From 4d2d4f70420e51376f0af1624ef0458da85cefa6 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Wed, 2 Feb 2022 10:52:30 +0530 Subject: [PATCH] Use ActionMode instead of popup window in editor Updated editor --- .../main/assets/data/common/gradle-plugin.zip | Bin 37274 -> 37273 bytes .../views/editor/EditorTextActionMode.java | 235 ++++++++++++++ .../androidide/views/editor/IDEEditor.java | 13 + .../views/editor/SignatureHelpWindow.java | 4 + .../rosemoe/sora/graphics/GraphicTextRow.java | 4 +- .../sora/lang/styling/MappedSpans.java | 10 +- .../github/rosemoe/sora/text/AndroidBidi.java | 295 ++++++++++++++++++ .../io/github/rosemoe/sora/text/ICUUtils.java | 2 + .../rosemoe/sora/widget/CodeEditor.java | 8 +- 9 files changed, 559 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/com/itsaky/androidide/views/editor/EditorTextActionMode.java create mode 100644 editor/src/main/java/io/github/rosemoe/sora/text/AndroidBidi.java diff --git a/app/src/main/assets/data/common/gradle-plugin.zip b/app/src/main/assets/data/common/gradle-plugin.zip index b6eb1fc1dcc0a035bdab1b1e52e6f1d59c0149d4..c33c8cdebf1c399ba35fc33f73baa2d991b7c35a 100644 GIT binary patch delta 2316 zcmZWq2Ut^A7=0O#1eq!UMP(?cU@?QBC}E=n5)3tFjta8U>=AfQPI5)dTjs!&9c z;YKS&#Zdv#x==BOf>on+*H#5 z>mUuEupixk_rwOn1F(;D$OcvsKVP5dg<)6#TEQFX~@&Ft+s=#P1wlh%?KK|hcSa>Bbxxd-T^sxR4+rw*O*r1y`48J~ z?cvU-#a4$RN9rHAPW;n6BU#ZE3?w}-Yp%?1xzlLFrS@6hjy-F-;?WbAtmGLkC-dK} zlhV7xoK0AjPhE#}cR2j@F}}*2t_Wi7$yQZr1*@M#m!%MvYHMoS>R$S`spZ<;wTV&A zQ~68O+EPti*9zB%pd|aP&blSDWlOt@uy60~c8kYZdoR8vHLV)r^C2iPU2}ShFoHxj z!!RlpGl-C#w2K4+DREpjvyl<*#X0OXrHRkyvG^P&o5y5o!Xnu$9_t8W774>QUhh~y z7!S3X09O>$X0|`mxNTWXaa7hD!LoKX~arZG{fcr zeHx1cKBn^EE(KKO2BF=%xhIf`*~1B|THzSqLKAz#7c~N?l$PoZL&2jw%QaCtH&3BN zaQp{K#rZCki+$V9lX<&LYm*XARlh2H({`e#UOj%^_|osLu_j+5|DB-dS_t}^BVu+f z$nou?K?4Uo9(T8n(~)cYEMaHdDoUBea^Bpr1;fwBp4n&DP%g3eeWH~MD=se|vE$Lo znK6mVz`8jCTaz(clPAV}lR_7TB+zZ;r>S-3Ubh~~Z@X(97x!seO-DY<_1A%@L(x&% zof*^SEdF58)X4F9S9Il<=gofjS1qY(>IKJE&)~AX84R|!CQQI<3gdZmID7%KIgHKZ zcttYVtSJ!!lR@?!n%}+DXy56dRiOv}&&5hRFU!{J(8Z5qOxhQPG*{d_=Q>7bbJfFh z`aKGM4vI_?%7!8bz{UMcHzLyHl#I|8KqepDBph|W@A?{rTkjIRZQQ~wGr613Zf{xf zba&3gw>4WtXQq36Fn&#YJ5RL;V_W7*2>A?R$OqvFJ-*yWi6kubg*A#fw2FD0|d=B0%5I} ziKqj-t_WxN#iO6v7q~Z{aCGph@GD{~4b-H$0lw0mNHT-BlO_ox2*lx}&v|&N=(VY5 zB8JUM$FM-$i2_4I@D~j)9d`t~K%f&`p}2+{V%QMknY4&A_>l5Oq1tG^8MvN!&{!0W zbfpzL5PpZB!a!276?(F!*ny}>gPh?Wg9(Rv1!ve1F)WjUVIep|e_*q2Hqxl?hM&qf zXc!K2G!uwRW`d4&1Lx@1IS>LsN=d{d=HPRNnfEpr|1u+R9G76jJT&Iz5(nb4EXWu> z8CrBOL$hU1&32eXW~9OTx@W_$769JKtsISsXB-!=>qMX}6(LNSy{f_uj$!x@!s~bd zVnTtB!WxZKwcdd^H5bvtgjAOe7%^;ZVeMSlDTNC!h|Y*L1KDNfNIhJN!Up42FWZm@ z#Hy)4UuKS+j}qxc8J$?a8b0?i*dEP_3mjuNto5=nAVn39Ft`-KVYWdG7i1O=OpeP0 U9Bz^n9_kBZ8-|(crG}k<0WMA*H2?qr delta 2196 zcmZWp2~bm46#e;$CIl1#4Pa1FC?Zl4NCJYOMnMx20xIBAK@?f0wWaJBfe^wfAj=as zKtxc-1|(qgd_&&k73POK z1M7lLo#YREKGPY*x3&a(sYjfm{Nd`F5BWi8uhs z@Epgrkqj_<8ksF3pYMIVqwm@J_99E~4W-9Q_peZw`qjGE45e3h6&Hp*boz|xowd__ ztgp=|3hB21x;jC=XS+!5_7qC%&gnaSB-Q;bX(!$>9~7s>F>YlX$Z(gGdO7QUZ+2-< zL%MU8Z1F;+G`4x;P}ktp5EK7adT7_if^NKp;#E*OvHL|OdLLeXz}1*3kqhzyP5-1v z8_1UaS#hAX@*mgJHghbNdxS2ZIZw>)6a=%3hx~<~EGx@L+-Q|$1AQt7df;^zv$cL+ zcdq`++!Xv%)N7-%!tRKqZzLEIK+Q#sg;#YMyQY!T&zpE6iM`NM>?m$j3F-tQ2ktSk zsEI39i+O6UgeT!RkiTIHp3puF*fOCL)H0jUPAO}LqT;h*o$`K1@p#w1LruEs#^Z+m zQ*WMD1}16?PZlgK0V)N*8!a&l zYH!=Svm270@2l=l*5+AiVm8+oEKCvjj%%Zm;7CzgJ#CY+ER-_Jz<8qz!rS!|_-6gi z_fHx;@V%;Zx`r3~8AY#eyl#5&&bgVWJHM-nv3JVT; z;l2Mg3Fyns>ffcrYRoS(bvF`>o}v^*rJ@W)QCc~QFVo7A%#aC!;`B(QM&uO}GZ;9F zT3F!<4bce1X6Ty}ibHMye}gq#%i$V(s^l%BSPO*vGWQU$ats7xmE51E;bef=ObsV7 z(S9(O0+%-Sy#|}P1aH+r>K0|$;1oK1wYBvTk?_gnuLjqaJv`nn48y!*FpLkG#-1v< zgeX`7ZA7a8%*6HwNKG($U$6m=@=J`Rq`O9%JxHDbMjrYfD0rdjha|n?CAl@-`-(9u1k7f!KWg8ARU=%H%GE>l7jv2T zj|r%rm1l&$@7Tm!75kx!(8=F+Z!Xx7=YueWNsW!6ht?T3;A);P!sMG2?6!ir|F^9c zVm!dSG|d%Cwli_(M9`Jz4o0>cqOUT@XW}JWrHm3WKuV#}_#Og*{RjBL`~?62 diff --git a/app/src/main/java/com/itsaky/androidide/views/editor/EditorTextActionMode.java b/app/src/main/java/com/itsaky/androidide/views/editor/EditorTextActionMode.java new file mode 100644 index 000000000..253c0330d --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/views/editor/EditorTextActionMode.java @@ -0,0 +1,235 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE 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. + * + * AndroidIDE 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 AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.views.editor; + +import android.annotation.SuppressLint; +import android.content.res.TypedArray; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.view.Menu; +import android.view.MenuItem; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.view.ActionMode; +import androidx.appcompat.view.menu.MenuBuilder; +import androidx.core.content.ContextCompat; + +import com.itsaky.androidide.R; + +import io.github.rosemoe.sora.event.ClickEvent; +import io.github.rosemoe.sora.event.LongPressEvent; +import io.github.rosemoe.sora.event.SelectionChangeEvent; +import io.github.rosemoe.sora.widget.component.EditorTextActionWindow; + +/** + * Text action mode actions for the editor. + * + * @author Akash Yadav + */ +public class EditorTextActionMode { + + private final IDEEditor editor; + private ActionMode actionMode; + + public EditorTextActionMode (IDEEditor editor) { + this.editor = editor; + + // Disable default text action window + this.editor.getComponent (EditorTextActionWindow.class).setEnabled (false); + + this.editor.subscribeEvent (SelectionChangeEvent.class, ((event, unsubscribe) -> { + final var left = event.getLeft (); + final var right = event.getRight (); + if (left.index != right.index) { + showAction(); + } + })); + + // When click event is received, finish the action mode if it is started + this.editor.subscribeEvent (ClickEvent.class, (event, unsubscribe) -> exit ()); + + // Start action mode even if no text is selected + this.editor.subscribeEvent (LongPressEvent.class, (event, unsubscribe) -> { + if (actionMode == null) { + showAction (); + } + }); + } + + private void showAction () { + if (actionMode != null) { + return; + } + + actionMode = ((AppCompatActivity) editor.getContext ()).startSupportActionMode (new ActionMode.Callback () { + + @SuppressLint("RestrictedApi") + @Override + public boolean onCreateActionMode (ActionMode mode, Menu menu) { + if (menu instanceof MenuBuilder) { + MenuBuilder builder = (MenuBuilder) menu; + builder.setOptionalIconsVisible(true); + } + mode.setTitle(android.R.string.selectTextMode); + TypedArray array = editor.getContext().getTheme().obtainStyledAttributes(new int[]{ + android.R.attr.actionModeSelectAllDrawable, + android.R.attr.actionModeCutDrawable, + android.R.attr.actionModeCopyDrawable, + android.R.attr.actionModePasteDrawable, + }); + + menu.add (0, 8, 0, R.string.action_expand_selection) + .setShowAsActionFlags (1) + .setIcon (createDrawable (R.drawable.ic_expand_selection)); + + menu.add(0, 0, 0, editor.getContext().getString(android.R.string.selectAll)) + .setShowAsActionFlags(1) + .setIcon(array.getDrawable(0)); + + if (editor.isEditable()) { + menu.add(0, 1, 0, editor.getContext().getString(android.R.string.cut)) + .setShowAsActionFlags(1) + .setIcon(array.getDrawable(1)); + } + + menu.add(0, 2, 0, editor.getContext().getString(android.R.string.copy)) + .setShowAsActionFlags(1) + .setIcon(array.getDrawable(2)); + + if (editor.isEditable()) { + menu.add(0, 3, 0, editor.getContext().getString(android.R.string.paste)) + .setShowAsActionFlags(1) + .setIcon(array.getDrawable(3)); + } + + menu.add(0, 4, 0, editor.getContext().getString(com.itsaky.androidide.R.string.menu_navigate_definition)) + .setShowAsActionFlags(1) + .setIcon(createDrawable(com.itsaky.androidide.R.drawable.ic_goto_definition)); + + menu.add(0, 5, 0, editor.getContext().getString(com.itsaky.androidide.R.string.menu_navigate_references)) + .setShowAsActionFlags(1) + .setIcon(createDrawable(com.itsaky.androidide.R.drawable.ic_find_references)); + + menu.add(0, 6, 0, editor.getContext().getString(com.itsaky.androidide.R.string.menu_comment_line)) + .setShowAsActionFlags(1) + .setIcon(createDrawable(com.itsaky.androidide.R.drawable.ic_comment_line)); + + menu.add(0, 7, 0, editor.getContext().getString(com.itsaky.androidide.R.string.menu_uncomment_line)) + .setShowAsActionFlags(1) + .setIcon(createDrawable(com.itsaky.androidide.R.drawable.ic_uncomment_line)); + + array.recycle(); + return true; + } + + private Drawable createDrawable(int icon) { + final Drawable d = ContextCompat.getDrawable(editor.getContext(), icon); + if (d != null) { + d.setColorFilter(ContextCompat.getColor(editor.getContext(), com.itsaky.androidide.R.color.secondaryColor), PorterDuff.Mode.SRC_ATOP); + } + return d; + } + + @Override + public boolean onPrepareActionMode (ActionMode mode, Menu menu) { + if (editor.getFile() == null) return false; + + final String name = editor.getFile().getName(); + final boolean isJava = name.endsWith(".java") || name.endsWith(".gradle"); + final boolean isXml = name.endsWith(".xml"); + final MenuItem def = menu.findItem(4); + final MenuItem ref = menu.findItem(5); + final MenuItem comment = menu.findItem(6); + final MenuItem uncomment = menu.findItem(7); + + comment.setEnabled(isJava || isXml).getIcon().setAlpha(isJava || isXml ? 255 : 76); + uncomment.setEnabled(isJava || isXml).getIcon().setAlpha(isJava || isXml ? 255 : 76); + + // These menu items may, or may not be added + // So we need to check if its null or not + // before we perform any further actions + if (def != null) { + def.getIcon().setAlpha(isJava ? 255 : 76); + def.setVisible(isJava); + } + + if (ref != null) { + ref.getIcon().setAlpha(isJava ? 255 : 76); + ref.setVisible(isJava); + } + + return true; + } + + @Override + public boolean onActionItemClicked (ActionMode mode, MenuItem item) { + switch (item.getItemId()) { + case 0: + editor.selectAll(); + break; + case 1: + editor.cutText(); + break; + case 2: + editor.copyText(); + break; + case 3: + editor.pasteText(); + break; + case 4: + editor.findDefinition(); + break; + case 5: + editor.findReferences(); + break; + case 6: + editor.commentLine(); + break; + case 7: + editor.uncommentLine(); + break; + case 8 : + editor.expandSelection (); + break; + } + + final var id = item.getItemId (); + if (id == 0 || id == 8) { + return true; + } + + return exit (); + } + + @Override + public void onDestroyActionMode (ActionMode mode) { + actionMode = null; + editor.setSelection (editor.getCursorRange ().getStart ()); + } + }); + } + + public boolean exit () { + if (actionMode != null) { + actionMode.finish(); + actionMode = null; + return true; + } + return false; + } +} diff --git a/app/src/main/java/com/itsaky/androidide/views/editor/IDEEditor.java b/app/src/main/java/com/itsaky/androidide/views/editor/IDEEditor.java index c527039b9..6bba84eed 100644 --- a/app/src/main/java/com/itsaky/androidide/views/editor/IDEEditor.java +++ b/app/src/main/java/com/itsaky/androidide/views/editor/IDEEditor.java @@ -67,6 +67,9 @@ public class IDEEditor extends CodeEditor { private IDELanguageClientImpl mLanguageClient; private SignatureHelpWindow mSignatureHelpWindow; + @SuppressWarnings("FieldCanBeLocal,unused") + private final EditorTextActionMode mActionMode; + public static final String KEY_FILE = "editor_file"; private static final Logger LOG = Logger.instance ("IDEEditor"); @@ -85,6 +88,8 @@ public class IDEEditor extends CodeEditor { public IDEEditor (Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); + this.mActionMode = new EditorTextActionMode (this); + setColorScheme (new SchemeAndroidIDE ()); subscribeEvent (ContentChangeEvent.class, (event, unsubscribe) -> notifyContentChanged ()); @@ -106,6 +111,14 @@ public class IDEEditor extends CodeEditor { range.getEnd ().getColumn ()); } + /** + * Set the selection of this editor to the given position. + * @param position The position to select. + */ + public void setSelection (@NonNull Position position) { + setSelection (position.getLine (), position.getColumn ()); + } + /** * Set the file that this editor is currently editing. * @param file The file to set. diff --git a/app/src/main/java/com/itsaky/androidide/views/editor/SignatureHelpWindow.java b/app/src/main/java/com/itsaky/androidide/views/editor/SignatureHelpWindow.java index 62666c060..08aabaed9 100644 --- a/app/src/main/java/com/itsaky/androidide/views/editor/SignatureHelpWindow.java +++ b/app/src/main/java/com/itsaky/androidide/views/editor/SignatureHelpWindow.java @@ -31,6 +31,7 @@ import androidx.core.content.ContextCompat; import com.blankj.utilcode.util.SizeUtils; import com.itsaky.androidide.R; +import com.itsaky.androidide.utils.Logger; import com.itsaky.lsp.models.SignatureHelp; import com.itsaky.lsp.models.SignatureInformation; @@ -45,6 +46,8 @@ public class SignatureHelpWindow extends EditorPopupWindow { private final TextView text; + private static final Logger LOG = Logger.instance ("SignatureHelpWindow"); + /** * Create a signature help popup window for editor * @@ -78,6 +81,7 @@ public class SignatureHelpWindow extends EditorPopupWindow { } public void setSignatureHelp (SignatureHelp signature) { + if (signature == null || signature.getSignatures ().isEmpty ()) { if (isShowing ()) { dismiss (); diff --git a/editor/src/main/java/io/github/rosemoe/sora/graphics/GraphicTextRow.java b/editor/src/main/java/io/github/rosemoe/sora/graphics/GraphicTextRow.java index 16347aae5..a7b09488c 100644 --- a/editor/src/main/java/io/github/rosemoe/sora/graphics/GraphicTextRow.java +++ b/editor/src/main/java/io/github/rosemoe/sora/graphics/GraphicTextRow.java @@ -114,12 +114,12 @@ public class GraphicTextRow { if (mText.widthCache != null) { float w = 0f; var cache = mText.widthCache; - for (int i = start; i < mEnd;i++) { + for (int i = start; i <= mEnd;i++) { if (w > advance) { mBuffer[0] = Math.max(start, i - 1); mBuffer[1] = i > start ? w - cache[i - 1] : w; return mBuffer; - } else { + } else if(i < mEnd) { w += cache[i]; } } diff --git a/editor/src/main/java/io/github/rosemoe/sora/lang/styling/MappedSpans.java b/editor/src/main/java/io/github/rosemoe/sora/lang/styling/MappedSpans.java index 882b01705..f3fb7a322 100644 --- a/editor/src/main/java/io/github/rosemoe/sora/lang/styling/MappedSpans.java +++ b/editor/src/main/java/io/github/rosemoe/sora/lang/styling/MappedSpans.java @@ -38,13 +38,13 @@ import io.github.rosemoe.sora.widget.schemes.EditorColorScheme; * @see Builder */ public class MappedSpans implements Spans { - + private final List> spanMap; - + private MappedSpans(@NonNull List> spanMap) { this.spanMap = spanMap; } - + @Override public void adjustOnInsert(CharPosition start, CharPosition end) { var startLine = start.line; @@ -294,12 +294,12 @@ public class MappedSpans implements Spans { } spanMap.set(line, spans); } - + @Override public void addLineAt(int line, List spans) { spanMap.add(line, spans); } - + @Override public void deleteLineAt(int line) { spanMap.remove(line); diff --git a/editor/src/main/java/io/github/rosemoe/sora/text/AndroidBidi.java b/editor/src/main/java/io/github/rosemoe/sora/text/AndroidBidi.java new file mode 100644 index 000000000..bd7afa3c8 --- /dev/null +++ b/editor/src/main/java/io/github/rosemoe/sora/text/AndroidBidi.java @@ -0,0 +1,295 @@ +/* + * sora-editor - the awesome code editor for Android + * https://github.com/Rosemoe/sora-editor + * Copyright (C) 2020-2022 Rosemoe + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * Please contact Rosemoe by email 2073412493@qq.com if you need + * additional information or have any questions + */ +package io.github.rosemoe.sora.text; + +import android.annotation.SuppressLint; +import android.icu.text.Bidi; +import android.os.Build; + +import androidx.annotation.IntRange; +import androidx.annotation.RequiresApi; + +/** + * Bidi algorithm for text directions. + * Replacement of android.text.AndroidBidi + */ +@SuppressLint("PrivateApi") +public final class AndroidBidi { + + public final static int RUN_LENGTH_MASK = 0x03ffffff; + public static final int RUN_LEVEL_SHIFT = 26; + public static final int RUN_LEVEL_MASK = 0x3f; + public static final int RUN_RTL_FLAG = 1 << RUN_LEVEL_SHIFT; + + public static final int DIR_LEFT_TO_RIGHT = 1; + public static final int DIR_RIGHT_TO_LEFT = -1; + + public static final Directions DIRS_ALL_LEFT_TO_RIGHT = + new Directions(new int[] { 0, RUN_LENGTH_MASK }); + + public static final Directions DIRS_ALL_RIGHT_TO_LEFT = + new Directions(new int[] { 0, RUN_LENGTH_MASK | RUN_RTL_FLAG }); + + public static final int DIR_REQUEST_LTR = 1; + public static final int DIR_REQUEST_RTL = -1; + public static final int DIR_REQUEST_DEFAULT_LTR = 2; + public static final int DIR_REQUEST_DEFAULT_RTL = -2; + + private static Class bidiClass; + static { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { + try { + bidiClass = Class.forName("android.text.AndroidBidi"); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + bidiClass = null; + } + } else { + bidiClass = null; + } + } + + public static int bidi(int dir, char[] chs, byte[] chInfo) { + return 0; + } + + @RequiresApi(Build.VERSION_CODES.P) + private static int bidiImpl(int dir, char[] chs, byte[] chInfo) { + return 0; + } + + + @RequiresApi(Build.VERSION_CODES.Q) + private static int bidiImplQ(int dir, char[] chs, byte[] chInfo) { + if (chs == null || chInfo == null) { + throw new NullPointerException(); + } + + final int length = chs.length; + if (chInfo.length < length) { + throw new IndexOutOfBoundsException(); + } + + final byte paraLevel; + switch (dir) { + case DIR_REQUEST_RTL: paraLevel = Bidi.RTL; break; + case DIR_REQUEST_DEFAULT_LTR: paraLevel = Bidi.LEVEL_DEFAULT_LTR; break; + case DIR_REQUEST_DEFAULT_RTL: paraLevel = Bidi.LEVEL_DEFAULT_RTL; break; + case DIR_REQUEST_LTR: + default: paraLevel = Bidi.LTR; break; + } + final Bidi icuBidi = new Bidi(length /* maxLength */, 0 /* maxRunCount */); + icuBidi.setPara(chs, paraLevel, null /* embeddingLevels */); + for (int i = 0; i < length; i++) { + chInfo[i] = icuBidi.getLevelAt(i); + } + final byte result = icuBidi.getParaLevel(); + return (result & 0x1) == 0 ? DIR_LEFT_TO_RIGHT : DIR_RIGHT_TO_LEFT; + } + + /** + * Returns run direction information for a line within a paragraph. + * + * @param dir baseline direction, either Layout.DIR_LEFT_TO_RIGHT or + * Layout.DIR_RIGHT_TO_LEFT + * @param levels levels as returned from {@link #bidi} + * @param lstart start of the line in the levels array + * @param chars the character array (used to determine whitespace) + * @param cstart the start of the line in the chars array + * @param len the length of the line + * @return the directions + */ + public static Directions directions(int dir, byte[] levels, int lstart, + char[] chars, int cstart, int len) { + if (len == 0) { + return DIRS_ALL_LEFT_TO_RIGHT; + } + + int baseLevel = dir == DIR_LEFT_TO_RIGHT ? 0 : 1; + int curLevel = levels[lstart]; + int minLevel = curLevel; + int runCount = 1; + for (int i = lstart + 1, e = lstart + len; i < e; ++i) { + int level = levels[i]; + if (level != curLevel) { + curLevel = level; + ++runCount; + } + } + + // add final run for trailing counter-directional whitespace + int visLen = len; + if ((curLevel & 1) != (baseLevel & 1)) { + // look for visible end + while (--visLen >= 0) { + char ch = chars[cstart + visLen]; + + if (ch == '\n') { + --visLen; + break; + } + + if (ch != ' ' && ch != '\t') { + break; + } + } + ++visLen; + if (visLen != len) { + ++runCount; + } + } + + if (runCount == 1 && minLevel == baseLevel) { + // we're done, only one run on this line + if ((minLevel & 1) != 0) { + return DIRS_ALL_RIGHT_TO_LEFT; + } + return DIRS_ALL_LEFT_TO_RIGHT; + } + + int[] ld = new int[runCount * 2]; + int maxLevel = minLevel; + int levelBits = minLevel << RUN_LEVEL_SHIFT; + { + // Start of first pair is always 0, we write + // length then start at each new run, and the + // last run length after we're done. + int n = 1; + int prev = lstart; + curLevel = minLevel; + for (int i = lstart, e = lstart + visLen; i < e; ++i) { + int level = levels[i]; + if (level != curLevel) { + curLevel = level; + if (level > maxLevel) { + maxLevel = level; + } else if (level < minLevel) { + minLevel = level; + } + // XXX ignore run length limit of 2^RUN_LEVEL_SHIFT + ld[n++] = (i - prev) | levelBits; + ld[n++] = i - lstart; + levelBits = curLevel << RUN_LEVEL_SHIFT; + prev = i; + } + } + ld[n] = (lstart + visLen - prev) | levelBits; + if (visLen < len) { + ld[++n] = visLen; + ld[++n] = (len - visLen) | (baseLevel << RUN_LEVEL_SHIFT); + } + } + + // See if we need to swap any runs. + // If the min level run direction doesn't match the base + // direction, we always need to swap (at this point + // we have more than one run). + // Otherwise, we don't need to swap the lowest level. + // Since there are no logically adjacent runs at the same + // level, if the max level is the same as the (new) min + // level, we have a series of alternating levels that + // is already in order, so there's no more to do. + // + boolean swap; + if ((minLevel & 1) == baseLevel) { + minLevel += 1; + swap = maxLevel > minLevel; + } else { + swap = runCount > 1; + } + if (swap) { + for (int level = maxLevel - 1; level >= minLevel; --level) { + for (int i = 0; i < ld.length; i += 2) { + if (levels[ld[i]] >= level) { + int e = i + 2; + while (e < ld.length && levels[ld[e]] >= level) { + e += 2; + } + for (int low = i, hi = e - 2; low < hi; low += 2, hi -= 2) { + int x = ld[low]; ld[low] = ld[hi]; ld[hi] = x; + x = ld[low+1]; ld[low+1] = ld[hi+1]; ld[hi+1] = x; + } + i = e + 2; + } + } + } + } + return new Directions(ld); + } + + /** + * Replacement of {@link android.text.Layout.Directions} + */ + public static class Directions { + + private final int[] mDirections; + + public Directions(int[] directions) { + mDirections = directions; + } + + /** + * Returns number of BiDi runs. + */ + public @IntRange(from = 0) int getRunCount() { + return mDirections.length / 2; + } + + /** + * Returns the start offset of the BiDi run. + * + * @param runIndex the index of the BiDi run + * @return the start offset of the BiDi run. + */ + public @IntRange(from = 0) int getRunStart(@IntRange(from = 0) int runIndex) { + return mDirections[runIndex * 2]; + } + + /** + * Returns the length of the BiDi run. + * + * Note that this method may return too large number due to reducing the number of object + * allocations. The too large number means the remaining part is assigned to this run. The + * caller must clamp the returned value. + * + * @param runIndex the index of the BiDi run + * @return the length of the BiDi run. + */ + public @IntRange(from = 0) int getRunLength(@IntRange(from = 0) int runIndex) { + return mDirections[runIndex * 2 + 1] & RUN_LENGTH_MASK; + } + + /** + * Returns true if the BiDi run is RTL. + * + * @param runIndex the index of the BiDi run + * @return true if the BiDi run is RTL. + */ + public boolean isRunRtl(int runIndex) { + return (mDirections[runIndex * 2 + 1] & RUN_RTL_FLAG) != 0; + } + + } + +} diff --git a/editor/src/main/java/io/github/rosemoe/sora/text/ICUUtils.java b/editor/src/main/java/io/github/rosemoe/sora/text/ICUUtils.java index bfd7d284e..24023e838 100644 --- a/editor/src/main/java/io/github/rosemoe/sora/text/ICUUtils.java +++ b/editor/src/main/java/io/github/rosemoe/sora/text/ICUUtils.java @@ -74,4 +74,6 @@ public class ICUUtils { return IntPair.pack(start, end); } + + } diff --git a/editor/src/main/java/io/github/rosemoe/sora/widget/CodeEditor.java b/editor/src/main/java/io/github/rosemoe/sora/widget/CodeEditor.java index 810e7dd5f..5311be009 100644 --- a/editor/src/main/java/io/github/rosemoe/sora/widget/CodeEditor.java +++ b/editor/src/main/java/io/github/rosemoe/sora/widget/CodeEditor.java @@ -782,7 +782,7 @@ public class CodeEditor extends View implements ContentListener, StyleReceiver, /** * Set the editor's language. - * A language is a for auto-completion,highlight and auto indent analysis. + * A language is a tool for auto-completion,highlight and auto indent analysis. * * @param lang New EditorLanguage for editor */ @@ -1847,7 +1847,7 @@ public class CodeEditor extends View implements ContentListener, StyleReceiver, if (ch == ' ') { paintCount = 1; } else if (ch == '\t') { - if ((getNonPrintableFlags() & FLAG_DRAW_TAB_SAME_AS_SPACE) != 0) { + if ((getNonPrintablePaintingFlags() & FLAG_DRAW_TAB_SAME_AS_SPACE) != 0) { paintCount = getTabWidth(); } else { paintLine = true; @@ -2914,7 +2914,7 @@ public class CodeEditor extends View implements ContentListener, StyleReceiver, * @see #FLAG_DRAW_LINE_SEPARATOR * @see #FLAG_DRAW_WHITESPACE_IN_SELECTION */ - public int getNonPrintableFlags() { + public int getNonPrintablePaintingFlags() { return mNonPrintableOptions; } @@ -4413,8 +4413,6 @@ public class CodeEditor extends View implements ContentListener, StyleReceiver, protected void onDraw(Canvas canvas) { super.onDraw(canvas); drawView(canvas); - int x = (int) (getWidth() - getDpUnit() * 50); - canvas.drawLine(x, 0, x, getHeight(), mPaintGraph); // Update magnifier if ((mLastCursorState != mCursorBlink.visibility || !mEventHandler.getScroller().isFinished()) && mEventHandler.mMagnifier.isShowing()) { mLastCursorState = mCursorBlink.visibility;