mirror of
https://github.com/nextcloud/notes-android.git
synced 2024-11-25 14:26:13 +03:00
Apply ToggleTaskListSpan only for areas without ClickableSpan
Signed-off-by: Stefan Niedermann <info@niedermann.it>
This commit is contained in:
parent
3ea6cf1226
commit
db3513961c
5 changed files with 129 additions and 124 deletions
|
@ -2,6 +2,7 @@ package it.niedermann.owncloud.notes.edit;
|
|||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Bundle;
|
||||
import android.text.Layout;
|
||||
|
|
|
@ -1,22 +1,19 @@
|
|||
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 org.commonmark.node.Link;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.function.Function;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
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 static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
|
||||
import static it.niedermann.android.markdown.MarkdownUtil.getContentAsSpannable;
|
||||
|
||||
public class LinkClickInterceptorPlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
@NonNull
|
||||
|
@ -27,20 +24,9 @@ public class LinkClickInterceptorPlugin extends AbstractMarkwonPlugin {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void afterSetText(@NonNull TextView textView) {
|
||||
super.afterSetText(textView);
|
||||
if (onLinkClickCallbacks.size() > 0) {
|
||||
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 configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||
super.configureSpansFactory(builder);
|
||||
builder.setFactory(Link.class, (configuration, props) -> new InterceptedURLSpan(onLinkClickCallbacks, CoreProps.LINK_DESTINATION.get(props)));
|
||||
}
|
||||
|
||||
public void registerOnLinkClickCallback(@NonNull Function<String, Boolean> callback) {
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
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 org.commonmark.node.AbstractVisitor;
|
||||
import org.commonmark.node.Block;
|
||||
import org.commonmark.node.HardLineBreak;
|
||||
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.function.BiConsumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.MarkwonVisitor;
|
||||
|
@ -56,32 +57,6 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin {
|
|||
.get(TaskListItem.class);
|
||||
final Object spans = spanFactory == null ? null :
|
||||
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(
|
||||
visitor.builder(),
|
||||
spans,
|
||||
|
@ -95,48 +70,111 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin {
|
|||
});
|
||||
}
|
||||
|
||||
static class TaskListContextVisitor extends AbstractVisitor {
|
||||
private int contentLength = 0;
|
||||
@Override
|
||||
public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) {
|
||||
super.afterRender(node, visitor);
|
||||
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());
|
||||
|
||||
static int contentLength(Node node) {
|
||||
final TaskListContextVisitor visitor = new TaskListContextVisitor();
|
||||
visitor.visitChildren(node);
|
||||
return visitor.contentLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Text text) {
|
||||
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;
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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}.
|
||||
*/
|
||||
@NonNull
|
||||
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;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static List<ClickableSpan> getClickableSpans(@NonNull Spannable spanned, int start, int end) {
|
||||
return Arrays.stream(spanned.getSpans(start, end, ClickableSpan.class))
|
||||
.sorted((o1, o2) -> spanned.getSpanStart(o1) - spanned.getSpanStart(o2))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// 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
|
||||
// public void visit(Text text) {
|
||||
// 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;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package it.niedermann.android.markdown.markwon.span;
|
||||
|
||||
import android.text.Spanned;
|
||||
import android.text.style.URLSpan;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
|
@ -22,7 +24,6 @@ public class InterceptedURLSpan extends URLSpan {
|
|||
super(url);
|
||||
this.onLinkClickCallbacks = onLinkClickCallbacks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View widget) {
|
||||
if (onLinkClickCallbacks.size() > 0) {
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
package it.niedermann.android.markdown.markwon.span;
|
||||
|
||||
import android.text.Spanned;
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
|
@ -19,42 +16,24 @@ public class ToggleTaskListSpan extends ClickableSpan {
|
|||
|
||||
private static final String TAG = ToggleTaskListSpan.class.getSimpleName();
|
||||
|
||||
final AtomicBoolean enabled;
|
||||
final BiConsumer<Integer, Boolean> toggleListener;
|
||||
final TaskListSpan span;
|
||||
final String content;
|
||||
private final AtomicBoolean enabled;
|
||||
private final BiConsumer<Integer, Boolean> toggleListener;
|
||||
private final TaskListSpan span;
|
||||
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.toggleListener = toggleListener;
|
||||
this.span = span;
|
||||
this.content = content;
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(@NonNull View widget) {
|
||||
if(enabled.get()) {
|
||||
if (enabled.get()) {
|
||||
span.setDone(!span.isDone());
|
||||
widget.invalidate();
|
||||
|
||||
// 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());
|
||||
toggleListener.accept(position, span.isDone());
|
||||
} else {
|
||||
Log.w(TAG, "Prevented toggling checkbox because the view is disabled");
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue