diff --git a/.idea/misc.xml b/.idea/misc.xml
index 30b6c7c42..0aba872b6 100755
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -40,6 +40,7 @@
+
@@ -72,6 +73,7 @@
+
diff --git a/app/src/main/java/com/itsaky/androidide/views/editor/ActionsPopupMenu.java b/app/src/main/java/com/itsaky/androidide/views/editor/ActionsPopupMenu.java
deleted file mode 100644
index d5be58003..000000000
--- a/app/src/main/java/com/itsaky/androidide/views/editor/ActionsPopupMenu.java
+++ /dev/null
@@ -1,294 +0,0 @@
-/*
- * 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 static com.itsaky.androidide.utils.Logger.newInstance;
-
-import android.content.Context;
-import android.graphics.RectF;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.widget.PopupMenu;
-
-import com.blankj.utilcode.util.SizeUtils;
-import com.itsaky.androidide.actions.ActionData;
-import com.itsaky.androidide.actions.ActionItem;
-import com.itsaky.androidide.actions.ActionsRegistry;
-import com.itsaky.androidide.actions.editor.SelectAllAction;
-import com.itsaky.androidide.app.StudioApp;
-import com.itsaky.androidide.language.IDELanguage;
-import com.itsaky.androidide.utils.Logger;
-import com.itsaky.lsp.java.JavaLanguageServer;
-import com.itsaky.lsp.models.DiagnosticItem;
-import com.itsaky.lsp.models.Range;
-import com.itsaky.lsp.util.DiagnosticUtil;
-import com.itsaky.lsp.xml.XMLLanguageServer;
-
-import org.jetbrains.annotations.NotNull;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
-
-import io.github.rosemoe.sora.event.HandleStateChangeEvent;
-import io.github.rosemoe.sora.event.ScrollEvent;
-import io.github.rosemoe.sora.event.SelectionChangeEvent;
-import io.github.rosemoe.sora.event.SubscriptionReceipt;
-import io.github.rosemoe.sora.event.Unsubscribe;
-import io.github.rosemoe.sora.widget.CodeEditor;
-import io.github.rosemoe.sora.widget.EditorTouchEventHandler;
-
-/**
- * @author Akash Yadav
- */
-public class ActionsPopupMenu extends PopupMenu implements ActionsRegistry.ActionExecListener {
-
- private static final Logger LOG = newInstance("ActionsPopMenu");
- private static final long DELAY = 200;
- private final List> receipts = new ArrayList<>();
- private final EditorTouchEventHandler touchHandler;
- private final IDEEditor editor;
- private long mLastScroll;
- private int mLastPosition;
- private boolean showing;
-
- public ActionsPopupMenu(@NonNull Context context, @NonNull View anchor, int gravity) {
- super(context, anchor, gravity);
- this.editor = (IDEEditor) anchor;
- this.touchHandler = editor.getEventHandler();
-
- subscribe();
- }
-
- @Override
- public void dismiss() {}
-
- @Override
- public void onExec(@NotNull ActionItem action, @NotNull Object result) {
- if (!(action instanceof SelectAllAction)) {
- dismissPopup();
- }
- }
-
- public void unsubscribeEvents() {
- for (var receipt : this.receipts) {
- receipt.unsubscribe();
- }
- }
-
- public void dismissPopup() {
- super.dismiss();
- showing = false;
-
- ActionsRegistry.getInstance().unregisterActionExecListener(this);
- }
-
- public boolean isShowing() {
- return showing;
- }
-
- public void fillMenu() {
- getMenu().clear();
-
- final var data = new ActionData();
- data.put(Context.class, editor.getContext());
- data.put(IDEEditor.class, editor);
- data.put(
- CodeEditor.class, editor); // For LSP actions, as they cannot access IDEEditor class
- data.put(File.class, editor.getFile());
- data.put(DiagnosticItem.class, getDiagnosticAtCursor());
- data.put(Range.class, editor.getCursorRange());
- data.put(
- JavaLanguageServer.class,
- (JavaLanguageServer) StudioApp.getInstance().getJavaLanguageServer());
- data.put(
- XMLLanguageServer.class,
- (XMLLanguageServer) StudioApp.getInstance().getXMLLanguageServer());
-
- final var registry = ActionsRegistry.getInstance();
- registry.registerActionExecListener(this);
- registry.fillMenu(data, ActionItem.Location.EDITOR_TEXT_ACTIONS, getMenu());
- }
-
- @Override
- public void show() {
- fillMenu();
-
- if (isShowing()) {
- dismissPopup();
- }
-
- try {
- final var field = PopupMenu.class.getDeclaredField("mPopup");
- field.setAccessible(true);
- final var popup = field.get(this);
- assert popup != null;
- final var show = popup.getClass().getDeclaredMethod("show", int.class, int.class);
-
- final var location = computePopupLocation();
- final var x = location[0];
- final var y = location[1];
- LOG.debug("Show editor text actions at location:", x, y);
-
- show.invoke(popup, x, y);
- showing = true;
- } catch (Throwable e) {
- LOG.error("Unable to show text actions at location", e);
- show();
- }
- }
-
- private int getHeight() {
- return getMenu().size() * SizeUtils.dp2px(48);
- }
-
- private int selectTop(@NonNull RectF rect) {
- var rowHeight = editor.getRowHeight();
- if (rect.top - rowHeight * 3 / 2F > getHeight()) {
- return (int) (rect.top - rowHeight * 3 / 2 - getHeight());
- } else {
- return (int) (rect.bottom + rowHeight / 2);
- }
- }
-
- private int[] computePopupLocation() {
- int top;
- var cursor = this.editor.getCursor();
- if (cursor.isSelected()) {
- var leftRect = this.editor.getLeftHandleDescriptor().position;
- var rightRect = this.editor.getRightHandleDescriptor().position;
- var top1 = selectTop(leftRect);
- var top2 = selectTop(rightRect);
- top = Math.min(top1, top2);
- } else {
- top = selectTop(this.editor.getInsertHandleDescriptor().position);
- }
- top = Math.max(0, Math.min(top, this.editor.getHeight() - getHeight() - 5));
- float handleLeftX =
- this.editor.getOffset(
- this.editor.getCursor().getLeftLine(),
- this.editor.getCursor().getLeftColumn());
- float handleRightX =
- this.editor.getOffset(
- this.editor.getCursor().getRightLine(),
- this.editor.getCursor().getRightColumn());
- int panelX = (int) ((handleLeftX + handleRightX) / 2f);
- top -= editor.getRowHeight() - SizeUtils.dp2px(2);
-
- panelX += editor.getOffsetX();
- top += editor.getOffsetY();
-
- return new int[] {panelX, top};
- }
-
- protected void onSelectionChanged(SelectionChangeEvent event, Unsubscribe unsubscribe) {
- if (touchHandler.hasAnyHeldHandle()) {
- return;
- }
- if (event.isSelected()) {
- if (!isShowing()) {
- this.editor.post(this::displayWindow);
- }
- mLastPosition = -1;
- } else {
- var show = false;
- if (event.getCause() == SelectionChangeEvent.CAUSE_TAP
- && event.getLeft().index == mLastPosition
- && !isShowing()
- && !this.editor.getText().isInBatchEdit()) {
- this.editor.post(this::displayWindow);
- show = true;
- } else {
- dismissPopup();
- }
- if (event.getCause() == SelectionChangeEvent.CAUSE_TAP && !show) {
- mLastPosition = event.getLeft().index;
- } else {
- mLastPosition = -1;
- }
- }
- }
-
- private void subscribe() {
- receipts.add(editor.subscribeEvent(SelectionChangeEvent.class, this::onSelectionChanged));
- receipts.add(editor.subscribeEvent(ScrollEvent.class, this::onScrollEvent));
- receipts.add(
- editor.subscribeEvent(HandleStateChangeEvent.class, this::onHandleStateChanged));
- }
-
- protected void onScrollEvent(ScrollEvent event, Unsubscribe unsubscribe) {
- var last = mLastScroll;
- mLastScroll = System.currentTimeMillis();
- if (mLastScroll - last < DELAY) {
- postDisplay();
- }
- }
-
- protected void onHandleStateChanged(
- @NonNull HandleStateChangeEvent event, Unsubscribe unsubscribe) {
- if (event.isHeld()) {
- postDisplay();
- }
- }
-
- public void displayWindow() {
- show();
- }
-
- private void postDisplay() {
- if (!isShowing()) {
- return;
- }
-
- dismissPopup();
-
- if (!this.editor.getCursor().isSelected()) {
- return;
- }
-
- this.editor.postDelayed(
- new Runnable() {
- @Override
- public void run() {
-
- if (!touchHandler.hasAnyHeldHandle()
- && System.currentTimeMillis() - mLastScroll > DELAY
- && touchHandler.getScroller().isFinished()) {
- displayWindow();
- } else {
- editor.postDelayed(this, DELAY);
- }
- }
- },
- DELAY);
- }
-
- @Nullable
- private DiagnosticItem getDiagnosticAtCursor() {
- final var language = editor.getEditorLanguage();
- if (!(language instanceof IDELanguage)) {
- return null;
- }
-
- final var diagnostics = ((IDELanguage) language).getDiagnostics();
- return DiagnosticUtil.binarySearchDiagnostic(
- diagnostics, editor.getCursorRange().getStart());
- }
-}
diff --git a/app/src/main/java/com/itsaky/androidide/views/editor/EditorActionsMenu.kt b/app/src/main/java/com/itsaky/androidide/views/editor/EditorActionsMenu.kt
new file mode 100644
index 000000000..c0fc3fcce
--- /dev/null
+++ b/app/src/main/java/com/itsaky/androidide/views/editor/EditorActionsMenu.kt
@@ -0,0 +1,380 @@
+/*
+ * 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.Context
+import android.content.res.ColorStateList
+import android.graphics.RectF
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.GradientDrawable
+import android.view.*
+import android.widget.BaseAdapter
+import android.widget.ListView
+import androidx.appcompat.view.menu.MenuBuilder
+import androidx.appcompat.view.menu.MenuItemImpl
+import androidx.appcompat.view.menu.SubMenuBuilder
+import androidx.core.content.ContextCompat
+import androidx.transition.ChangeBounds
+import androidx.transition.TransitionManager
+import com.blankj.utilcode.util.SizeUtils
+import com.google.android.material.button.MaterialButton
+import com.google.android.material.textview.MaterialTextView
+import com.itsaky.androidide.R
+import com.itsaky.androidide.actions.ActionData
+import com.itsaky.androidide.actions.ActionItem
+import com.itsaky.androidide.actions.ActionsRegistry
+import com.itsaky.androidide.actions.ActionsRegistry.Companion.getInstance
+import com.itsaky.androidide.actions.editor.SelectAllAction
+import com.itsaky.androidide.app.StudioApp
+import com.itsaky.androidide.language.IDELanguage
+import com.itsaky.androidide.utils.Logger
+import com.itsaky.lsp.java.JavaLanguageServer
+import com.itsaky.lsp.models.DiagnosticItem
+import com.itsaky.lsp.models.Range
+import com.itsaky.lsp.util.DiagnosticUtil
+import com.itsaky.lsp.xml.XMLLanguageServer
+import io.github.rosemoe.sora.event.HandleStateChangeEvent
+import io.github.rosemoe.sora.event.ScrollEvent
+import io.github.rosemoe.sora.event.SelectionChangeEvent
+import io.github.rosemoe.sora.event.SubscriptionReceipt
+import io.github.rosemoe.sora.widget.CodeEditor
+import io.github.rosemoe.sora.widget.EditorTouchEventHandler
+import io.github.rosemoe.sora.widget.base.EditorPopupWindow
+import java.io.File
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * PopupMenu for showing editor's text and code actions.
+ *
+ * @author Akash Yadav
+ */
+@SuppressLint("RestrictedApi")
+open class EditorActionsMenu(val editor: IDEEditor) :
+ EditorPopupWindow(editor, FEATURE_SHOW_OUTSIDE_VIEW_ALLOWED),
+ ActionsRegistry.ActionExecListener,
+ MenuBuilder.Callback {
+
+ companion object {
+ const val DELAY: Long = 200
+ }
+
+ private val touchHandler: EditorTouchEventHandler = editor.eventHandler
+ private val receipts: MutableList> = mutableListOf()
+ private val log = Logger.newInstance(javaClass.simpleName)
+ private val list: ListView = ListView(editor.context)
+ private var mLastScroll: Long = 0
+ private var mLastPosition: Int = 0
+
+ private val menu: MenuBuilder = MenuBuilder(editor.context)
+ protected open var location = ActionItem.Location.EDITOR_TEXT_ACTIONS
+
+ open fun init() {
+ subscribe()
+ applyBackground()
+
+ list.divider = null
+ list.layoutParams =
+ ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+
+ popup.contentView = this.list
+ popup.animationStyle = R.style.PopupAnimation
+
+ val menu = getMenu()
+ if (menu is MenuBuilder) {
+ menu.setCallback(this)
+ }
+ }
+
+ open fun subscribe() {
+ receipts.add(
+ editor.subscribeEvent(SelectionChangeEvent::class.java) { event, _ ->
+ this.onSelectionChanged(event)
+ })
+ receipts.add(
+ editor.subscribeEvent(ScrollEvent::class.java) { _, _ -> this.onScrollEvent() })
+ receipts.add(
+ editor.subscribeEvent(HandleStateChangeEvent::class.java) { event, _ ->
+ this.onHandleStateChanged(event)
+ })
+ }
+
+ open fun unsubscribeEvents() {
+ for (receipt in receipts) {
+ receipt.unsubscribe()
+ }
+ }
+
+ protected open fun onSelectionChanged(event: SelectionChangeEvent) {
+ if (touchHandler.hasAnyHeldHandle()) {
+ return
+ }
+ if (event.isSelected) {
+ if (!isShowing) {
+ editor.post(::displayWindow)
+ }
+ mLastPosition = -1
+ } else {
+ var show = false
+ if (event.cause == SelectionChangeEvent.CAUSE_TAP &&
+ event.left.index == mLastPosition &&
+ !isShowing &&
+ !editor.text.isInBatchEdit) {
+ editor.post(::displayWindow)
+ show = true
+ } else {
+ dismiss()
+ }
+ mLastPosition =
+ if (event.cause == SelectionChangeEvent.CAUSE_TAP && !show) {
+ event.left.index
+ } else {
+ -1
+ }
+ }
+ }
+
+ protected open fun onScrollEvent() {
+ val last = mLastScroll
+ mLastScroll = System.currentTimeMillis()
+ if (mLastScroll - last < DELAY) {
+ postDisplay()
+ }
+ }
+
+ protected open fun onHandleStateChanged(
+ event: HandleStateChangeEvent,
+ ) {
+ if (event.isHeld) {
+ postDisplay()
+ }
+ }
+
+ protected open fun applyBackground() {
+ val drawable = GradientDrawable()
+ drawable.shape = GradientDrawable.RECTANGLE
+ drawable.cornerRadius = 8f
+ drawable.color =
+ ColorStateList.valueOf(
+ ContextCompat.getColor(editor.context, R.color.content_background))
+ list.background = drawable
+ }
+
+ private fun postDisplay() {
+ if (!isShowing) {
+ return
+ }
+ dismiss()
+ if (!editor.cursor.isSelected) {
+ return
+ }
+ editor.postDelayed(
+ object : Runnable {
+ override fun run() {
+ if (!touchHandler.hasAnyHeldHandle() &&
+ System.currentTimeMillis() - mLastScroll > DELAY &&
+ touchHandler.scroller.isFinished) {
+ displayWindow()
+ } else {
+ editor.postDelayed(this, DELAY)
+ }
+ }
+ },
+ DELAY)
+ }
+
+ private fun selectTop(rect: RectF): Int {
+ val rowHeight = editor.rowHeight
+ return if (rect.top - rowHeight * 3 / 2f > height) {
+ (rect.top - rowHeight * 3 / 2 - height).toInt()
+ } else {
+ (rect.bottom + rowHeight / 2).toInt()
+ }
+ }
+
+ open fun displayWindow() {
+ var top: Int
+ val cursor = editor.cursor
+ top =
+ if (cursor.isSelected) {
+ val leftRect = editor.leftHandleDescriptor.position
+ val rightRect = editor.rightHandleDescriptor.position
+ val top1 = selectTop(leftRect)
+ val top2 = selectTop(rightRect)
+ min(top1, top2)
+ } else {
+ selectTop(editor.insertHandleDescriptor.position)
+ }
+ top = max(0, min(top, editor.height - height - 5))
+ val handleLeftX = editor.getOffset(editor.cursor.leftLine, editor.cursor.leftColumn)
+ val handleRightX = editor.getOffset(editor.cursor.rightLine, editor.cursor.rightColumn)
+ val panelX = ((handleLeftX + handleRightX) / 2f).toInt()
+ setLocationAbsolutely(panelX, top)
+ show()
+ }
+
+ protected open fun fillMenu() {
+ getMenu().clear()
+
+ val data = onCreateActionData()
+
+ val registry = getInstance()
+ registry.registerActionExecListener(this)
+ onFillMenu(registry, data)
+
+ this.list.adapter = ActionsListAdapter(getMenu(), editor.context)
+ }
+
+ protected open fun onFillMenu(registry: ActionsRegistry, data: ActionData) {
+ registry.fillMenu(data, onGetActionLocation(), getMenu())
+ }
+
+ protected open fun onGetActionLocation() = location
+
+ protected open fun onCreateActionData(): ActionData {
+ val data = ActionData()
+ data.put(Context::class.java, editor.context)
+ data.put(IDEEditor::class.java, this.editor)
+ data.put(
+ CodeEditor::class.java,
+ editor) // For LSP actions, as they cannot access IDEEditor class
+ data.put(File::class.java, editor.file)
+ data.put(DiagnosticItem::class.java, getDiagnosticAtCursor())
+ data.put(Range::class.java, editor.cursorRange)
+ data.put(
+ JavaLanguageServer::class.java,
+ StudioApp.getInstance().javaLanguageServer as JavaLanguageServer)
+ data.put(
+ XMLLanguageServer::class.java,
+ StudioApp.getInstance().xmlLanguageServer as XMLLanguageServer)
+ return data
+ }
+
+ protected open fun getMenu(): Menu = menu
+
+ private fun getDiagnosticAtCursor(): DiagnosticItem? {
+ val language = editor.editorLanguage as? IDELanguage ?: return null
+ val diagnostics = language.diagnostics
+ return DiagnosticUtil.binarySearchDiagnostic(diagnostics, editor.cursorRange.start)
+ }
+
+ override fun onExec(action: ActionItem, result: Any) {
+ if (action !is SelectAllAction) {
+ dismiss()
+ }
+ }
+
+ override fun show() {
+ if (list.parent != null) {
+ (list.parent as ViewGroup).removeView(list)
+ }
+
+ fillMenu()
+
+ val dp8 = SizeUtils.dp2px(8f)
+ val dp16 = dp8 * 2
+ this.list.measure(
+ View.MeasureSpec.makeMeasureSpec(editor.width - dp16 * 2, View.MeasureSpec.AT_MOST),
+ View.MeasureSpec.makeMeasureSpec(
+ (260 * editor.dpUnit).toInt() - dp16 * 2, View.MeasureSpec.AT_MOST))
+ setSize(findWidestItem(), this.list.measuredHeight)
+ super.show()
+ }
+
+ private fun findWidestItem(): Int {
+ var widest = 0
+ val text =
+ LayoutInflater.from(editor.context).inflate(R.layout.layout_popup_menu_item, null) as
+ MaterialTextView
+ val dp30 = SizeUtils.dp2px(30f)
+ val paddingHorizontal = text.paddingStart + text.paddingEnd
+ val drawablePadding = text.compoundDrawablePadding
+ val extraWidth = dp30 * 2 // 30dp for start and end drawables both
+
+ for (i in 0 until getMenu().size()) {
+ val item = getMenu().getItem(i)
+ val title = item.title.toString()
+ var width = paddingHorizontal + (drawablePadding * 2)
+ width += text.paint.measureText(title).toInt()
+ width += extraWidth
+
+ if (width > widest) {
+ widest = width
+ }
+ }
+
+ return widest
+ }
+
+ private class ActionsListAdapter(val menu: Menu, val context: Context) : BaseAdapter() {
+
+ override fun getCount(): Int = menu.size()
+
+ override fun getItem(position: Int): MenuItem = menu.getItem(position)
+
+ override fun getItemId(position: Int): Long = position.toLong()
+
+ override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
+ val view: MaterialTextView =
+ if (convertView == null || convertView !is MaterialButton) {
+ (context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater)
+ .inflate(R.layout.layout_popup_menu_item, parent, false) as
+ MaterialTextView
+ } else {
+ convertView as MaterialTextView
+ }
+
+ val item = getItem(position)
+ view.text = item.title
+
+ val start = item.icon ?: null
+ var end: Drawable? = null
+ if (item.hasSubMenu()) {
+ end = ContextCompat.getDrawable(context, R.drawable.ic_submenu_arrow)
+ }
+
+ view.setCompoundDrawablesRelativeWithIntrinsicBounds(start, null, end, null)
+ view.setOnClickListener { (item as MenuItemImpl).invoke() }
+
+ return view
+ }
+ }
+
+ override fun onMenuItemSelected(menu: MenuBuilder, item: MenuItem): Boolean {
+ // Click event of MenuItems without SubMenu is consumed by the ActionsRegistry
+ // So we only need to handle click event of SubMenus
+ if (!item.hasSubMenu()) {
+ return false
+ }
+
+ if (item.hasSubMenu() && item.subMenu is SubMenuBuilder) {
+ (item.subMenu as SubMenuBuilder).setCallback(this)
+ }
+
+ this.editor.post {
+ TransitionManager.beginDelayedTransition(this.list, ChangeBounds())
+ this.list.adapter = ActionsListAdapter(item.subMenu, editor.context)
+ }
+
+ return true
+ }
+
+ override fun onMenuModeChange(menu: MenuBuilder) {}
+}
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 bc8e781c2..39086158b 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
@@ -21,7 +21,6 @@ import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.util.AttributeSet;
-import android.view.Gravity;
import android.view.inputmethod.EditorInfo;
import androidx.annotation.NonNull;
@@ -76,7 +75,7 @@ public class IDEEditor extends CodeEditor {
public static final String KEY_FILE = "editor_file";
private static final Logger LOG = Logger.newInstance("IDEEditor");
- private final ActionsPopupMenu mActionsPopup;
+ private final EditorActionsMenu mActionsPopup;
private int mFileVersion;
private File file;
private ILanguageServer mLanguageServer;
@@ -99,7 +98,8 @@ public class IDEEditor extends CodeEditor {
public IDEEditor(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
- mActionsPopup = new ActionsPopupMenu(context, this, Gravity.BOTTOM);
+ mActionsPopup = new EditorActionsMenu(this);
+ mActionsPopup.init ();
setColorScheme(new SchemeAndroidIDE());
getComponent(EditorTextActionWindow.class).setEnabled(false);
@@ -813,7 +813,7 @@ public class IDEEditor extends CodeEditor {
if (mActionsPopup != null) {
if (mActionsPopup.isShowing()) {
- mActionsPopup.dismissPopup();
+ mActionsPopup.dismiss();
}
}
}
diff --git a/app/src/main/res/drawable/ic_submenu_arrow.xml b/app/src/main/res/drawable/ic_submenu_arrow.xml
new file mode 100644
index 000000000..751f871e8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_submenu_arrow.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_popup_menu_item.xml b/app/src/main/res/layout/layout_popup_menu_item.xml
new file mode 100644
index 000000000..586bc55b6
--- /dev/null
+++ b/app/src/main/res/layout/layout_popup_menu_item.xml
@@ -0,0 +1,34 @@
+
+
\ No newline at end of file
diff --git a/lsp/api/src/main/java/com/itsaky/lsp/actions/CodeActionsMenu.kt b/lsp/api/src/main/java/com/itsaky/lsp/actions/CodeActionsMenu.kt
index 806948349..180c1e09d 100644
--- a/lsp/api/src/main/java/com/itsaky/lsp/actions/CodeActionsMenu.kt
+++ b/lsp/api/src/main/java/com/itsaky/lsp/actions/CodeActionsMenu.kt
@@ -17,7 +17,9 @@
package com.itsaky.lsp.actions
+import android.content.Context
import android.graphics.drawable.Drawable
+import androidx.core.content.ContextCompat
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.ActionMenu
@@ -36,10 +38,24 @@ open class CodeActionsMenu : ActionMenu {
override var visible = true
override var enabled: Boolean = true
override var icon: Drawable? = null
+
+ private fun tryGetIcon(context: Context): Drawable? {
+ return try {
+ val klass = Class.forName("com.itsaky.androidide.R\$drawable")
+ val field = klass.getDeclaredField("ic_code")
+ ContextCompat.getDrawable(context, field.get(null) as Int)
+ } catch (error: Throwable) {
+ null
+ }
+ }
+
override var requiresUIThread: Boolean = false
override var location: ActionItem.Location = ActionItem.Location.EDITOR_TEXT_ACTIONS
override fun prepare(data: ActionData) {
+ if (icon == null) {
+ icon = tryGetIcon(data[Context::class.java]!!)
+ }
visible = children.size > 0 && atLeastOneChildVisible(data)
enabled = true
}