2019-09-27 17:05:34 +00:00
|
|
|
package eu.faircode.email;
|
|
|
|
|
2021-01-01 07:56:36 +00:00
|
|
|
/*
|
|
|
|
This file is part of FairEmail.
|
|
|
|
|
|
|
|
FairEmail is free software: you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU General Public License as published by
|
|
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
FairEmail is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
along with FairEmail. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
2023-01-01 07:52:55 +00:00
|
|
|
Copyright 2018-2023 by Marcel Bokhorst (M66B)
|
2021-01-01 07:56:36 +00:00
|
|
|
*/
|
|
|
|
|
2020-08-12 08:25:08 +00:00
|
|
|
import android.content.Context;
|
2020-06-28 21:36:31 +00:00
|
|
|
import android.content.DialogInterface;
|
2022-02-12 14:16:14 +00:00
|
|
|
import android.content.Intent;
|
2020-08-14 18:15:52 +00:00
|
|
|
import android.content.SharedPreferences;
|
2022-05-04 09:30:00 +00:00
|
|
|
import android.graphics.Color;
|
2019-09-27 17:05:34 +00:00
|
|
|
import android.graphics.Typeface;
|
2020-08-12 06:18:39 +00:00
|
|
|
import android.os.Build;
|
2023-01-01 16:41:07 +00:00
|
|
|
import android.os.Bundle;
|
2020-11-17 18:37:56 +00:00
|
|
|
import android.text.Editable;
|
2020-10-03 09:37:43 +00:00
|
|
|
import android.text.Layout;
|
2022-03-15 08:31:04 +00:00
|
|
|
import android.text.NoCopySpan;
|
|
|
|
import android.text.Spannable;
|
2021-05-10 20:10:25 +00:00
|
|
|
import android.text.SpannableStringBuilder;
|
2019-09-27 17:05:34 +00:00
|
|
|
import android.text.Spanned;
|
2022-05-04 09:30:00 +00:00
|
|
|
import android.text.TextPaint;
|
2020-10-03 09:37:43 +00:00
|
|
|
import android.text.TextUtils;
|
2022-12-08 18:47:18 +00:00
|
|
|
import android.text.TextWatcher;
|
2020-10-03 09:37:43 +00:00
|
|
|
import android.text.style.AlignmentSpan;
|
2021-07-04 11:17:15 +00:00
|
|
|
import android.text.style.BackgroundColorSpan;
|
2020-08-11 16:46:26 +00:00
|
|
|
import android.text.style.BulletSpan;
|
2020-11-17 19:22:34 +00:00
|
|
|
import android.text.style.CharacterStyle;
|
2019-09-27 17:05:34 +00:00
|
|
|
import android.text.style.ForegroundColorSpan;
|
2020-11-17 18:37:56 +00:00
|
|
|
import android.text.style.ParagraphStyle;
|
2020-10-01 08:22:20 +00:00
|
|
|
import android.text.style.QuoteSpan;
|
2019-09-27 17:05:34 +00:00
|
|
|
import android.text.style.RelativeSizeSpan;
|
2020-10-01 08:31:58 +00:00
|
|
|
import android.text.style.StrikethroughSpan;
|
2019-09-27 17:05:34 +00:00
|
|
|
import android.text.style.StyleSpan;
|
2020-06-28 16:39:27 +00:00
|
|
|
import android.text.style.TypefaceSpan;
|
2019-09-27 17:05:34 +00:00
|
|
|
import android.text.style.URLSpan;
|
|
|
|
import android.text.style.UnderlineSpan;
|
2022-12-08 18:47:18 +00:00
|
|
|
import android.util.LogPrinter;
|
2020-10-03 12:17:13 +00:00
|
|
|
import android.util.Pair;
|
2020-06-28 15:18:42 +00:00
|
|
|
import android.view.MenuItem;
|
|
|
|
import android.view.View;
|
2019-09-27 17:05:34 +00:00
|
|
|
import android.widget.EditText;
|
2022-09-20 08:24:21 +00:00
|
|
|
import android.widget.TextView;
|
2022-10-17 09:00:49 +00:00
|
|
|
import android.widget.Toast;
|
2019-09-27 17:05:34 +00:00
|
|
|
|
2022-02-10 15:05:52 +00:00
|
|
|
import androidx.annotation.NonNull;
|
2020-06-28 15:18:42 +00:00
|
|
|
import androidx.appcompat.widget.PopupMenu;
|
2021-06-01 11:34:57 +00:00
|
|
|
import androidx.core.content.res.ResourcesCompat;
|
2020-11-12 14:08:34 +00:00
|
|
|
import androidx.lifecycle.LifecycleOwner;
|
2020-08-14 18:15:52 +00:00
|
|
|
import androidx.preference.PreferenceManager;
|
2020-06-28 15:18:42 +00:00
|
|
|
|
2020-06-28 21:36:31 +00:00
|
|
|
import com.flask.colorpicker.ColorPickerView;
|
|
|
|
import com.flask.colorpicker.builder.ColorPickerClickListener;
|
|
|
|
import com.flask.colorpicker.builder.ColorPickerDialogBuilder;
|
|
|
|
|
2019-09-27 17:05:34 +00:00
|
|
|
import java.util.ArrayList;
|
2021-09-23 14:17:49 +00:00
|
|
|
import java.util.Arrays;
|
|
|
|
import java.util.Collections;
|
2019-09-27 17:05:34 +00:00
|
|
|
import java.util.List;
|
2020-10-03 09:37:43 +00:00
|
|
|
import java.util.Locale;
|
2019-09-27 17:05:34 +00:00
|
|
|
|
|
|
|
public class StyleHelper {
|
2022-10-22 17:51:55 +00:00
|
|
|
private static final List<Class<?>> CLEAR_STYLES = Collections.unmodifiableList(Arrays.asList(
|
2021-09-23 14:17:49 +00:00
|
|
|
StyleSpan.class,
|
|
|
|
UnderlineSpan.class,
|
|
|
|
RelativeSizeSpan.class,
|
|
|
|
BackgroundColorSpan.class,
|
|
|
|
ForegroundColorSpan.class,
|
2022-12-07 09:16:50 +00:00
|
|
|
AlignmentSpan.class, AlignmentSpan.Standard.class,
|
2022-05-21 19:13:05 +00:00
|
|
|
BulletSpanEx.class, NumberSpan.class,
|
2021-09-23 14:17:49 +00:00
|
|
|
QuoteSpan.class, IndentSpan.class,
|
2022-10-15 08:41:07 +00:00
|
|
|
SubscriptSpanEx.class, SuperscriptSpanEx.class,
|
2021-09-23 14:17:49 +00:00
|
|
|
StrikethroughSpan.class,
|
|
|
|
URLSpan.class,
|
2022-03-15 08:31:04 +00:00
|
|
|
TypefaceSpan.class, CustomTypefaceSpan.class,
|
2022-05-04 09:30:00 +00:00
|
|
|
MarkSpan.class,
|
2022-05-03 11:13:26 +00:00
|
|
|
InsertedSpan.class
|
2021-09-23 14:17:49 +00:00
|
|
|
));
|
|
|
|
|
2022-12-07 11:32:16 +00:00
|
|
|
private static Integer[] ids = new Integer[]{
|
|
|
|
R.id.menu_bold,
|
|
|
|
R.id.menu_italic,
|
|
|
|
R.id.menu_underline,
|
|
|
|
R.id.menu_style_size,
|
|
|
|
R.id.menu_style_background,
|
|
|
|
R.id.menu_style_color,
|
|
|
|
R.id.menu_style_font,
|
|
|
|
R.id.menu_style_align,
|
|
|
|
R.id.menu_style_list,
|
|
|
|
R.id.menu_style_indentation,
|
|
|
|
R.id.menu_style_blockquote,
|
|
|
|
R.id.menu_style_mark,
|
|
|
|
R.id.menu_style_subscript,
|
|
|
|
R.id.menu_style_superscript,
|
|
|
|
R.id.menu_style_strikethrough,
|
2022-12-27 08:31:13 +00:00
|
|
|
R.id.menu_style_insert_line,
|
2023-01-01 16:41:07 +00:00
|
|
|
R.id.menu_style_spell_check,
|
2022-12-07 11:32:16 +00:00
|
|
|
R.id.menu_style_password,
|
|
|
|
R.id.menu_style_code,
|
|
|
|
R.id.menu_style_clear
|
|
|
|
};
|
|
|
|
|
2022-12-08 08:47:14 +00:00
|
|
|
private static final int group_style_size = 1;
|
|
|
|
private static final int group_style_font_standard = 2;
|
|
|
|
private static final int group_style_font_custom = 3;
|
|
|
|
private static final int group_style_align = 4;
|
|
|
|
private static final int group_style_list = 5;
|
|
|
|
private static final int group_style_indentation = 6;
|
|
|
|
|
2022-12-07 11:32:16 +00:00
|
|
|
static void wire(LifecycleOwner owner, View view, EditText etBody) {
|
|
|
|
View.OnClickListener styleListener = new View.OnClickListener() {
|
|
|
|
@Override
|
|
|
|
public void onClick(View v) {
|
2022-12-09 07:59:13 +00:00
|
|
|
StyleHelper.apply(v.getId(), owner, v, etBody);
|
2022-12-07 11:32:16 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
for (int id : ids) {
|
|
|
|
View v = view.findViewById(id);
|
|
|
|
|
|
|
|
v.setOnClickListener(styleListener);
|
|
|
|
|
2023-01-01 16:41:07 +00:00
|
|
|
if (id == R.id.menu_style_spell_check)
|
|
|
|
v.setVisibility(
|
|
|
|
BuildConfig.DEBUG && LanguageTool.isEnabled(v.getContext())
|
|
|
|
? View.VISIBLE : View.GONE);
|
|
|
|
else if (id == R.id.menu_style_password)
|
2022-12-07 11:32:16 +00:00
|
|
|
v.setVisibility(
|
|
|
|
!BuildConfig.PLAY_STORE_RELEASE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
|
|
|
? View.VISIBLE : View.GONE);
|
|
|
|
else if (id == R.id.menu_style_code)
|
|
|
|
v.setVisibility(BuildConfig.DEBUG ? View.VISIBLE : View.GONE);
|
|
|
|
}
|
2022-12-13 19:02:14 +00:00
|
|
|
|
|
|
|
view.findViewById(R.id.menu_link).setVisibility(View.GONE);
|
2022-12-07 11:32:16 +00:00
|
|
|
}
|
|
|
|
|
2022-12-08 18:47:18 +00:00
|
|
|
static TextWatcher getTextWatcher(EditText etBody) {
|
|
|
|
// https://developer.android.com/reference/android/text/TextWatcher
|
|
|
|
return new TextWatcher() {
|
|
|
|
private Integer added = null;
|
|
|
|
private Integer removed = null;
|
|
|
|
private Integer inserted = null;
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void beforeTextChanged(CharSequence text, int start, int count, int after) {
|
|
|
|
if (count == 1 && after == 0 && (start == 0 || text.charAt(start) == '\n')) {
|
|
|
|
Log.i("Removed=" + start);
|
|
|
|
removed = start;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (BuildConfig.DEBUG && count - after == 1 && start + after > 0) {
|
|
|
|
int replaced = start + after;
|
|
|
|
Spanned spanned = ((Spanned) text);
|
|
|
|
StyleHelper.InsertedSpan[] spans =
|
|
|
|
spanned.getSpans(replaced, replaced, StyleHelper.InsertedSpan.class);
|
|
|
|
for (StyleHelper.InsertedSpan span : spans) {
|
|
|
|
int end = spanned.getSpanEnd(span);
|
|
|
|
Log.i("Replaced=" + replaced);
|
|
|
|
if (end - 1 == replaced) {
|
|
|
|
inserted = end - 1;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onTextChanged(CharSequence text, int start, int before, int count) {
|
|
|
|
int index = start + before;
|
|
|
|
|
|
|
|
if (count - before == 1 && index > 0) {
|
|
|
|
char c = text.charAt(index);
|
|
|
|
if (c == '\n') {
|
|
|
|
Log.i("Added=" + index);
|
|
|
|
added = index;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void afterTextChanged(Editable text) {
|
|
|
|
if (etBody == null)
|
|
|
|
return;
|
|
|
|
|
|
|
|
LogPrinter lp = null;
|
|
|
|
if (BuildConfig.DEBUG &&
|
|
|
|
(added != null || removed != null))
|
|
|
|
lp = new LogPrinter(android.util.Log.INFO, "FairEmail");
|
|
|
|
|
|
|
|
if (lp != null)
|
|
|
|
TextUtils.dumpSpans(text, new LogPrinter(android.util.Log.INFO, "FairEmail"), "---before>");
|
|
|
|
|
|
|
|
if (added != null)
|
|
|
|
try {
|
|
|
|
// break block quotes
|
|
|
|
boolean broken = false;
|
|
|
|
QuoteSpan[] spans = text.getSpans(added + 1, added + 1, QuoteSpan.class);
|
|
|
|
for (QuoteSpan span : spans) {
|
|
|
|
int s = text.getSpanStart(span);
|
|
|
|
int e = text.getSpanEnd(span);
|
|
|
|
int f = text.getSpanFlags(span);
|
|
|
|
Log.i(span + " " + s + "..." + e + " added=" + added);
|
|
|
|
|
|
|
|
if (s > 0 && added - s > 0 && e - (added + 1) > 0 &&
|
|
|
|
text.charAt(s - 1) == '\n' && text.charAt(added - 1) == '\n' &&
|
|
|
|
text.charAt(added) == '\n' && text.charAt(e - 1) == '\n') {
|
|
|
|
broken = true;
|
|
|
|
|
|
|
|
QuoteSpan q1 = StyleHelper.clone(span, QuoteSpan.class, etBody.getContext());
|
|
|
|
text.setSpan(q1, s, added, f);
|
|
|
|
Log.i(span + " " + s + "..." + added);
|
|
|
|
|
|
|
|
QuoteSpan q2 = StyleHelper.clone(span, QuoteSpan.class, etBody.getContext());
|
|
|
|
text.setSpan(q2, added + 1, e, f);
|
|
|
|
Log.i(span + " " + (added + 1) + "..." + e);
|
|
|
|
|
|
|
|
text.removeSpan(span);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (broken) {
|
|
|
|
CharacterStyle[] sspan = text.getSpans(added + 1, added + 1, CharacterStyle.class);
|
|
|
|
for (CharacterStyle span : sspan) {
|
|
|
|
int s = text.getSpanStart(span);
|
|
|
|
int e = text.getSpanEnd(span);
|
|
|
|
int f = text.getSpanFlags(span);
|
|
|
|
Log.i(span + " " + s + "..." + e + " start=" + added);
|
|
|
|
|
|
|
|
if (s <= added && added + 1 <= e) {
|
|
|
|
CharacterStyle s1 = CharacterStyle.wrap(span);
|
|
|
|
text.setSpan(s1, s, added, f);
|
|
|
|
Log.i(span + " " + s + "..." + added);
|
|
|
|
|
|
|
|
CharacterStyle s2 = CharacterStyle.wrap(span);
|
|
|
|
text.setSpan(s2, added + 1, e, f);
|
|
|
|
Log.i(span + " " + (added + 1) + "..." + e);
|
|
|
|
|
|
|
|
text.removeSpan(span);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
etBody.setSelection(added);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Escape indent at end
|
|
|
|
IndentSpan[] indents = text.getSpans(added + 1, added + 1, IndentSpan.class);
|
|
|
|
for (IndentSpan indent : indents) {
|
|
|
|
int s = text.getSpanStart(indent);
|
|
|
|
int e = text.getSpanEnd(indent);
|
|
|
|
int f = text.getSpanFlags(indent);
|
|
|
|
if (e - 1 > s && added + 1 == e) {
|
|
|
|
text.removeSpan(indent);
|
|
|
|
text.setSpan(new IndentSpan(indent.getLeadingMargin(true)), s, e - 1, f);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
boolean renum = false;
|
|
|
|
BulletSpan[] bullets = text.getSpans(added + 1, added + 1, BulletSpan.class);
|
|
|
|
|
|
|
|
int len = 0;
|
|
|
|
BulletSpan shortest = null;
|
|
|
|
for (BulletSpan span : bullets) {
|
|
|
|
int s = text.getSpanStart(span);
|
|
|
|
int e = text.getSpanEnd(span);
|
|
|
|
if (shortest == null || e - s < len) {
|
|
|
|
shortest = span;
|
|
|
|
len = e - s;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (shortest != null) {
|
|
|
|
int s = text.getSpanStart(shortest);
|
|
|
|
int e = text.getSpanEnd(shortest);
|
|
|
|
int f = text.getSpanFlags(shortest) | Spanned.SPAN_PARAGRAPH;
|
|
|
|
Log.i(shortest + " " + s + "..." + e + " added=" + added);
|
|
|
|
|
|
|
|
if (s > 0 &&
|
|
|
|
added + 1 > s && e > added + 1 &&
|
|
|
|
text.charAt(s - 1) == '\n' && text.charAt(e - 1) == '\n') {
|
|
|
|
if (e - s > 2) {
|
|
|
|
BulletSpan b1 = StyleHelper.clone(shortest, shortest.getClass(), etBody.getContext());
|
|
|
|
text.setSpan(b1, s, added + 1, f);
|
|
|
|
Log.i(shortest + " " + s + "..." + (added + 1));
|
|
|
|
|
|
|
|
BulletSpan b2 = StyleHelper.clone(b1, shortest.getClass(), etBody.getContext());
|
|
|
|
text.setSpan(b2, added + 1, e, f);
|
|
|
|
Log.i(shortest + " " + (added + 1) + "..." + e);
|
|
|
|
}
|
|
|
|
|
|
|
|
renum = true;
|
|
|
|
text.removeSpan(shortest);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (renum)
|
|
|
|
StyleHelper.renumber(text, false, etBody.getContext());
|
2022-12-29 21:28:06 +00:00
|
|
|
|
|
|
|
if (BuildConfig.DEBUG) {
|
|
|
|
StyleHelper.InsertedSpan[] inserts =
|
|
|
|
text.getSpans(0, text.length(), StyleHelper.InsertedSpan.class);
|
|
|
|
for (StyleHelper.InsertedSpan span : inserts)
|
|
|
|
text.removeSpan(span);
|
|
|
|
}
|
2022-12-08 18:47:18 +00:00
|
|
|
} catch (Throwable ex) {
|
|
|
|
Log.e(ex);
|
|
|
|
} finally {
|
|
|
|
added = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (removed != null)
|
|
|
|
try {
|
|
|
|
ParagraphStyle[] ps = text.getSpans(removed, removed + 1, ParagraphStyle.class);
|
|
|
|
if (ps != null)
|
|
|
|
for (ParagraphStyle p : ps) {
|
|
|
|
int start = text.getSpanStart(p);
|
|
|
|
int end = text.getSpanEnd(p);
|
|
|
|
if (start >= removed && end <= removed + 1)
|
|
|
|
text.removeSpan(p);
|
|
|
|
}
|
|
|
|
|
|
|
|
StyleHelper.renumber(text, true, etBody.getContext());
|
|
|
|
} finally {
|
|
|
|
removed = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (inserted != null)
|
|
|
|
try {
|
|
|
|
StyleHelper.InsertedSpan[] spans =
|
|
|
|
text.getSpans(inserted, inserted, StyleHelper.InsertedSpan.class);
|
|
|
|
for (StyleHelper.InsertedSpan span : spans) {
|
|
|
|
int start = text.getSpanStart(span);
|
|
|
|
int end = text.getSpanEnd(span);
|
|
|
|
if (end == inserted) {
|
|
|
|
text.delete(start, end);
|
|
|
|
text.removeSpan(span);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} finally {
|
|
|
|
inserted = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (lp != null)
|
|
|
|
TextUtils.dumpSpans(text, lp, "---after>");
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-12-09 07:59:13 +00:00
|
|
|
static boolean apply(int itemId, LifecycleOwner owner, View anchor, EditText etBody, Object... args) {
|
|
|
|
return apply(-1, itemId, owner, anchor, etBody, args);
|
|
|
|
}
|
|
|
|
|
2022-12-07 09:15:00 +00:00
|
|
|
static boolean apply(int groupId, int itemId, LifecycleOwner owner, View anchor, EditText etBody, Object... args) {
|
|
|
|
Log.i("Style action=" + groupId + ":" + itemId);
|
2019-09-27 17:05:34 +00:00
|
|
|
|
|
|
|
try {
|
2022-12-08 08:47:14 +00:00
|
|
|
int start = etBody.getSelectionStart();
|
|
|
|
int end = etBody.getSelectionEnd();
|
|
|
|
|
|
|
|
if (start < 0)
|
|
|
|
start = 0;
|
|
|
|
if (end < 0)
|
|
|
|
end = 0;
|
|
|
|
|
|
|
|
if (start > end) {
|
|
|
|
int tmp = start;
|
|
|
|
start = end;
|
|
|
|
end = tmp;
|
2019-09-27 17:05:34 +00:00
|
|
|
}
|
|
|
|
|
2023-01-01 16:41:07 +00:00
|
|
|
if (start == end &&
|
|
|
|
itemId == R.id.menu_style_spell_check) {
|
|
|
|
Pair<Integer, Integer> paragraph = getParagraph(etBody);
|
|
|
|
if (paragraph == null)
|
|
|
|
return false;
|
|
|
|
start = paragraph.first;
|
|
|
|
end = paragraph.second;
|
|
|
|
}
|
|
|
|
|
2022-12-08 08:47:14 +00:00
|
|
|
if (start == end &&
|
2022-12-16 07:54:35 +00:00
|
|
|
itemId != R.id.menu_link &&
|
2022-12-08 07:01:45 +00:00
|
|
|
itemId != R.id.menu_clear &&
|
2022-12-08 08:47:14 +00:00
|
|
|
itemId != R.id.menu_style_align && groupId != group_style_align &&
|
|
|
|
itemId != R.id.menu_style_list && groupId != group_style_list &&
|
|
|
|
itemId != R.id.menu_style_indentation && groupId != group_style_indentation &&
|
2022-12-27 08:31:13 +00:00
|
|
|
itemId != R.id.menu_style_blockquote &&
|
|
|
|
itemId != R.id.menu_style_insert_line) {
|
2022-12-07 12:31:22 +00:00
|
|
|
Pair<Integer, Integer> word = getWord(etBody);
|
|
|
|
if (word == null)
|
|
|
|
return false;
|
2022-12-08 08:47:14 +00:00
|
|
|
start = word.first;
|
|
|
|
end = word.second;
|
2022-12-07 12:31:22 +00:00
|
|
|
}
|
|
|
|
|
2022-12-08 08:47:14 +00:00
|
|
|
if (groupId < 0) {
|
|
|
|
if (itemId == R.id.menu_bold || itemId == R.id.menu_italic)
|
|
|
|
return setBoldItalic(itemId, etBody, start, end, false);
|
|
|
|
else if (itemId == R.id.menu_underline)
|
|
|
|
return setUnderline(etBody, start, end, false);
|
|
|
|
else if (itemId == R.id.menu_style_size)
|
|
|
|
return selectSize(owner, anchor, etBody);
|
|
|
|
else if (itemId == R.id.menu_style_background)
|
|
|
|
return selectBackground(etBody, start, end);
|
|
|
|
else if (itemId == R.id.menu_style_color)
|
|
|
|
return selectColor(etBody, start, end);
|
|
|
|
else if (itemId == R.id.menu_style_font)
|
|
|
|
return selectFont(owner, anchor, etBody, start, end);
|
|
|
|
else if (itemId == R.id.menu_style_align)
|
|
|
|
return selectAlignment(owner, anchor, etBody, start, end);
|
|
|
|
else if (itemId == R.id.menu_style_list)
|
|
|
|
return selectList(owner, anchor, etBody, start, end);
|
|
|
|
else if (itemId == R.id.menu_style_indentation)
|
|
|
|
return selectIndentation(owner, anchor, etBody, start, end);
|
|
|
|
else if (itemId == R.id.menu_style_blockquote) {
|
|
|
|
if (start == end) {
|
|
|
|
Pair<Integer, Integer> block = StyleHelper.getParagraph(etBody, true);
|
|
|
|
if (block == null)
|
|
|
|
return false;
|
|
|
|
return StyleHelper.setBlockQuote(etBody, block.first, block.second, false);
|
|
|
|
} else
|
2022-12-09 07:54:45 +00:00
|
|
|
return setBlockQuote(etBody, start, end, false);
|
2022-12-08 08:47:14 +00:00
|
|
|
} else if (itemId == R.id.menu_style_mark)
|
|
|
|
return setMark(etBody, start, end, false);
|
|
|
|
else if (itemId == R.id.menu_style_subscript)
|
|
|
|
return setSubscript(etBody, start, end, false);
|
|
|
|
else if (itemId == R.id.menu_style_superscript)
|
|
|
|
return setSuperscript(etBody, start, end, false);
|
|
|
|
else if (itemId == R.id.menu_style_strikethrough)
|
|
|
|
return setStrikeThrough(etBody, start, end, false);
|
2022-12-27 08:31:13 +00:00
|
|
|
else if (itemId == R.id.menu_style_insert_line)
|
|
|
|
return setLine(etBody, end);
|
2023-01-01 16:41:07 +00:00
|
|
|
else if (itemId == R.id.menu_style_spell_check)
|
|
|
|
return spellCheck(owner, etBody, start, end);
|
2022-12-08 08:47:14 +00:00
|
|
|
else if (itemId == R.id.menu_style_password)
|
|
|
|
return setPassword(owner, etBody, start, end);
|
|
|
|
else if (itemId == R.id.menu_style_code) {
|
|
|
|
Log.breadcrumb("style", "action", "code");
|
2022-12-09 07:54:45 +00:00
|
|
|
setSize(etBody, start, end, HtmlHelper.FONT_SMALL, false);
|
|
|
|
setFont(etBody, start, end, "monospace", false);
|
2022-12-08 08:47:14 +00:00
|
|
|
return true;
|
|
|
|
} else if (itemId == R.id.menu_link)
|
|
|
|
return setLink(etBody, start, end, args);
|
|
|
|
else if (itemId == R.id.menu_style_clear)
|
2022-12-08 10:08:48 +00:00
|
|
|
return clear(etBody, start, end, false);
|
2022-12-08 08:47:14 +00:00
|
|
|
else if (itemId == R.id.menu_clear)
|
2022-12-08 10:08:48 +00:00
|
|
|
return clearAll(etBody, start, end, false);
|
2022-12-08 08:47:14 +00:00
|
|
|
} else {
|
|
|
|
switch (groupId) {
|
|
|
|
case group_style_size: {
|
|
|
|
Float size;
|
|
|
|
if (itemId == 1)
|
|
|
|
size = HtmlHelper.FONT_XSMALL;
|
|
|
|
else if (itemId == 2)
|
|
|
|
size = HtmlHelper.FONT_SMALL;
|
|
|
|
else if (itemId == 4)
|
|
|
|
size = HtmlHelper.FONT_LARGE;
|
|
|
|
else if (itemId == 5)
|
|
|
|
size = HtmlHelper.FONT_XLARGE;
|
|
|
|
else
|
|
|
|
size = null;
|
2022-12-07 11:32:16 +00:00
|
|
|
|
2022-12-09 07:54:45 +00:00
|
|
|
return setSize(etBody, start, end, size, false);
|
2022-12-07 11:32:16 +00:00
|
|
|
}
|
|
|
|
|
2022-12-08 08:47:14 +00:00
|
|
|
case group_style_font_standard:
|
|
|
|
case group_style_font_custom:
|
2022-12-09 07:54:45 +00:00
|
|
|
return setFont(etBody, start, end, (String) args[0], false);
|
2022-12-08 08:47:14 +00:00
|
|
|
|
|
|
|
case group_style_align: {
|
|
|
|
if (start == end) {
|
|
|
|
Pair<Integer, Integer> block = StyleHelper.getParagraph(etBody, true);
|
|
|
|
if (block == null)
|
|
|
|
return false;
|
|
|
|
return setAlignment(itemId, etBody, block.first, block.second, false);
|
|
|
|
} else
|
2022-12-09 07:54:45 +00:00
|
|
|
return setAlignment(itemId, etBody, start, end, false);
|
2022-12-07 11:32:16 +00:00
|
|
|
}
|
2022-12-07 12:31:22 +00:00
|
|
|
|
2022-12-08 08:47:14 +00:00
|
|
|
case group_style_list: {
|
|
|
|
boolean level = (itemId == R.id.menu_style_list_decrease || itemId == R.id.menu_style_list_increase);
|
|
|
|
if (start == end) {
|
|
|
|
Pair<Integer, Integer> p = StyleHelper.getParagraph(etBody, false);
|
|
|
|
if (p == null)
|
|
|
|
return false;
|
|
|
|
if (level)
|
|
|
|
return StyleHelper.setListLevel(itemId, etBody, p.first, p.second, false);
|
|
|
|
else
|
|
|
|
return StyleHelper.setList(itemId, etBody, p.first, p.second, false);
|
|
|
|
} else {
|
|
|
|
if (level)
|
2022-12-09 07:54:45 +00:00
|
|
|
return setListLevel(itemId, etBody, start, end, false);
|
2022-12-08 08:47:14 +00:00
|
|
|
else
|
2022-12-09 07:54:45 +00:00
|
|
|
return setList(itemId, etBody, start, end, false);
|
2022-12-08 08:47:14 +00:00
|
|
|
}
|
2022-12-07 11:32:16 +00:00
|
|
|
}
|
|
|
|
|
2022-12-08 08:47:14 +00:00
|
|
|
case group_style_indentation: {
|
|
|
|
if (start == end) {
|
|
|
|
Pair<Integer, Integer> block = StyleHelper.getParagraph(etBody, true);
|
|
|
|
if (block == null)
|
|
|
|
return false;
|
|
|
|
return StyleHelper.setIndentation(itemId, etBody, block.first, block.second, false);
|
|
|
|
} else
|
2022-12-09 07:54:45 +00:00
|
|
|
return setIndentation(itemId, etBody, start, end, false);
|
2021-05-11 06:08:49 +00:00
|
|
|
}
|
|
|
|
}
|
2022-12-08 08:47:14 +00:00
|
|
|
}
|
|
|
|
} catch (Throwable ex) {
|
|
|
|
Log.e(ex);
|
|
|
|
}
|
2021-02-05 10:15:02 +00:00
|
|
|
|
2022-12-08 08:47:14 +00:00
|
|
|
return false;
|
|
|
|
}
|
2021-07-24 07:46:56 +00:00
|
|
|
|
2022-12-08 08:47:14 +00:00
|
|
|
static boolean setBoldItalic(int itemId, EditText etBody, int start, int end, boolean select) {
|
|
|
|
String name = (itemId == R.id.menu_bold ? "bold" : "italic");
|
|
|
|
Log.breadcrumb("style", "action", name);
|
2021-05-11 06:08:49 +00:00
|
|
|
|
2022-12-08 08:47:14 +00:00
|
|
|
boolean has = false;
|
|
|
|
Editable edit = etBody.getText();
|
|
|
|
int style = (itemId == R.id.menu_bold ? Typeface.BOLD : Typeface.ITALIC);
|
|
|
|
StyleSpan[] spans = edit.getSpans(start, end, StyleSpan.class);
|
|
|
|
for (StyleSpan span : spans)
|
|
|
|
if (span.getStyle() == style) {
|
|
|
|
int s = edit.getSpanStart(span);
|
|
|
|
int e = edit.getSpanEnd(span);
|
|
|
|
int f = edit.getSpanFlags(span);
|
|
|
|
edit.removeSpan(span);
|
|
|
|
if (splitSpan(edit, start, end, s, e, f, true,
|
|
|
|
new StyleSpan(style), new StyleSpan(style)))
|
|
|
|
has = true;
|
|
|
|
}
|
2019-09-27 17:05:34 +00:00
|
|
|
|
2022-12-08 08:47:14 +00:00
|
|
|
if (!has)
|
|
|
|
edit.setSpan(new StyleSpan(style), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
2019-12-29 12:13:52 +00:00
|
|
|
|
2022-12-08 08:47:14 +00:00
|
|
|
etBody.setText(edit);
|
|
|
|
etBody.setSelection(select ? start : end, end);
|
2021-05-14 17:20:01 +00:00
|
|
|
|
2022-12-08 08:47:14 +00:00
|
|
|
return true;
|
|
|
|
}
|
2019-09-27 17:05:34 +00:00
|
|
|
|
2022-12-08 08:47:14 +00:00
|
|
|
static boolean setUnderline(EditText etBody, int start, int end, boolean select) {
|
|
|
|
Log.breadcrumb("style", "action", "underline");
|
2019-09-27 17:05:34 +00:00
|
|
|
|
2022-12-08 08:47:14 +00:00
|
|
|
boolean has = false;
|
|
|
|
Editable edit = etBody.getText();
|
|
|
|
UnderlineSpan[] spans = edit.getSpans(start, end, UnderlineSpan.class);
|
|
|
|
for (UnderlineSpan span : spans) {
|
|
|
|
int s = edit.getSpanStart(span);
|
|
|
|
int e = edit.getSpanEnd(span);
|
|
|
|
int f = edit.getSpanFlags(span);
|
2022-12-09 07:54:45 +00:00
|
|
|
if ((f & Spanned.SPAN_COMPOSING) != 0)
|
|
|
|
continue;
|
2022-12-08 08:47:14 +00:00
|
|
|
edit.removeSpan(span);
|
|
|
|
if (splitSpan(edit, start, end, s, e, f, true,
|
|
|
|
new UnderlineSpan(), new UnderlineSpan()))
|
|
|
|
has = true;
|
|
|
|
}
|
2022-06-01 05:47:08 +00:00
|
|
|
|
2022-12-08 08:47:14 +00:00
|
|
|
if (!has)
|
|
|
|
edit.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
2019-09-27 17:05:34 +00:00
|
|
|
|
2022-12-08 08:47:14 +00:00
|
|
|
etBody.setText(edit);
|
|
|
|
etBody.setSelection(select ? start : end, end);
|
2019-09-29 10:10:19 +00:00
|
|
|
|
2022-12-08 08:47:14 +00:00
|
|
|
return true;
|
|
|
|
}
|
2019-09-29 10:10:19 +00:00
|
|
|
|
2022-12-08 08:47:14 +00:00
|
|
|
static boolean selectSize(LifecycleOwner owner, View anchor, EditText etBody) {
|
|
|
|
Log.breadcrumb("style", "action", "selectSize");
|
|
|
|
|
|
|
|
Context context = anchor.getContext();
|
|
|
|
PopupMenuLifecycle popupMenu = new PopupMenuLifecycle(context, owner, anchor);
|
|
|
|
|
|
|
|
int[] titles = new int[]{
|
|
|
|
R.string.title_style_size_xsmall,
|
|
|
|
R.string.title_style_size_small,
|
|
|
|
R.string.title_style_size_medium,
|
|
|
|
R.string.title_style_size_large,
|
|
|
|
R.string.title_style_size_xlarge};
|
|
|
|
|
|
|
|
float[] sizes = new float[]{
|
|
|
|
HtmlHelper.FONT_XSMALL,
|
|
|
|
HtmlHelper.FONT_SMALL,
|
|
|
|
1.0f,
|
|
|
|
HtmlHelper.FONT_LARGE,
|
|
|
|
HtmlHelper.FONT_XLARGE};
|
|
|
|
|
|
|
|
for (int i = 0; i < titles.length; i++) {
|
|
|
|
SpannableStringBuilder ssb = new SpannableStringBuilderEx(context.getString(titles[i]));
|
|
|
|
ssb.setSpan(new RelativeSizeSpan(sizes[i]), 0, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
|
|
popupMenu.getMenu().add(group_style_size, i + 1, i, ssb);
|
|
|
|
}
|
2021-05-14 17:20:01 +00:00
|
|
|
|
2022-12-08 08:47:14 +00:00
|
|
|
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
|
|
|
|
@Override
|
|
|
|
public boolean onMenuItemClick(MenuItem item) {
|
|
|
|
return StyleHelper.apply(group_style_size, item.getItemId(), owner, anchor, etBody);
|
|
|
|
}
|
|
|
|
});
|
2021-02-05 10:15:02 +00:00
|
|
|
|
2022-12-08 08:47:14 +00:00
|
|
|
popupMenu.show();
|
2019-09-29 10:10:19 +00:00
|
|
|
|
2022-12-08 08:47:14 +00:00
|
|
|
return true;
|
2019-09-27 17:05:34 +00:00
|
|
|
}
|
2020-11-17 18:37:56 +00:00
|
|
|
|
2022-12-09 07:54:45 +00:00
|
|
|
static boolean setSize(EditText etBody, int start, int end, Float size, boolean select) {
|
2022-12-07 09:15:00 +00:00
|
|
|
Log.breadcrumb("style", "action", "size");
|
|
|
|
|
|
|
|
Editable edit = etBody.getText();
|
|
|
|
RelativeSizeSpan[] spans = edit.getSpans(start, end, RelativeSizeSpan.class);
|
|
|
|
for (RelativeSizeSpan span : spans) {
|
|
|
|
int s = edit.getSpanStart(span);
|
|
|
|
int e = edit.getSpanEnd(span);
|
|
|
|
int f = edit.getSpanFlags(span);
|
|
|
|
edit.removeSpan(span);
|
|
|
|
splitSpan(edit, start, end, s, e, f, false,
|
|
|
|
new RelativeSizeSpan(span.getSizeChange()),
|
|
|
|
new RelativeSizeSpan(span.getSizeChange()));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (size != null)
|
|
|
|
edit.setSpan(new RelativeSizeSpan(size), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
|
|
|
|
|
|
etBody.setText(edit);
|
2022-12-09 07:54:45 +00:00
|
|
|
etBody.setSelection(select ? start : end, end);
|
2022-12-07 09:15:00 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-12-08 08:47:14 +00:00
|
|
|
static boolean selectBackground(EditText etBody, int start, int end) {
|
|
|
|
Log.breadcrumb("style", "action", "selectBackground");
|
|
|
|
|
|
|
|
Helper.hideKeyboard(etBody);
|
|
|
|
|
|
|
|
Context context = etBody.getContext();
|
|
|
|
int editTextColor = Helper.resolveColor(context, android.R.attr.editTextColor);
|
|
|
|
|
|
|
|
ColorPickerDialogBuilder builder = ColorPickerDialogBuilder
|
|
|
|
.with(context)
|
|
|
|
.setTitle(R.string.title_background)
|
|
|
|
.showColorEdit(true)
|
|
|
|
.setColorEditTextColor(editTextColor)
|
|
|
|
.wheelType(ColorPickerView.WHEEL_TYPE.FLOWER)
|
|
|
|
.density(6)
|
|
|
|
//.lightnessSliderOnly()
|
|
|
|
.setPositiveButton(android.R.string.ok, new ColorPickerClickListener() {
|
|
|
|
@Override
|
|
|
|
public void onClick(DialogInterface dialog, int selectedColor, Integer[] allColors) {
|
2022-12-09 07:54:45 +00:00
|
|
|
setBackground(etBody, start, end, selectedColor, false);
|
2022-12-08 08:47:14 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
.setNegativeButton(R.string.title_reset, new DialogInterface.OnClickListener() {
|
|
|
|
@Override
|
|
|
|
public void onClick(DialogInterface dialog, int which) {
|
2022-12-09 07:54:45 +00:00
|
|
|
setBackground(etBody, start, end, null, false);
|
2022-12-08 08:47:14 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
BackgroundColorSpan[] spans = etBody.getText().getSpans(start, end, BackgroundColorSpan.class);
|
|
|
|
if (spans != null && spans.length == 1)
|
|
|
|
builder.initialColor(spans[0].getBackgroundColor());
|
|
|
|
|
|
|
|
builder.build().show();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-12-09 07:54:45 +00:00
|
|
|
static void setBackground(EditText etBody, int start, int end, Integer color, boolean select) {
|
2022-12-07 09:15:00 +00:00
|
|
|
Log.breadcrumb("style", "action", "background");
|
|
|
|
|
|
|
|
Editable edit = etBody.getText();
|
|
|
|
BackgroundColorSpan spans[] = edit.getSpans(start, end, BackgroundColorSpan.class);
|
|
|
|
for (BackgroundColorSpan span : spans) {
|
|
|
|
int s = edit.getSpanStart(span);
|
|
|
|
int e = edit.getSpanEnd(span);
|
|
|
|
int f = edit.getSpanFlags(span);
|
|
|
|
edit.removeSpan(span);
|
|
|
|
splitSpan(edit, start, end, s, e, f, false,
|
|
|
|
new BackgroundColorSpan(span.getBackgroundColor()),
|
|
|
|
new BackgroundColorSpan(span.getBackgroundColor()));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (color != null)
|
|
|
|
edit.setSpan(new BackgroundColorSpan(color), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
|
|
|
|
|
|
etBody.setText(edit);
|
2022-12-09 07:54:45 +00:00
|
|
|
etBody.setSelection(select ? start : end, end);
|
2022-12-07 09:15:00 +00:00
|
|
|
}
|
|
|
|
|
2022-12-08 08:47:14 +00:00
|
|
|
static boolean selectColor(EditText etBody, int start, int end) {
|
|
|
|
Log.breadcrumb("style", "action", "selectColor");
|
|
|
|
|
|
|
|
Helper.hideKeyboard(etBody);
|
|
|
|
|
|
|
|
Context context = etBody.getContext();
|
|
|
|
int editTextColor = Helper.resolveColor(context, android.R.attr.editTextColor);
|
|
|
|
|
|
|
|
ColorPickerDialogBuilder builder = ColorPickerDialogBuilder
|
|
|
|
.with(context)
|
|
|
|
.setTitle(R.string.title_color)
|
|
|
|
.showColorEdit(true)
|
|
|
|
.setColorEditTextColor(editTextColor)
|
|
|
|
.wheelType(ColorPickerView.WHEEL_TYPE.FLOWER)
|
|
|
|
.density(6)
|
|
|
|
//.lightnessSliderOnly()
|
|
|
|
.setPositiveButton(android.R.string.ok, new ColorPickerClickListener() {
|
|
|
|
@Override
|
|
|
|
public void onClick(DialogInterface dialog, int selectedColor, Integer[] allColors) {
|
2022-12-09 07:54:45 +00:00
|
|
|
setColor(etBody, start, end, selectedColor, false);
|
2022-12-08 08:47:14 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
.setNegativeButton(R.string.title_reset, new DialogInterface.OnClickListener() {
|
|
|
|
@Override
|
|
|
|
public void onClick(DialogInterface dialog, int which) {
|
2022-12-09 07:54:45 +00:00
|
|
|
setColor(etBody, start, end, null, false);
|
2022-12-08 08:47:14 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
ForegroundColorSpan[] spans = etBody.getText().getSpans(start, end, ForegroundColorSpan.class);
|
|
|
|
if (spans != null && spans.length == 1)
|
|
|
|
builder.initialColor(spans[0].getForegroundColor());
|
|
|
|
|
|
|
|
builder.build().show();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-12-09 07:54:45 +00:00
|
|
|
static void setColor(EditText etBody, int start, int end, Integer color, boolean select) {
|
2022-12-07 09:15:00 +00:00
|
|
|
Log.breadcrumb("style", "action", "color");
|
|
|
|
|
|
|
|
Editable edit = etBody.getText();
|
|
|
|
ForegroundColorSpan spans[] = edit.getSpans(start, end, ForegroundColorSpan.class);
|
|
|
|
for (ForegroundColorSpan span : spans) {
|
|
|
|
int s = edit.getSpanStart(span);
|
|
|
|
int e = edit.getSpanEnd(span);
|
|
|
|
int f = edit.getSpanFlags(span);
|
|
|
|
edit.removeSpan(span);
|
|
|
|
splitSpan(edit, start, end, s, e, f, false,
|
|
|
|
new ForegroundColorSpan(span.getForegroundColor()),
|
|
|
|
new ForegroundColorSpan(span.getForegroundColor()));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (color != null)
|
|
|
|
edit.setSpan(new ForegroundColorSpan(color), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
|
|
|
|
|
|
etBody.setText(edit);
|
2022-12-09 07:54:45 +00:00
|
|
|
etBody.setSelection(select ? start : end, end);
|
2022-12-07 09:15:00 +00:00
|
|
|
}
|
|
|
|
|
2022-12-08 08:47:14 +00:00
|
|
|
static boolean selectFont(LifecycleOwner owner, View anchor, EditText etBody, int start, int end) {
|
|
|
|
Log.breadcrumb("style", "action", "selectFont");
|
|
|
|
|
|
|
|
Context context = anchor.getContext();
|
|
|
|
PopupMenuLifecycle popupMenu = new PopupMenuLifecycle(context, owner, anchor);
|
|
|
|
|
|
|
|
List<FontDescriptor> fonts = getFonts(context, false);
|
|
|
|
for (int i = 0; i < fonts.size(); i++) {
|
|
|
|
FontDescriptor font = fonts.get(i);
|
|
|
|
SpannableStringBuilder ssb = new SpannableStringBuilderEx(font.toString());
|
|
|
|
ssb.setSpan(getTypefaceSpan(font.type, context), 0, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
|
|
popupMenu.getMenu().add(font.custom ? group_style_font_custom : group_style_font_standard, i, 0, ssb)
|
|
|
|
.setIntent(new Intent().putExtra("face", font.type));
|
|
|
|
}
|
|
|
|
popupMenu.getMenu().add(group_style_font_standard, fonts.size(), 0, R.string.title_style_font_default)
|
|
|
|
.setIntent(new Intent());
|
|
|
|
|
|
|
|
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
|
|
|
|
@Override
|
|
|
|
public boolean onMenuItemClick(MenuItem item) {
|
2022-12-09 07:54:45 +00:00
|
|
|
return setFont(etBody, start, end, item.getIntent().getStringExtra("face"), false);
|
2022-12-08 08:47:14 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
popupMenu.show();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-12-09 07:54:45 +00:00
|
|
|
static boolean setFont(EditText etBody, int start, int end, String face, boolean select) {
|
2022-12-07 09:15:00 +00:00
|
|
|
Log.breadcrumb("style", "action", "font");
|
|
|
|
|
|
|
|
Context context = etBody.getContext();
|
|
|
|
|
|
|
|
Editable edit = etBody.getText();
|
|
|
|
TypefaceSpan[] spans = edit.getSpans(start, end, TypefaceSpan.class);
|
|
|
|
for (TypefaceSpan span : spans) {
|
|
|
|
int s = edit.getSpanStart(span);
|
|
|
|
int e = edit.getSpanEnd(span);
|
|
|
|
int f = edit.getSpanFlags(span);
|
|
|
|
edit.removeSpan(span);
|
|
|
|
splitSpan(edit, start, end, s, e, f, false,
|
|
|
|
getTypefaceSpan(span.getFamily(), context),
|
|
|
|
getTypefaceSpan(span.getFamily(), context));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (face != null)
|
|
|
|
edit.setSpan(getTypefaceSpan(face, context), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
|
|
|
|
|
|
etBody.setText(edit);
|
2022-12-09 07:54:45 +00:00
|
|
|
etBody.setSelection(select ? start : end, end);
|
2022-12-07 09:15:00 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-12-08 08:47:14 +00:00
|
|
|
static boolean selectAlignment(LifecycleOwner owner, View anchor, EditText etBody, int start, int end) {
|
|
|
|
Log.breadcrumb("style", "action", "selectAlignment");
|
|
|
|
|
|
|
|
Context context = anchor.getContext();
|
|
|
|
PopupMenuLifecycle popupMenu = new PopupMenuLifecycle(context, owner, anchor);
|
|
|
|
popupMenu.inflate(R.menu.popup_style_alignment);
|
|
|
|
|
|
|
|
if (start == end) {
|
|
|
|
Pair<Integer, Integer> block = StyleHelper.getParagraph(etBody, true);
|
|
|
|
if (block == null)
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
|
|
|
|
@Override
|
|
|
|
public boolean onMenuItemClick(MenuItem item) {
|
|
|
|
return StyleHelper.apply(group_style_align, item.getItemId(), owner, anchor, etBody);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
popupMenu.insertIcons(context);
|
|
|
|
|
|
|
|
popupMenu.show();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-09-18 12:05:19 +00:00
|
|
|
static boolean setAlignment(int itemId, EditText etBody, int start, int end, boolean select) {
|
2022-09-18 09:33:32 +00:00
|
|
|
Log.breadcrumb("style", "action", "alignment");
|
|
|
|
|
|
|
|
Editable edit = etBody.getText();
|
|
|
|
Pair<Integer, Integer> paragraph = ensureParagraph(edit, start, end);
|
|
|
|
if (paragraph == null)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
int s = paragraph.first;
|
|
|
|
int e = paragraph.second;
|
|
|
|
|
|
|
|
AlignmentSpan[] spans = edit.getSpans(s, e, AlignmentSpan.class);
|
|
|
|
for (AlignmentSpan span : spans)
|
|
|
|
edit.removeSpan(span);
|
|
|
|
|
|
|
|
Layout.Alignment alignment = null;
|
|
|
|
boolean ltr = (TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_LTR);
|
|
|
|
if (itemId == R.id.menu_style_align_start) {
|
|
|
|
alignment = (ltr ? Layout.Alignment.ALIGN_NORMAL : Layout.Alignment.ALIGN_OPPOSITE);
|
|
|
|
} else if (itemId == R.id.menu_style_align_center) {
|
|
|
|
alignment = Layout.Alignment.ALIGN_CENTER;
|
|
|
|
} else if (itemId == R.id.menu_style_align_end) {
|
|
|
|
alignment = (ltr ? Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_NORMAL);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (alignment != null)
|
|
|
|
edit.setSpan(new AlignmentSpan.Standard(alignment),
|
|
|
|
s, e, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_PARAGRAPH);
|
|
|
|
|
|
|
|
etBody.setText(edit);
|
2022-09-18 12:05:19 +00:00
|
|
|
etBody.setSelection(select ? s : e, e);
|
2022-09-18 09:33:32 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-12-08 08:47:14 +00:00
|
|
|
static boolean selectList(LifecycleOwner owner, View anchor, EditText etBody, int start, int end) {
|
|
|
|
Log.breadcrumb("style", "action", "selectList");
|
|
|
|
|
|
|
|
Context context = anchor.getContext();
|
|
|
|
PopupMenuLifecycle popupMenu = new PopupMenuLifecycle(context, owner, anchor);
|
|
|
|
popupMenu.inflate(R.menu.popup_style_list);
|
|
|
|
|
|
|
|
int s = start;
|
|
|
|
int e = end;
|
|
|
|
if (s == e) {
|
|
|
|
Pair<Integer, Integer> p = StyleHelper.getParagraph(etBody, false);
|
|
|
|
if (p == null)
|
|
|
|
return false;
|
|
|
|
s = p.first;
|
|
|
|
e = p.second;
|
|
|
|
}
|
|
|
|
|
|
|
|
Editable edit = etBody.getText();
|
|
|
|
Integer maxLevel = getMaxListLevel(edit, s, e);
|
|
|
|
IndentSpan[] indents = edit.getSpans(s, e, IndentSpan.class);
|
|
|
|
|
|
|
|
popupMenu.getMenu().findItem(R.id.menu_style_list_bullets).setEnabled(indents.length == 0);
|
|
|
|
popupMenu.getMenu().findItem(R.id.menu_style_list_numbered).setEnabled(indents.length == 0);
|
|
|
|
popupMenu.getMenu().findItem(R.id.menu_style_list_increase).setEnabled(indents.length == 0 && maxLevel != null);
|
|
|
|
popupMenu.getMenu().findItem(R.id.menu_style_list_decrease).setEnabled(indents.length == 0 && maxLevel != null && maxLevel > 0);
|
|
|
|
|
|
|
|
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
|
|
|
|
@Override
|
|
|
|
public boolean onMenuItemClick(MenuItem item) {
|
|
|
|
return StyleHelper.apply(group_style_list, item.getItemId(), owner, anchor, etBody);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
popupMenu.insertIcons(context);
|
|
|
|
|
|
|
|
popupMenu.show();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-09-18 12:05:19 +00:00
|
|
|
static boolean setListLevel(int itemId, EditText etBody, int start, int end, boolean select) {
|
2022-09-18 09:33:32 +00:00
|
|
|
Log.breadcrumb("style", "action", "level");
|
|
|
|
|
|
|
|
Context context = etBody.getContext();
|
|
|
|
int add = (itemId == R.id.menu_style_list_increase ? 1 : -1);
|
|
|
|
|
|
|
|
boolean renum = false;
|
2022-12-08 08:47:14 +00:00
|
|
|
Editable edit = etBody.getText();
|
2022-09-18 09:33:32 +00:00
|
|
|
BulletSpan[] spans = edit.getSpans(start, end, BulletSpan.class);
|
|
|
|
for (BulletSpan span : spans)
|
|
|
|
if (span instanceof BulletSpanEx) {
|
|
|
|
BulletSpanEx bs = (BulletSpanEx) span;
|
2022-09-18 11:30:11 +00:00
|
|
|
bs.setLevel(Math.max(0, bs.getLevel() + add));
|
2022-09-18 09:33:32 +00:00
|
|
|
} else if (span instanceof NumberSpan) {
|
|
|
|
renum = true;
|
|
|
|
NumberSpan ns = (NumberSpan) span;
|
2022-09-18 11:30:11 +00:00
|
|
|
ns.setLevel(Math.max(0, ns.getLevel() + add));
|
2022-09-18 09:33:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (renum)
|
|
|
|
renumber(edit, false, context);
|
|
|
|
|
|
|
|
etBody.setText(edit);
|
2022-09-18 12:05:19 +00:00
|
|
|
etBody.setSelection(select ? start : end, end);
|
2022-09-18 09:33:32 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-09-18 12:05:19 +00:00
|
|
|
static boolean setList(int itemId, EditText etBody, int start, int end, boolean select) {
|
2022-09-18 09:33:32 +00:00
|
|
|
Log.breadcrumb("style", "action", "list");
|
|
|
|
|
|
|
|
Context context = etBody.getContext();
|
|
|
|
Editable edit = etBody.getText();
|
|
|
|
|
|
|
|
int colorAccent = Helper.resolveColor(context, R.attr.colorAccent);
|
|
|
|
int bulletGap = context.getResources().getDimensionPixelSize(R.dimen.bullet_gap_size);
|
|
|
|
int bulletRadius = context.getResources().getDimensionPixelSize(R.dimen.bullet_radius_size);
|
|
|
|
int bulletIndent = context.getResources().getDimensionPixelSize(R.dimen.bullet_indent_size);
|
|
|
|
|
|
|
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
|
|
|
int message_zoom = prefs.getInt("message_zoom", 100);
|
|
|
|
float textSize = Helper.getTextSize(context, 0) * message_zoom / 100f;
|
|
|
|
|
|
|
|
Pair<Integer, Integer> paragraph = ensureParagraph(edit, start, end);
|
|
|
|
if (paragraph == null)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
int s = paragraph.first;
|
|
|
|
int e = paragraph.second;
|
|
|
|
|
|
|
|
// Remove existing bullets
|
|
|
|
BulletSpan[] spans = edit.getSpans(s, e, BulletSpan.class);
|
|
|
|
for (BulletSpan span : spans)
|
|
|
|
edit.removeSpan(span);
|
|
|
|
|
|
|
|
int i = s;
|
|
|
|
int j = s + 1;
|
|
|
|
int index = 1;
|
|
|
|
while (j < e) {
|
|
|
|
if (i > 0 && edit.charAt(i - 1) == '\n' && edit.charAt(j) == '\n') {
|
|
|
|
Log.i("Insert " + i + "..." + (j + 1) + " size=" + e);
|
|
|
|
if (itemId == R.id.menu_style_list_bullets)
|
|
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P)
|
|
|
|
edit.setSpan(new BulletSpanEx(bulletIndent, bulletGap, colorAccent, 0), i, j + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_PARAGRAPH);
|
|
|
|
else
|
|
|
|
edit.setSpan(new BulletSpanEx(bulletIndent, bulletGap, colorAccent, bulletRadius, 0), i, j + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_PARAGRAPH);
|
|
|
|
else
|
|
|
|
edit.setSpan(new NumberSpan(bulletIndent, bulletGap, colorAccent, textSize, 0, index++), i, j + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_PARAGRAPH);
|
|
|
|
|
|
|
|
i = j + 1;
|
|
|
|
}
|
|
|
|
j++;
|
|
|
|
}
|
|
|
|
|
|
|
|
renumber(edit, false, context);
|
|
|
|
|
|
|
|
etBody.setText(edit);
|
2022-12-08 07:01:45 +00:00
|
|
|
etBody.setSelection(select ? s : e - 1, e - 1);
|
2022-09-18 09:33:32 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-12-08 08:47:14 +00:00
|
|
|
static boolean selectIndentation(LifecycleOwner owner, View anchor, EditText etBody, int start, int end) {
|
|
|
|
Log.breadcrumb("style", "action", "selectIndentation");
|
|
|
|
|
|
|
|
Context context = anchor.getContext();
|
|
|
|
PopupMenuLifecycle popupMenu = new PopupMenuLifecycle(context, owner, anchor);
|
|
|
|
popupMenu.inflate(R.menu.popup_style_indentation);
|
|
|
|
|
|
|
|
int s = start;
|
|
|
|
int e = end;
|
|
|
|
if (s == e) {
|
|
|
|
Pair<Integer, Integer> block = StyleHelper.getParagraph(etBody, true);
|
|
|
|
if (block == null)
|
|
|
|
return false;
|
|
|
|
s = block.first;
|
|
|
|
e = block.second;
|
|
|
|
}
|
|
|
|
|
|
|
|
Editable edit = etBody.getText();
|
|
|
|
Integer maxLevel = getMaxListLevel(edit, s, e);
|
|
|
|
IndentSpan[] indents = edit.getSpans(s, e, IndentSpan.class);
|
|
|
|
|
|
|
|
popupMenu.getMenu().findItem(R.id.menu_style_indentation_increase).setEnabled(maxLevel == null);
|
|
|
|
popupMenu.getMenu().findItem(R.id.menu_style_indentation_decrease).setEnabled(indents.length > 0);
|
|
|
|
|
|
|
|
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
|
|
|
|
@Override
|
|
|
|
public boolean onMenuItemClick(MenuItem item) {
|
|
|
|
return StyleHelper.apply(group_style_indentation, item.getItemId(), owner, anchor, etBody);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
popupMenu.insertIcons(context);
|
|
|
|
|
|
|
|
popupMenu.show();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-09-18 12:05:19 +00:00
|
|
|
static boolean setIndentation(int itemId, EditText etBody, int start, int end, boolean select) {
|
2022-09-18 09:33:32 +00:00
|
|
|
Log.breadcrumb("style", "action", "indent");
|
|
|
|
|
|
|
|
Editable edit = etBody.getText();
|
|
|
|
Pair<Integer, Integer> paragraph = ensureParagraph(edit, start, end);
|
|
|
|
if (paragraph == null)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
Context context = etBody.getContext();
|
|
|
|
int intentSize = context.getResources().getDimensionPixelSize(R.dimen.indent_size);
|
|
|
|
|
|
|
|
QuoteSpan[] quotes = edit.getSpans(start, end, QuoteSpan.class);
|
|
|
|
for (QuoteSpan quote : quotes)
|
|
|
|
edit.removeSpan(quote);
|
|
|
|
|
|
|
|
int prev = paragraph.first;
|
|
|
|
int next = paragraph.first;
|
|
|
|
while (next < paragraph.second) {
|
|
|
|
while (next < paragraph.second && edit.charAt(next) != '\n')
|
|
|
|
next++;
|
|
|
|
|
|
|
|
if (itemId == R.id.menu_style_indentation_decrease) {
|
|
|
|
IndentSpan[] indents = edit.getSpans(prev, prev, IndentSpan.class);
|
|
|
|
if (indents.length > 0)
|
|
|
|
edit.removeSpan(indents[0]);
|
|
|
|
} else {
|
|
|
|
IndentSpan is = new IndentSpan(intentSize);
|
|
|
|
edit.setSpan(is, prev, next + 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
|
|
|
|
}
|
|
|
|
|
|
|
|
next++;
|
|
|
|
prev = next;
|
|
|
|
}
|
|
|
|
|
|
|
|
etBody.setText(edit);
|
2022-09-18 12:05:19 +00:00
|
|
|
etBody.setSelection(select ? paragraph.first : paragraph.second, paragraph.second);
|
2022-09-18 09:33:32 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-09-18 12:05:19 +00:00
|
|
|
static boolean setBlockQuote(EditText etBody, int start, int end, boolean select) {
|
2022-09-18 09:33:32 +00:00
|
|
|
Log.breadcrumb("style", "action", "quote");
|
|
|
|
|
|
|
|
Context context = etBody.getContext();
|
|
|
|
Editable edit = etBody.getText();
|
|
|
|
|
|
|
|
int colorPrimary = Helper.resolveColor(context, R.attr.colorPrimary);
|
|
|
|
final int colorBlockquote = Helper.resolveColor(context, R.attr.colorBlockquote, colorPrimary);
|
|
|
|
int quoteGap = context.getResources().getDimensionPixelSize(R.dimen.quote_gap_size);
|
|
|
|
int quoteStripe = context.getResources().getDimensionPixelSize(R.dimen.quote_stripe_width);
|
|
|
|
|
|
|
|
Pair<Integer, Integer> paragraph = ensureParagraph(edit, start, end);
|
|
|
|
if (paragraph == null)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
QuoteSpan[] quotes = edit.getSpans(paragraph.first, paragraph.second, QuoteSpan.class);
|
|
|
|
for (QuoteSpan quote : quotes)
|
|
|
|
edit.removeSpan(quote);
|
|
|
|
|
2022-12-09 07:54:45 +00:00
|
|
|
if (quotes.length == 0) {
|
|
|
|
IndentSpan[] indents = edit.getSpans(start, end, IndentSpan.class);
|
|
|
|
for (IndentSpan indent : indents)
|
|
|
|
edit.removeSpan(indent);
|
|
|
|
|
|
|
|
QuoteSpan q;
|
|
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P)
|
|
|
|
q = new QuoteSpan(colorBlockquote);
|
|
|
|
else
|
|
|
|
q = new QuoteSpan(colorBlockquote, quoteStripe, quoteGap);
|
|
|
|
edit.setSpan(q, paragraph.first, paragraph.second, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
|
|
|
|
}
|
2022-09-18 09:33:32 +00:00
|
|
|
|
|
|
|
etBody.setText(edit);
|
2022-09-18 12:05:19 +00:00
|
|
|
etBody.setSelection(select ? paragraph.first : paragraph.second, paragraph.second);
|
2022-12-07 09:15:00 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-12-07 11:32:16 +00:00
|
|
|
static boolean setMark(EditText etBody, int start, int end, boolean select) {
|
2022-12-07 09:15:00 +00:00
|
|
|
Log.breadcrumb("style", "action", "mark");
|
|
|
|
|
|
|
|
boolean has = false;
|
|
|
|
Editable edit = etBody.getText();
|
|
|
|
MarkSpan[] spans = edit.getSpans(start, end, MarkSpan.class);
|
|
|
|
for (MarkSpan span : spans) {
|
|
|
|
int s = edit.getSpanStart(span);
|
|
|
|
int e = edit.getSpanEnd(span);
|
|
|
|
int f = edit.getSpanFlags(span);
|
|
|
|
edit.removeSpan(span);
|
|
|
|
if (splitSpan(edit, start, end, s, e, f, true,
|
|
|
|
new MarkSpan(), new MarkSpan()))
|
|
|
|
has = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!has)
|
|
|
|
edit.setSpan(new MarkSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
|
|
|
|
|
|
etBody.setText(edit);
|
2022-12-07 11:32:16 +00:00
|
|
|
etBody.setSelection(select ? start : end, end);
|
2022-12-07 09:15:00 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-12-07 11:32:16 +00:00
|
|
|
static boolean setSubscript(EditText etBody, int start, int end, boolean select) {
|
2022-12-07 09:15:00 +00:00
|
|
|
Log.breadcrumb("style", "action", "subscript");
|
|
|
|
|
|
|
|
boolean has = false;
|
|
|
|
Editable edit = etBody.getText();
|
|
|
|
SubscriptSpanEx[] spans = edit.getSpans(start, end, SubscriptSpanEx.class);
|
|
|
|
for (SubscriptSpanEx span : spans) {
|
|
|
|
int s = edit.getSpanStart(span);
|
|
|
|
int e = edit.getSpanEnd(span);
|
|
|
|
int f = edit.getSpanFlags(span);
|
|
|
|
edit.removeSpan(span);
|
|
|
|
if (splitSpan(edit, start, end, s, e, f, true,
|
|
|
|
new SubscriptSpanEx(), new SubscriptSpanEx()))
|
|
|
|
has = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!has)
|
|
|
|
edit.setSpan(new SubscriptSpanEx(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
|
|
|
|
|
|
etBody.setText(edit);
|
2022-12-07 11:32:16 +00:00
|
|
|
etBody.setSelection(select ? start : end, end);
|
2022-12-07 09:15:00 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-12-07 11:32:16 +00:00
|
|
|
static boolean setSuperscript(EditText etBody, int start, int end, boolean select) {
|
2022-12-07 09:15:00 +00:00
|
|
|
Log.breadcrumb("style", "action", "superscript");
|
|
|
|
|
|
|
|
boolean has = false;
|
|
|
|
Editable edit = etBody.getText();
|
|
|
|
SuperscriptSpanEx[] spans = edit.getSpans(start, end, SuperscriptSpanEx.class);
|
|
|
|
for (SuperscriptSpanEx span : spans) {
|
|
|
|
int s = edit.getSpanStart(span);
|
|
|
|
int e = edit.getSpanEnd(span);
|
|
|
|
int f = edit.getSpanFlags(span);
|
|
|
|
edit.removeSpan(span);
|
|
|
|
if (splitSpan(edit, start, end, s, e, f, true,
|
|
|
|
new SuperscriptSpanEx(), new SuperscriptSpanEx()))
|
|
|
|
has = true;
|
|
|
|
}
|
|
|
|
|
2022-12-07 11:32:16 +00:00
|
|
|
if (!has)
|
2022-12-07 09:15:00 +00:00
|
|
|
edit.setSpan(new SuperscriptSpanEx(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
|
|
|
|
|
|
etBody.setText(edit);
|
2022-12-07 11:32:16 +00:00
|
|
|
etBody.setSelection(select ? start : end, end);
|
2022-12-07 09:15:00 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-12-07 11:32:16 +00:00
|
|
|
static boolean setStrikeThrough(EditText etBody, int start, int end, boolean select) {
|
2022-12-07 09:15:00 +00:00
|
|
|
Log.breadcrumb("style", "action", "strike");
|
|
|
|
|
|
|
|
boolean has = false;
|
|
|
|
Editable edit = etBody.getText();
|
|
|
|
StrikethroughSpan[] spans = edit.getSpans(start, end, StrikethroughSpan.class);
|
|
|
|
for (StrikethroughSpan span : spans) {
|
|
|
|
int s = edit.getSpanStart(span);
|
|
|
|
int e = edit.getSpanEnd(span);
|
|
|
|
int f = edit.getSpanFlags(span);
|
|
|
|
edit.removeSpan(span);
|
|
|
|
if (splitSpan(edit, start, end, s, e, f, true,
|
|
|
|
new StrikethroughSpan(), new StrikethroughSpan()))
|
|
|
|
has = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!has)
|
|
|
|
edit.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
|
|
|
|
|
|
etBody.setText(edit);
|
2022-12-07 11:32:16 +00:00
|
|
|
etBody.setSelection(select ? start : end, end);
|
2022-12-07 09:15:00 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-12-27 08:31:13 +00:00
|
|
|
static boolean setLine(EditText etBody, int end) {
|
|
|
|
Log.breadcrumb("style", "action", "line");
|
|
|
|
|
|
|
|
Context context = etBody.getContext();
|
|
|
|
Editable edit = etBody.getText();
|
|
|
|
|
|
|
|
if (end == 0 || edit.charAt(end - 1) != '\n')
|
|
|
|
edit.insert(end++, "\n");
|
|
|
|
if (end == edit.length() || edit.charAt(end) != '\n')
|
|
|
|
edit.insert(end, "\n");
|
|
|
|
|
|
|
|
edit.insert(end, "\uFFFC"); // Object replacement character
|
|
|
|
|
|
|
|
int colorSeparator = Helper.resolveColor(context, R.attr.colorSeparator);
|
|
|
|
float stroke = context.getResources().getDisplayMetrics().density;
|
|
|
|
edit.setSpan(
|
|
|
|
new LineSpan(colorSeparator, stroke, 0f),
|
|
|
|
end, end + 1,
|
|
|
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
|
|
|
|
|
|
etBody.setSelection(end + 2);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-01-01 16:41:07 +00:00
|
|
|
static boolean spellCheck(LifecycleOwner owner, EditText etBody, int start, int end) {
|
|
|
|
Log.breadcrumb("style", "action", "spell");
|
|
|
|
|
|
|
|
etBody.setSelection(end);
|
|
|
|
|
|
|
|
final Context context = etBody.getContext();
|
|
|
|
|
|
|
|
Bundle args = new Bundle();
|
|
|
|
args.putCharSequence("text", etBody.getText().subSequence(start, end));
|
|
|
|
|
|
|
|
new SimpleTask<List<LanguageTool.Suggestion>>() {
|
|
|
|
private BackgroundColorSpan highlightSpan = null;
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected void onPreExecute(Bundle args) {
|
|
|
|
int textColorHighlight = Helper.resolveColor(context, android.R.attr.textColorHighlight);
|
|
|
|
highlightSpan = new BackgroundColorSpan(textColorHighlight);
|
|
|
|
etBody.getText().setSpan(highlightSpan, start, end,
|
|
|
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected void onPostExecute(Bundle args) {
|
|
|
|
if (highlightSpan != null)
|
|
|
|
etBody.getText().removeSpan(highlightSpan);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected List<LanguageTool.Suggestion> onExecute(Context context, Bundle args) throws Throwable {
|
|
|
|
CharSequence text = args.getCharSequence("text");
|
|
|
|
return LanguageTool.getSuggestions(context, text);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected void onExecuted(Bundle args, List<LanguageTool.Suggestion> suggestions) {
|
|
|
|
LanguageTool.applySuggestions(etBody, start, end, suggestions);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected void onException(Bundle args, Throwable ex) {
|
|
|
|
ToastEx.makeText(context, Log.formatThrowable(ex), Toast.LENGTH_LONG).show();
|
|
|
|
}
|
|
|
|
}.execute(context, owner, args, "spell");
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-12-07 09:15:00 +00:00
|
|
|
static boolean setPassword(LifecycleOwner owner, EditText etBody, int start, int end) {
|
|
|
|
Log.breadcrumb("style", "action", "password");
|
|
|
|
|
|
|
|
Context context = etBody.getContext();
|
|
|
|
if (!ActivityBilling.isPro(context)) {
|
|
|
|
context.startActivity(new Intent(context, ActivityBilling.class));
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
boolean toolong = false;
|
|
|
|
if (end - start > ProtectedContent.MAX_PROTECTED_TEXT) {
|
|
|
|
toolong = true;
|
|
|
|
} else {
|
|
|
|
Editable edit = etBody.getText();
|
|
|
|
Spanned text = (Spanned) edit.subSequence(start, end);
|
|
|
|
String html = ProtectedContent.getContent(context, text);
|
|
|
|
if (html.length() > ProtectedContent.MAX_PROTECTED_TEXT)
|
|
|
|
toolong = true;
|
|
|
|
}
|
|
|
|
if (toolong) {
|
|
|
|
ToastEx.makeText(context, R.string.title_style_protect_size, Toast.LENGTH_LONG).show();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
ProtectedContent.showDialogEncrypt(context, owner, etBody);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-12-08 08:47:14 +00:00
|
|
|
static boolean setLink(EditText etBody, int start, int end, Object... args) {
|
|
|
|
Log.breadcrumb("style", "action", "link");
|
|
|
|
|
|
|
|
String url = (String) args[0];
|
|
|
|
String title = (String) args[1];
|
|
|
|
|
|
|
|
Editable edit = etBody.getText();
|
|
|
|
URLSpan[] spans = edit.getSpans(start, end, URLSpan.class);
|
|
|
|
for (URLSpan span : spans)
|
|
|
|
edit.removeSpan(span);
|
|
|
|
|
|
|
|
if (!TextUtils.isEmpty(url)) {
|
|
|
|
if (TextUtils.isEmpty(title))
|
|
|
|
title = url;
|
|
|
|
|
|
|
|
if (start == end)
|
|
|
|
edit.insert(start, title);
|
|
|
|
else if (!title.equals(edit.subSequence(start, end).toString()))
|
|
|
|
edit.replace(start, end, title);
|
|
|
|
|
|
|
|
edit.setSpan(new URLSpan(url), start, start + title.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
|
|
}
|
|
|
|
|
|
|
|
etBody.setText(edit);
|
|
|
|
etBody.setSelection(start + title.length());
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2022-12-08 10:08:48 +00:00
|
|
|
static boolean clear(EditText etBody, int start, int end, boolean select) {
|
2022-12-07 09:15:00 +00:00
|
|
|
Log.breadcrumb("style", "action", "clear");
|
|
|
|
|
|
|
|
int e = end;
|
|
|
|
|
|
|
|
// Expand to paragraph (block quotes)
|
|
|
|
Editable edit = etBody.getText();
|
|
|
|
if (e + 1 < edit.length() && edit.charAt(e) == '\n')
|
|
|
|
e++;
|
|
|
|
|
|
|
|
for (Object span : edit.getSpans(start, e, Object.class)) {
|
|
|
|
boolean has = false;
|
2022-12-08 18:59:32 +00:00
|
|
|
for (Class<?> cls : CLEAR_STYLES)
|
2022-12-07 09:15:00 +00:00
|
|
|
if (cls.isAssignableFrom(span.getClass())) {
|
|
|
|
has = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (!has)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
int sstart = edit.getSpanStart(span);
|
|
|
|
int send = edit.getSpanEnd(span);
|
|
|
|
int flags = edit.getSpanFlags(span);
|
|
|
|
|
|
|
|
if (sstart < start && send > start)
|
|
|
|
setSpan(edit, span, sstart, start, flags, etBody.getContext());
|
|
|
|
if (sstart < end && send > end)
|
|
|
|
setSpan(edit, span, e, send, flags, etBody.getContext());
|
|
|
|
|
|
|
|
edit.removeSpan(span);
|
|
|
|
}
|
|
|
|
|
|
|
|
etBody.setText(edit);
|
2022-12-08 18:59:32 +00:00
|
|
|
etBody.setSelection(select ? start : end, end);
|
2022-09-18 09:33:32 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-12-08 10:08:48 +00:00
|
|
|
static boolean clearAll(EditText etBody, int start, int end, boolean select) {
|
2022-12-08 08:47:14 +00:00
|
|
|
Log.breadcrumb("style", "action", "clear/all");
|
|
|
|
|
|
|
|
Editable edit = etBody.getText();
|
|
|
|
for (Object span : edit.getSpans(0, etBody.length(), Object.class)) {
|
|
|
|
if (!CLEAR_STYLES.contains(span.getClass()))
|
|
|
|
continue;
|
|
|
|
edit.removeSpan(span);
|
|
|
|
}
|
|
|
|
|
|
|
|
etBody.setText(edit);
|
2022-12-08 10:08:48 +00:00
|
|
|
etBody.setSelection(select ? start : end, end);
|
2022-12-08 08:47:14 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-06-12 18:19:24 +00:00
|
|
|
static boolean splitSpan(Editable edit, int start, int end, int s, int e, int f, boolean extend, Object span1, Object span2) {
|
2021-05-14 17:20:01 +00:00
|
|
|
if (start < 0 || end < 0) {
|
|
|
|
Log.e(span1 + " invalid selection=" + start + "..." + end);
|
2021-06-12 18:19:24 +00:00
|
|
|
return false;
|
2021-05-14 17:20:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (s < 0 || e < 0) {
|
|
|
|
Log.e(span1 + " not attached=" + s + "..." + e);
|
2021-06-12 18:19:24 +00:00
|
|
|
return false;
|
2021-05-14 17:20:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (s > e) {
|
|
|
|
int tmp = s;
|
|
|
|
s = e;
|
|
|
|
e = tmp;
|
|
|
|
}
|
|
|
|
|
2021-05-14 06:49:26 +00:00
|
|
|
if (start < s && end > s && end < e) {
|
|
|
|
// overlap before
|
|
|
|
if (extend)
|
|
|
|
edit.setSpan(span1, start, e, f);
|
|
|
|
else
|
|
|
|
edit.setSpan(span1, end, e, f);
|
2021-06-12 18:19:24 +00:00
|
|
|
return true;
|
2021-05-14 06:49:26 +00:00
|
|
|
} else if (start < e && end > e && start > s) {
|
|
|
|
// overlap after
|
|
|
|
if (extend)
|
|
|
|
edit.setSpan(span1, s, end, f);
|
|
|
|
else
|
|
|
|
edit.setSpan(span1, s, start, f);
|
2021-06-12 18:19:24 +00:00
|
|
|
return true;
|
2021-05-14 06:49:26 +00:00
|
|
|
} else if (start < s && end > e) {
|
|
|
|
// overlap all
|
2021-06-12 18:19:24 +00:00
|
|
|
if (extend) {
|
2021-05-14 06:49:26 +00:00
|
|
|
edit.setSpan(span1, start, end, f);
|
2021-06-12 18:19:24 +00:00
|
|
|
return true;
|
|
|
|
}
|
2021-06-01 11:52:33 +00:00
|
|
|
} else if (start >= s && end <= e) {
|
2021-06-12 18:19:24 +00:00
|
|
|
if (start == s && end == e)
|
|
|
|
return true;
|
|
|
|
|
2021-05-14 06:49:26 +00:00
|
|
|
// overlap inner
|
2021-06-01 11:52:33 +00:00
|
|
|
if (s < start)
|
|
|
|
edit.setSpan(span1, s, start, f);
|
|
|
|
if (end < e)
|
|
|
|
edit.setSpan(span2, end, e, f);
|
2021-06-12 18:19:24 +00:00
|
|
|
if (s < start || end < e)
|
|
|
|
return true;
|
2021-05-14 06:49:26 +00:00
|
|
|
}
|
2021-06-12 18:19:24 +00:00
|
|
|
|
|
|
|
return false;
|
2021-05-14 06:49:26 +00:00
|
|
|
}
|
|
|
|
|
2021-05-06 05:58:04 +00:00
|
|
|
static void setSpan(Editable edit, Object span, int start, int end, int flags, Context context) {
|
2020-11-17 19:22:34 +00:00
|
|
|
if (span instanceof CharacterStyle)
|
2021-05-06 05:58:04 +00:00
|
|
|
edit.setSpan(CharacterStyle.wrap((CharacterStyle) span), start, end, flags);
|
2020-11-17 19:22:34 +00:00
|
|
|
else if (span instanceof QuoteSpan) {
|
2020-11-17 20:22:02 +00:00
|
|
|
ParagraphStyle ps = (ParagraphStyle) span;
|
2021-05-06 05:58:04 +00:00
|
|
|
Pair<Integer, Integer> p = ensureParagraph(edit, start, end);
|
2021-07-06 17:25:00 +00:00
|
|
|
if (p == null)
|
|
|
|
return;
|
2021-05-06 05:58:04 +00:00
|
|
|
edit.setSpan(clone(span, ps.getClass(), context), p.first, p.second, flags);
|
2020-11-17 20:22:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-06 05:58:04 +00:00
|
|
|
static private Pair<Integer, Integer> ensureParagraph(Editable edit, int s, int e) {
|
2020-11-17 20:22:02 +00:00
|
|
|
int start = s;
|
|
|
|
int end = e;
|
|
|
|
|
|
|
|
// Expand selection at start
|
2021-05-06 05:58:04 +00:00
|
|
|
while (start > 0 && edit.charAt(start - 1) != '\n')
|
2020-11-17 20:22:02 +00:00
|
|
|
start--;
|
|
|
|
|
|
|
|
// Expand selection at end
|
2021-05-06 05:58:04 +00:00
|
|
|
while (end > 0 && end < edit.length() && edit.charAt(end - 1) != '\n')
|
2020-11-17 20:22:02 +00:00
|
|
|
end++;
|
|
|
|
|
|
|
|
// Nothing to do
|
|
|
|
if (start == end)
|
|
|
|
return null;
|
|
|
|
|
|
|
|
// Create paragraph at start
|
2021-05-06 05:58:04 +00:00
|
|
|
if (start == 0 && edit.charAt(start) != '\n') {
|
|
|
|
edit.insert(0, "\n");
|
2020-11-17 20:22:02 +00:00
|
|
|
start++;
|
|
|
|
end++;
|
2020-11-17 19:22:34 +00:00
|
|
|
}
|
2020-11-17 20:22:02 +00:00
|
|
|
|
|
|
|
// Create paragraph at end
|
2021-05-06 05:58:04 +00:00
|
|
|
if (end == edit.length() && edit.charAt(end - 1) != '\n') {
|
|
|
|
edit.append("\n");
|
2020-11-17 20:22:02 +00:00
|
|
|
end++;
|
|
|
|
}
|
|
|
|
|
2021-05-06 05:58:04 +00:00
|
|
|
if (end == edit.length())
|
|
|
|
edit.append("\n"); // workaround Android bug
|
2020-11-17 20:22:02 +00:00
|
|
|
|
2021-05-06 06:22:04 +00:00
|
|
|
return new Pair<>(start, end);
|
2020-11-17 19:22:34 +00:00
|
|
|
}
|
|
|
|
|
2022-12-07 12:31:22 +00:00
|
|
|
static private Pair<Integer, Integer> getWord(TextView tvBody) {
|
|
|
|
int start = tvBody.getSelectionStart();
|
|
|
|
int end = tvBody.getSelectionEnd();
|
|
|
|
Spannable edit = (Spannable) tvBody.getText();
|
|
|
|
|
|
|
|
if (start < 0 || end < 0)
|
|
|
|
return null;
|
|
|
|
|
|
|
|
if (start > end) {
|
|
|
|
int tmp = start;
|
|
|
|
start = end;
|
|
|
|
end = tmp;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Expand selection at start
|
|
|
|
while (start > 0 && edit.charAt(start - 1) != ' ' && edit.charAt(start - 1) != '\n')
|
|
|
|
start--;
|
|
|
|
|
|
|
|
// Expand selection at end
|
|
|
|
while (end < edit.length() && edit.charAt(end) != ' ' && edit.charAt(end) != '\n')
|
|
|
|
end++;
|
|
|
|
|
|
|
|
if (start == end)
|
|
|
|
return null;
|
|
|
|
|
|
|
|
return new Pair<>(start, end);
|
|
|
|
}
|
|
|
|
|
2022-09-20 08:24:21 +00:00
|
|
|
public static Pair<Integer, Integer> getParagraph(TextView tvBody) {
|
|
|
|
return getParagraph(tvBody, false);
|
2022-09-18 09:33:32 +00:00
|
|
|
}
|
|
|
|
|
2022-09-20 08:24:21 +00:00
|
|
|
public static Pair<Integer, Integer> getParagraph(TextView tvBody, boolean block) {
|
|
|
|
int start = tvBody.getSelectionStart();
|
|
|
|
int end = tvBody.getSelectionEnd();
|
|
|
|
Spannable edit = (Spannable) tvBody.getText();
|
2022-09-18 09:33:32 +00:00
|
|
|
|
|
|
|
if (start < 0 || end < 0)
|
|
|
|
return null;
|
|
|
|
|
|
|
|
if (start > end) {
|
|
|
|
int tmp = start;
|
|
|
|
start = end;
|
|
|
|
end = tmp;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Expand selection at start
|
|
|
|
while (start > 0 &&
|
|
|
|
(edit.charAt(start - 1) != '\n' ||
|
|
|
|
(block && start - 1 > 0 && edit.charAt(start - 2) != '\n')))
|
|
|
|
start--;
|
|
|
|
|
|
|
|
if (start == end && end < edit.length())
|
|
|
|
end++;
|
|
|
|
|
|
|
|
// Expand selection at end
|
|
|
|
while (end > 0 && end < edit.length() &&
|
|
|
|
(edit.charAt(end - 1) != '\n' ||
|
|
|
|
(block && end - 1 > 0 && edit.charAt(end - 2) != '\n')))
|
|
|
|
end++;
|
|
|
|
|
|
|
|
// Trim start
|
|
|
|
while (start < edit.length() - 1 && edit.charAt(start) == '\n')
|
|
|
|
start++;
|
|
|
|
|
|
|
|
// Trim end
|
|
|
|
while (end > 0 && edit.charAt(end - 1) == '\n')
|
|
|
|
end--;
|
|
|
|
|
|
|
|
if (start < end)
|
|
|
|
return new Pair(start, end);
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2020-11-17 18:37:56 +00:00
|
|
|
static <T extends ParagraphStyle> T clone(Object span, Class<T> type, Context context) {
|
|
|
|
if (QuoteSpan.class.isAssignableFrom(type)) {
|
|
|
|
QuoteSpan q = (QuoteSpan) span;
|
|
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P)
|
|
|
|
return (T) new QuoteSpan(q.getColor());
|
|
|
|
else
|
|
|
|
return (T) new QuoteSpan(q.getColor(), q.getStripeWidth(), q.getGapWidth());
|
|
|
|
} else if (NumberSpan.class.isAssignableFrom(type)) {
|
|
|
|
NumberSpan n = (NumberSpan) span;
|
2021-05-07 12:09:16 +00:00
|
|
|
int bulletGap = context.getResources().getDimensionPixelSize(R.dimen.bullet_gap_size);
|
|
|
|
int bulletIndent = context.getResources().getDimensionPixelSize(R.dimen.bullet_indent_size);
|
2020-11-17 18:37:56 +00:00
|
|
|
int colorAccent = Helper.resolveColor(context, R.attr.colorAccent);
|
2021-05-07 12:09:16 +00:00
|
|
|
return (T) new NumberSpan(bulletIndent, bulletGap, colorAccent, n.getTextSize(), n.getLevel(), n.getIndex() + 1);
|
2021-05-06 06:22:04 +00:00
|
|
|
} else if (BulletSpanEx.class.isAssignableFrom(type)) {
|
|
|
|
BulletSpanEx b = (BulletSpanEx) span;
|
2021-05-07 12:09:16 +00:00
|
|
|
int bulletIndent = context.getResources().getDimensionPixelSize(R.dimen.bullet_indent_size);
|
2020-11-17 18:37:56 +00:00
|
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
|
|
|
int colorAccent = Helper.resolveColor(context, R.attr.colorAccent);
|
2021-05-07 12:09:16 +00:00
|
|
|
int bulletGap = context.getResources().getDimensionPixelSize(R.dimen.bullet_gap_size);
|
|
|
|
return (T) new BulletSpanEx(bulletIndent, bulletGap, colorAccent, b.getLevel());
|
2020-11-17 18:37:56 +00:00
|
|
|
} else
|
2021-05-07 12:09:16 +00:00
|
|
|
return (T) new BulletSpanEx(bulletIndent, b.getGapWidth(), b.getColor(), b.getBulletRadius(), b.getLevel());
|
2020-11-17 18:37:56 +00:00
|
|
|
|
|
|
|
} else
|
|
|
|
throw new IllegalArgumentException(type.getName());
|
|
|
|
}
|
|
|
|
|
2022-09-18 14:04:37 +00:00
|
|
|
static Integer getMaxListLevel(Editable edit, int start, int end) {
|
|
|
|
Integer maxLevel = null;
|
|
|
|
BulletSpan[] bullets = edit.getSpans(start, end, BulletSpan.class);
|
|
|
|
for (BulletSpan span : bullets) {
|
|
|
|
Integer level = null;
|
|
|
|
if (span instanceof NumberSpan)
|
|
|
|
level = ((NumberSpan) span).getLevel();
|
|
|
|
else if (span instanceof BulletSpanEx)
|
|
|
|
level = ((BulletSpanEx) span).getLevel();
|
|
|
|
if (level != null && (maxLevel == null || level > maxLevel))
|
|
|
|
maxLevel = level;
|
|
|
|
}
|
|
|
|
return maxLevel;
|
|
|
|
}
|
|
|
|
|
2020-11-17 18:37:56 +00:00
|
|
|
static void renumber(Editable text, boolean clean, Context context) {
|
2021-05-07 12:09:16 +00:00
|
|
|
int bulletGap = context.getResources().getDimensionPixelSize(R.dimen.bullet_gap_size);
|
|
|
|
int bulletIndent = context.getResources().getDimensionPixelSize(R.dimen.bullet_indent_size);
|
2020-11-17 18:37:56 +00:00
|
|
|
int colorAccent = Helper.resolveColor(context, R.attr.colorAccent);
|
|
|
|
|
|
|
|
Log.i("Renumber clean=" + clean + " text=" + text);
|
|
|
|
|
|
|
|
int next;
|
|
|
|
int pos = -1;
|
2021-05-06 06:22:04 +00:00
|
|
|
List<Integer> levels = new ArrayList<>();
|
2020-11-17 18:37:56 +00:00
|
|
|
for (int i = 0; i < text.length(); i = next) {
|
|
|
|
next = text.nextSpanTransition(i, text.length(), NumberSpan.class);
|
|
|
|
Log.i("Bullet span next=" + next);
|
|
|
|
|
|
|
|
BulletSpan[] spans = text.getSpans(i, next, BulletSpan.class);
|
|
|
|
for (BulletSpan span : spans) {
|
|
|
|
int start = text.getSpanStart(span);
|
|
|
|
int end = text.getSpanEnd(span);
|
|
|
|
int flags = text.getSpanFlags(span);
|
|
|
|
Log.i("Bullet span " + start + "..." + end);
|
|
|
|
|
|
|
|
if (clean && start == end) {
|
|
|
|
text.removeSpan(span);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2021-05-06 06:22:04 +00:00
|
|
|
int level;
|
|
|
|
if (span instanceof NumberSpan)
|
|
|
|
level = ((NumberSpan) span).getLevel();
|
|
|
|
else if (span instanceof BulletSpanEx)
|
|
|
|
level = ((BulletSpanEx) span).getLevel();
|
|
|
|
else
|
|
|
|
level = 0;
|
|
|
|
|
|
|
|
if (start != pos)
|
|
|
|
levels.clear();
|
|
|
|
while (levels.size() > level + 1)
|
|
|
|
levels.remove(levels.size() - 1);
|
|
|
|
if (levels.size() == level + 1 && !(span instanceof NumberSpan))
|
2021-05-06 18:46:50 +00:00
|
|
|
levels.remove(level);
|
2021-05-06 06:22:04 +00:00
|
|
|
while (levels.size() < level + 1)
|
|
|
|
levels.add(0);
|
|
|
|
|
|
|
|
int index = levels.get(level) + 1;
|
|
|
|
levels.remove(level);
|
|
|
|
levels.add(level, index);
|
2020-11-17 18:37:56 +00:00
|
|
|
|
2021-05-06 06:22:04 +00:00
|
|
|
if (span instanceof NumberSpan) {
|
2020-11-17 18:37:56 +00:00
|
|
|
NumberSpan ns = (NumberSpan) span;
|
|
|
|
if (index != ns.getIndex()) {
|
|
|
|
text.removeSpan(span);
|
2021-05-06 06:22:04 +00:00
|
|
|
// Text size needs measuring
|
2021-05-07 12:09:16 +00:00
|
|
|
NumberSpan clone = new NumberSpan(bulletIndent, bulletGap, colorAccent, ns.getTextSize(), level, index);
|
2020-11-17 18:37:56 +00:00
|
|
|
text.setSpan(clone, start, end, flags);
|
|
|
|
}
|
|
|
|
|
|
|
|
pos = end;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-03 11:13:26 +00:00
|
|
|
static void markAsInserted(Editable text, int start, int end) {
|
|
|
|
for (InsertedSpan span : text.getSpans(0, text.length(), InsertedSpan.class))
|
2022-03-15 08:31:04 +00:00
|
|
|
text.removeSpan(span);
|
2022-05-03 11:17:29 +00:00
|
|
|
if (start >= 0 && start < end && end <= text.length())
|
|
|
|
text.setSpan(new InsertedSpan(), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
2022-03-15 08:31:04 +00:00
|
|
|
}
|
|
|
|
|
2022-05-03 11:13:26 +00:00
|
|
|
static class InsertedSpan implements NoCopySpan {
|
2022-03-15 08:31:04 +00:00
|
|
|
}
|
|
|
|
|
2022-05-04 09:30:00 +00:00
|
|
|
static class MarkSpan extends BackgroundColorSpan {
|
|
|
|
public MarkSpan() {
|
|
|
|
super(Color.YELLOW);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void updateDrawState(@NonNull TextPaint textPaint) {
|
|
|
|
super.updateDrawState(textPaint);
|
|
|
|
textPaint.setColor(Color.BLACK);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-11 08:36:32 +00:00
|
|
|
static String getFamily(String family) {
|
2022-02-12 15:38:38 +00:00
|
|
|
// https://web.mit.edu/jmorzins/www/fonts.html
|
2022-02-12 19:58:01 +00:00
|
|
|
// https://en.wikipedia.org/wiki/Croscore_fonts
|
2022-02-12 15:38:38 +00:00
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/CSS/font-family
|
2022-02-12 19:58:01 +00:00
|
|
|
// TODO: Microsoft: Georgia (Serif), Tahoma (Sans-serif), Trebuchet MS (Sans-serif)
|
2021-06-05 16:12:22 +00:00
|
|
|
String faces = family.toLowerCase(Locale.ROOT);
|
2022-02-24 19:55:07 +00:00
|
|
|
if (faces.contains("montserrat"))
|
2022-03-05 20:17:46 +00:00
|
|
|
return "Montserrat, Gotham, \"Proxima Nova\", sans-serif";
|
2022-02-10 20:46:55 +00:00
|
|
|
if (faces.contains("arimo"))
|
2022-03-05 20:16:44 +00:00
|
|
|
return "Arimo, Arial, Verdana, Helvetica, \"Helvetica Neue\", sans-serif";
|
2022-02-10 20:46:55 +00:00
|
|
|
if (faces.contains("tinos"))
|
2022-02-12 10:31:39 +00:00
|
|
|
return "Tinos, \"Times New Roman\", Times, serif";
|
2022-02-10 20:46:55 +00:00
|
|
|
if (faces.contains("cousine"))
|
2022-02-12 10:31:39 +00:00
|
|
|
return "Cousine, \"Courier New\", Courier, monospace";
|
2022-02-10 20:46:55 +00:00
|
|
|
if (faces.contains("lato"))
|
2022-02-11 14:49:30 +00:00
|
|
|
return "Lato, Carlito, Calibri, sans-serif";
|
|
|
|
if (faces.contains("caladea"))
|
|
|
|
return "Caladea, Cambo, Cambria, serif";
|
2022-02-12 11:28:52 +00:00
|
|
|
if (faces.contains("comic sans"))
|
2022-02-11 15:57:50 +00:00
|
|
|
return "OpenDyslexic, \"Comic Sans\", \"Comic Sans MS\", sans-serif";
|
2022-02-11 08:36:32 +00:00
|
|
|
return family;
|
|
|
|
}
|
2022-02-10 20:46:55 +00:00
|
|
|
|
2022-02-11 08:36:32 +00:00
|
|
|
static TypefaceSpan getTypefaceSpan(String family, Context context) {
|
|
|
|
family = getFamily(family);
|
2021-06-05 16:12:22 +00:00
|
|
|
return new CustomTypefaceSpan(family, getTypeface(family, context));
|
2021-06-01 12:46:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static Typeface getTypeface(String family, Context context) {
|
2022-02-11 08:36:32 +00:00
|
|
|
if (TextUtils.isEmpty(family))
|
|
|
|
return Typeface.DEFAULT;
|
|
|
|
|
2022-02-12 09:04:05 +00:00
|
|
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
|
|
|
boolean bundled_fonts = prefs.getBoolean("bundled_fonts", true);
|
|
|
|
|
2022-02-10 20:46:55 +00:00
|
|
|
List<String> faces = new ArrayList<>();
|
|
|
|
for (String face : family.split(","))
|
|
|
|
faces.add(face
|
|
|
|
.trim()
|
|
|
|
.toLowerCase(Locale.ROOT)
|
2022-02-11 15:57:50 +00:00
|
|
|
.replace("'", "")
|
2022-02-10 20:46:55 +00:00
|
|
|
.replace("\"", ""));
|
|
|
|
|
|
|
|
if (faces.contains("fairemail"))
|
2022-04-25 08:13:48 +00:00
|
|
|
return ResourcesCompat.getFont(context.getApplicationContext(), R.font.fantasy);
|
2021-06-05 18:47:05 +00:00
|
|
|
|
2022-02-12 09:04:05 +00:00
|
|
|
if (bundled_fonts) {
|
2022-03-03 19:54:40 +00:00
|
|
|
if (faces.contains("montserrat") ||
|
2022-03-05 20:17:46 +00:00
|
|
|
faces.contains("gotham") ||
|
|
|
|
faces.contains("proxima nova"))
|
2022-04-25 08:13:48 +00:00
|
|
|
return ResourcesCompat.getFont(context.getApplicationContext(), R.font.montserrat);
|
2022-02-24 19:55:07 +00:00
|
|
|
|
2022-02-12 09:04:05 +00:00
|
|
|
if (faces.contains("arimo") ||
|
|
|
|
faces.contains("arial") ||
|
|
|
|
faces.contains("verdana") ||
|
2022-02-19 09:26:07 +00:00
|
|
|
faces.contains("helvetica") ||
|
|
|
|
faces.contains("helvetica neue"))
|
2022-04-25 08:13:48 +00:00
|
|
|
return ResourcesCompat.getFont(context.getApplicationContext(), R.font.arimo);
|
2022-02-12 09:04:05 +00:00
|
|
|
|
|
|
|
if (faces.contains("tinos") ||
|
2022-02-12 10:31:39 +00:00
|
|
|
faces.contains("times") ||
|
2022-02-12 09:04:05 +00:00
|
|
|
faces.contains("times new roman"))
|
2022-04-25 08:13:48 +00:00
|
|
|
return ResourcesCompat.getFont(context.getApplicationContext(), R.font.tinos);
|
2022-02-12 09:04:05 +00:00
|
|
|
|
|
|
|
if (faces.contains("cousine") ||
|
2022-02-12 10:31:39 +00:00
|
|
|
faces.contains("courier") ||
|
2022-02-12 09:04:05 +00:00
|
|
|
faces.contains("courier new"))
|
2022-04-25 08:13:48 +00:00
|
|
|
return ResourcesCompat.getFont(context.getApplicationContext(), R.font.cousine);
|
2022-02-12 09:04:05 +00:00
|
|
|
|
|
|
|
if (faces.contains("lato") ||
|
|
|
|
faces.contains("carlito") ||
|
|
|
|
faces.contains("calibri"))
|
2022-04-25 08:13:48 +00:00
|
|
|
return ResourcesCompat.getFont(context.getApplicationContext(), R.font.lato);
|
2022-02-12 09:04:05 +00:00
|
|
|
|
|
|
|
if (faces.contains("caladea") ||
|
|
|
|
faces.contains("cambo") ||
|
|
|
|
faces.contains("cambria"))
|
2022-04-25 08:13:48 +00:00
|
|
|
return ResourcesCompat.getFont(context.getApplicationContext(), R.font.caladea);
|
2022-02-12 09:04:05 +00:00
|
|
|
|
|
|
|
if (faces.contains("opendyslexic") ||
|
|
|
|
faces.contains("comic sans") ||
|
|
|
|
faces.contains("comic sans ms"))
|
2022-04-25 08:13:48 +00:00
|
|
|
return ResourcesCompat.getFont(context.getApplicationContext(), R.font.opendyslexic);
|
2022-02-12 09:04:05 +00:00
|
|
|
}
|
2021-06-05 18:47:05 +00:00
|
|
|
|
2022-02-10 20:46:55 +00:00
|
|
|
for (String face : faces) {
|
2021-06-05 18:47:05 +00:00
|
|
|
Typeface tf = Typeface.create(face, Typeface.NORMAL);
|
|
|
|
if (!tf.equals(Typeface.DEFAULT))
|
|
|
|
return tf;
|
2021-06-05 16:12:22 +00:00
|
|
|
}
|
2022-02-10 20:46:55 +00:00
|
|
|
|
2021-06-05 18:47:05 +00:00
|
|
|
return Typeface.DEFAULT;
|
2021-06-01 11:34:57 +00:00
|
|
|
}
|
|
|
|
|
2022-02-10 15:05:52 +00:00
|
|
|
public static List<FontDescriptor> getFonts(Context context) {
|
2022-02-12 14:16:14 +00:00
|
|
|
return getFonts(context, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
public static List<FontDescriptor> getFonts(Context context, boolean all) {
|
|
|
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
|
|
|
boolean bundled_fonts = prefs.getBoolean("bundled_fonts", true);
|
|
|
|
|
2022-02-10 15:05:52 +00:00
|
|
|
List<FontDescriptor> result = new ArrayList<>();
|
2022-02-07 11:28:19 +00:00
|
|
|
String[] fontNameNames = context.getResources().getStringArray(R.array.fontNameNames);
|
|
|
|
String[] fontNameValues = context.getResources().getStringArray(R.array.fontNameValues);
|
|
|
|
for (int i = 0; i < fontNameNames.length; i++)
|
2022-02-10 15:05:52 +00:00
|
|
|
result.add(new FontDescriptor(fontNameValues[i], fontNameNames[i]));
|
2022-02-10 20:46:55 +00:00
|
|
|
|
|
|
|
// https://en.wikipedia.org/wiki/Croscore_fonts
|
2022-02-12 14:16:14 +00:00
|
|
|
if (all || bundled_fonts) {
|
|
|
|
result.add(new FontDescriptor("arimo", "Arimo (Arial, Verdana)", true));
|
|
|
|
result.add(new FontDescriptor("tinos", "Tinos (Times New Roman)", true));
|
|
|
|
result.add(new FontDescriptor("cousine", "Cousine (Courier New)", true));
|
|
|
|
result.add(new FontDescriptor("lato", "Lato (Calibri)", true));
|
|
|
|
result.add(new FontDescriptor("caladea", "Caladea (Cambria)", true));
|
2022-02-24 19:55:07 +00:00
|
|
|
|
|
|
|
if (BuildConfig.DEBUG) {
|
|
|
|
result.add(new FontDescriptor("montserrat", "Montserrat", true));
|
|
|
|
}
|
2022-02-12 14:16:14 +00:00
|
|
|
|
|
|
|
result.add(new FontDescriptor("comic sans", "OpenDyslexic", true));
|
|
|
|
}
|
2022-02-10 20:46:55 +00:00
|
|
|
|
2022-02-07 11:28:19 +00:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2022-02-10 15:05:52 +00:00
|
|
|
public static class FontDescriptor {
|
|
|
|
@NonNull
|
|
|
|
public String type;
|
|
|
|
@NonNull
|
|
|
|
public String name;
|
|
|
|
public boolean custom;
|
|
|
|
|
|
|
|
FontDescriptor(String type, String name) {
|
|
|
|
this(type, name, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
FontDescriptor(String type, String name, boolean custom) {
|
|
|
|
this.type = type;
|
|
|
|
this.name = name;
|
|
|
|
this.custom = custom;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public String toString() {
|
|
|
|
return name;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-17 18:37:56 +00:00
|
|
|
//TextUtils.dumpSpans(text, new LogPrinter(android.util.Log.INFO, "FairEmail"), "afterTextChanged ");
|
2019-09-27 17:05:34 +00:00
|
|
|
}
|