Apply ToggleTaskListSpan only for areas without ClickableSpan

Signed-off-by: Stefan Niedermann <info@niedermann.it>
This commit is contained in:
Stefan Niedermann 2021-06-13 11:38:43 +02:00
parent 3ea6cf1226
commit db3513961c
5 changed files with 129 additions and 124 deletions

View file

@ -2,6 +2,7 @@ package it.niedermann.owncloud.notes.edit;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.graphics.Color;
import android.graphics.Typeface; import android.graphics.Typeface;
import android.os.Bundle; import android.os.Bundle;
import android.text.Layout; import android.text.Layout;

View file

@ -1,22 +1,19 @@
package it.niedermann.android.markdown.markwon.plugins; package it.niedermann.android.markdown.markwon.plugins;
import android.text.Spannable;
import android.text.style.URLSpan;
import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.commonmark.node.Link;
import java.util.Collection; import java.util.Collection;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.function.Function; import java.util.function.Function;
import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.MarkwonPlugin; import io.noties.markwon.MarkwonPlugin;
import io.noties.markwon.MarkwonSpansFactory;
import io.noties.markwon.core.CoreProps;
import it.niedermann.android.markdown.markwon.span.InterceptedURLSpan; import it.niedermann.android.markdown.markwon.span.InterceptedURLSpan;
import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
import static it.niedermann.android.markdown.MarkdownUtil.getContentAsSpannable;
public class LinkClickInterceptorPlugin extends AbstractMarkwonPlugin { public class LinkClickInterceptorPlugin extends AbstractMarkwonPlugin {
@NonNull @NonNull
@ -27,20 +24,9 @@ public class LinkClickInterceptorPlugin extends AbstractMarkwonPlugin {
} }
@Override @Override
public void afterSetText(@NonNull TextView textView) { public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
super.afterSetText(textView); super.configureSpansFactory(builder);
if (onLinkClickCallbacks.size() > 0) { builder.setFactory(Link.class, (configuration, props) -> new InterceptedURLSpan(onLinkClickCallbacks, CoreProps.LINK_DESTINATION.get(props)));
final Spannable spannable = getContentAsSpannable(textView);
final URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class);
for (URLSpan originalSpan : spans) {
final InterceptedURLSpan interceptedSpan = new InterceptedURLSpan(onLinkClickCallbacks, originalSpan.getURL());
final int start = spannable.getSpanStart(originalSpan);
final int end = spannable.getSpanEnd(originalSpan);
spannable.removeSpan(originalSpan);
spannable.setSpan(interceptedSpan, start, end, SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
} }
public void registerOnLinkClickCallback(@NonNull Function<String, Boolean> callback) { public void registerOnLinkClickCallback(@NonNull Function<String, Boolean> callback) {

View file

@ -1,19 +1,20 @@
package it.niedermann.android.markdown.markwon.plugins; package it.niedermann.android.markdown.markwon.plugins;
import android.util.Log; import android.text.Spannable;
import android.text.style.ClickableSpan;
import android.util.Range;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.commonmark.node.AbstractVisitor;
import org.commonmark.node.Block;
import org.commonmark.node.HardLineBreak;
import org.commonmark.node.Node; import org.commonmark.node.Node;
import org.commonmark.node.Paragraph;
import org.commonmark.node.SoftLineBreak;
import org.commonmark.node.Text;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.MarkwonVisitor; import io.noties.markwon.MarkwonVisitor;
@ -56,32 +57,6 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin {
.get(TaskListItem.class); .get(TaskListItem.class);
final Object spans = spanFactory == null ? null : final Object spans = spanFactory == null ? null :
spanFactory.getSpans(visitor.configuration(), visitor.renderProps()); spanFactory.getSpans(visitor.configuration(), visitor.renderProps());
if (spans != null) {
final TaskListSpan taskListSpan;
if (spans instanceof TaskListSpan[]) {
if (((TaskListSpan[]) spans).length > 0) {
taskListSpan = ((TaskListSpan[]) spans)[0];
} else {
taskListSpan = null;
}
} else if (spans instanceof TaskListSpan) {
taskListSpan = (TaskListSpan) spans;
} else {
taskListSpan = null;
}
final int content = TaskListContextVisitor.contentLength(node);
if (content > 0 && taskListSpan != null) {
// maybe additionally identify this task list (for persistence)
visitor.builder().setSpan(
new ToggleTaskListSpan(enabled, toggleListener, taskListSpan, visitor.builder().subSequence(length, length + content).toString()),
length,
length + content
);
}
}
SpannableBuilder.setSpans( SpannableBuilder.setSpans(
visitor.builder(), visitor.builder(),
spans, spans,
@ -95,48 +70,111 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin {
}); });
} }
static class TaskListContextVisitor extends AbstractVisitor {
private int contentLength = 0;
static int contentLength(Node node) {
final TaskListContextVisitor visitor = new TaskListContextVisitor();
visitor.visitChildren(node);
return visitor.contentLength;
}
@Override @Override
public void visit(Text text) { public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) {
super.visit(text); super.afterRender(node, visitor);
contentLength += text.getLiteral().length(); final Spannable spanned = visitor.builder().spannableStringBuilder();
final List<SpannableBuilder.Span> spans = visitor.builder().getSpans(0, visitor.builder().length());
final List<TaskListSpan> taskListSpans = spans.stream()
.filter(span -> span.what instanceof TaskListSpan)
.map(span -> ((TaskListSpan) span.what))
.sorted((o1, o2) -> spanned.getSpanStart(o1) - spanned.getSpanStart(o2))
.collect(Collectors.toList());
for (int position = 0; position < taskListSpans.size(); position++) {
final TaskListSpan taskListSpan = taskListSpans.get(position);
final int start = spanned.getSpanStart(taskListSpan);
// final int contentLength = TaskListContextVisitor.contentLength(node);
// final int end = start + contentLength;
final int end = spanned.getSpanEnd(taskListSpan);
final List<Range<Integer>> freeRanges = findFreeRanges(spanned, start, end);
for (Range<Integer> freeRange : freeRanges) {
visitor.builder().setSpan(
new ToggleTaskListSpan(enabled, toggleListener, taskListSpan, position),
freeRange.getLower(), freeRange.getUpper());
}
}
} }
// NB! if count both soft and hard breaks as having length of 1 /**
@Override * @return a list of ranges in the given {@param spanned} from {@param start} to {@param end} which is <strong>not</strong> taken for a {@link ClickableSpan}.
public void visit(SoftLineBreak softLineBreak) { */
super.visit(softLineBreak); @NonNull
contentLength += 1; private static List<Range<Integer>> findFreeRanges(@NonNull Spannable spanned, int start, int end) {
final List<Range<Integer>> freeRanges;
final List<ClickableSpan> clickableSpans = getClickableSpans(spanned, start, end);
if (clickableSpans.size() > 0) {
freeRanges = new ArrayList<>(clickableSpans.size());
int from = start;
for (ClickableSpan clickableSpan : clickableSpans) {
final int clickableStart = spanned.getSpanStart(clickableSpan);
final int clickableEnd = spanned.getSpanEnd(clickableSpan);
if (from != clickableStart) {
freeRanges.add(new Range<>(from, clickableStart));
}
from = clickableEnd;
}
if (clickableSpans.size() > 0) {
final int lastUpperBlocker = spanned.getSpanEnd(clickableSpans.get(clickableSpans.size() - 1));
if (lastUpperBlocker < end) {
freeRanges.add(new Range<>(lastUpperBlocker, end));
}
}
} else {
freeRanges = Collections.singletonList(new Range<>(start, end));
}
return freeRanges;
} }
// NB! if count both soft and hard breaks as having length of 1 @NonNull
@Override private static List<ClickableSpan> getClickableSpans(@NonNull Spannable spanned, int start, int end) {
public void visit(HardLineBreak hardLineBreak) { return Arrays.stream(spanned.getSpans(start, end, ClickableSpan.class))
super.visit(hardLineBreak); .sorted((o1, o2) -> spanned.getSpanStart(o1) - spanned.getSpanStart(o2))
contentLength += 1; .collect(Collectors.toList());
} }
@Override // static class TaskListContextVisitor extends AbstractVisitor {
protected void visitChildren(Node parent) { // private int contentLength = 0;
Node node = parent.getFirstChild(); //
while (node != null) { // static int contentLength(Node node) {
// A subclass of this visitor might modify the node, resulting in getNext returning a different node or no // final TaskListContextVisitor visitor = new TaskListContextVisitor();
// node after visiting it. So get the next node before visiting. // visitor.visitChildren(node);
Node next = node.getNext(); // return visitor.contentLength;
if (node instanceof Block && !(node instanceof Paragraph)) { // }
break; //
} // @Override
node.accept(this); // public void visit(Text text) {
node = next; // super.visit(text);
} // contentLength += text.getLiteral().length();
} // }
} //
// // NB! if count both soft and hard breaks as having length of 1
// @Override
// public void visit(SoftLineBreak softLineBreak) {
// super.visit(softLineBreak);
// contentLength += 1;
// }
//
// // NB! if count both soft and hard breaks as having length of 1
// @Override
// public void visit(HardLineBreak hardLineBreak) {
// super.visit(hardLineBreak);
// contentLength += 1;
// }
//
// @Override
// protected void visitChildren(Node parent) {
// Node node = parent.getFirstChild();
// while (node != null) {
// // A subclass of this visitor might modify the node, resulting in getNext returning a different node or no
// // node after visiting it. So get the next node before visiting.
// Node next = node.getNext();
// if (node instanceof Block && !(node instanceof Paragraph)) {
// break;
// }
// node.accept(this);
// node = next;
// }
// }
// }
} }

View file

@ -1,8 +1,10 @@
package it.niedermann.android.markdown.markwon.span; package it.niedermann.android.markdown.markwon.span;
import android.text.Spanned;
import android.text.style.URLSpan; import android.text.style.URLSpan;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -22,7 +24,6 @@ public class InterceptedURLSpan extends URLSpan {
super(url); super(url);
this.onLinkClickCallbacks = onLinkClickCallbacks; this.onLinkClickCallbacks = onLinkClickCallbacks;
} }
@Override @Override
public void onClick(View widget) { public void onClick(View widget) {
if (onLinkClickCallbacks.size() > 0) { if (onLinkClickCallbacks.size() > 0) {

View file

@ -1,15 +1,12 @@
package it.niedermann.android.markdown.markwon.span; package it.niedermann.android.markdown.markwon.span;
import android.text.Spanned;
import android.text.TextPaint; import android.text.TextPaint;
import android.text.style.ClickableSpan; import android.text.style.ClickableSpan;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
@ -19,42 +16,24 @@ public class ToggleTaskListSpan extends ClickableSpan {
private static final String TAG = ToggleTaskListSpan.class.getSimpleName(); private static final String TAG = ToggleTaskListSpan.class.getSimpleName();
final AtomicBoolean enabled; private final AtomicBoolean enabled;
final BiConsumer<Integer, Boolean> toggleListener; private final BiConsumer<Integer, Boolean> toggleListener;
final TaskListSpan span; private final TaskListSpan span;
final String content; private final int position;
public ToggleTaskListSpan(@NonNull AtomicBoolean enabled, @NonNull BiConsumer<Integer, Boolean> toggleListener, @NonNull TaskListSpan span, String content) { public ToggleTaskListSpan(@NonNull AtomicBoolean enabled, @NonNull BiConsumer<Integer, Boolean> toggleListener, @NonNull TaskListSpan span, int position) {
this.enabled = enabled; this.enabled = enabled;
this.toggleListener = toggleListener; this.toggleListener = toggleListener;
this.span = span; this.span = span;
this.content = content; this.position = position;
} }
@Override @Override
public void onClick(@NonNull View widget) { public void onClick(@NonNull View widget) {
if(enabled.get()) { if (enabled.get()) {
span.setDone(!span.isDone()); span.setDone(!span.isDone());
widget.invalidate(); widget.invalidate();
toggleListener.accept(position, span.isDone());
// it must be a TextView
final TextView textView = (TextView) widget;
// it must be spanned
// TODO what if textView is not a spanned?
final Spanned spanned = (Spanned) textView.getText();
final ClickableSpan[] toggles = spanned.getSpans(0, spanned.length(), getClass());
Arrays.sort(toggles, (o1, o2) -> spanned.getSpanStart(o1) - spanned.getSpanStart(o2));
int currentTogglePosition = -1;
for (int i = 0; i < toggles.length; i++) {
if (spanned.getSpanStart(toggles[i]) == spanned.getSpanStart(this) && spanned.getSpanEnd(toggles[i]) == spanned.getSpanEnd(this)) {
currentTogglePosition = i;
break;
}
}
toggleListener.accept(currentTogglePosition, span.isDone());
} else { } else {
Log.w(TAG, "Prevented toggling checkbox because the view is disabled"); Log.w(TAG, "Prevented toggling checkbox because the view is disabled");
} }