From 928b00cac63f4f00a46553377bcbe4e89f97da7d Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Thu, 14 Apr 2022 17:59:53 +0530 Subject: [PATCH] Implemented actions menu with editor's APIs --- .idea/misc.xml | 2 + .../views/editor/ActionsPopupMenu.java | 294 -------------- .../views/editor/EditorActionsMenu.kt | 380 ++++++++++++++++++ .../androidide/views/editor/IDEEditor.java | 8 +- .../main/res/drawable/ic_submenu_arrow.xml | 10 + .../res/layout/layout_popup_menu_item.xml | 34 ++ .../com/itsaky/lsp/actions/CodeActionsMenu.kt | 16 + 7 files changed, 446 insertions(+), 298 deletions(-) delete mode 100644 app/src/main/java/com/itsaky/androidide/views/editor/ActionsPopupMenu.java create mode 100644 app/src/main/java/com/itsaky/androidide/views/editor/EditorActionsMenu.kt create mode 100644 app/src/main/res/drawable/ic_submenu_arrow.xml create mode 100644 app/src/main/res/layout/layout_popup_menu_item.xml 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 }