Implemented actions menu with editor's APIs

This commit is contained in:
Akash Yadav
2022-04-14 17:59:53 +05:30
parent 969604f7ac
commit 928b00cac6
7 changed files with 446 additions and 298 deletions
+2
View File
@@ -40,6 +40,7 @@
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/drawable/ic_run_debug.xml" value="0.151" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/drawable/ic_save.xml" value="0.26205128205128203" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/drawable/ic_signature_help.xml" value="0.19907407407407407" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/drawable/ic_submenu_arrow.xml" value="0.1305" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/drawable/ic_text_actions.xml" value="0.147" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/drawable/ic_text_actions_horizontal.xml" value="0.147" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/drawable/ic_text_lower.xml" value="0.19907407407407407" />
@@ -72,6 +73,7 @@
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/layout_editor_code_actions.xml" value="0.25" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/layout_editor_text_actions.xml" value="0.25" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/layout_filetree_item.xml" value="0.25" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/layout_popup_menu_item.xml" value="0.264" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/layout_reference_attr_editor.xml" value="0.25" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/layout_resource_input_field.xml" value="0.25" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/layout_string_attr_editor.xml" value="0.25" />
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<SubscriptionReceipt<?>> 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());
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<SubscriptionReceipt<*>> = 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) {}
}
@@ -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();
}
}
}
@@ -0,0 +1,10 @@
<!-- drawable/menu_right.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#fff"
android:pathData="M10,17L15,12L10,7V17Z" />
</vector>
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
-->
<com.google.android.material.textview.MaterialTextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@drawable/bg_ripple"
android:clickable="true"
android:drawablePadding="12dp"
android:drawableTint="@color/secondaryColor"
android:focusable="true"
android:gravity="start|center_vertical"
android:paddingStart="16dp"
android:paddingTop="4dp"
android:paddingEnd="8dp"
android:paddingBottom="4dp"
android:textAllCaps="false"
android:textColor="@color/primaryTextColor"
tools:drawableStart="@drawable/ic_code"
tools:text="Code actions" />
@@ -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
}