mirror of
https://github.com/nextcloud/notes-android.git
synced 2024-11-29 11:59:07 +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.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;
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue