mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
Implemented actions menu with editor's APIs
This commit is contained in:
Generated
+2
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user