Use ActionMode instead of popup window in editor

Updated editor
This commit is contained in:
Akash Yadav
2022-02-02 10:52:30 +05:30
parent e5fd997013
commit 4d2d4f7042
9 changed files with 559 additions and 12 deletions
Binary file not shown.
@@ -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 <https://www.gnu.org/licenses/>.
*/
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;
}
}
@@ -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.
@@ -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 ();
@@ -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];
}
}
@@ -38,13 +38,13 @@ import io.github.rosemoe.sora.widget.schemes.EditorColorScheme;
* @see Builder
*/
public class MappedSpans implements Spans {
private final List<List<Span>> spanMap;
private MappedSpans(@NonNull List<List<Span>> 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<Span> spans) {
spanMap.add(line, spans);
}
@Override
public void deleteLineAt(int line) {
spanMap.remove(line);
@@ -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;
}
}
}
@@ -74,4 +74,6 @@ public class ICUUtils {
return IntPair.pack(start, end);
}
}
@@ -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;