mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
Horizontal + vertical (expanded) editor actions window
This commit is contained in:
@@ -130,12 +130,13 @@ public class SchemeAndroidIDE extends EditorColorScheme {
|
||||
// Apply customized colors
|
||||
setColor(WHOLE_BACKGROUND, 0xff212121);
|
||||
setColor(LINE_NUMBER_BACKGROUND, 0xff212121);
|
||||
setColor(MATCHED_TEXT_BACKGROUND, 0xffFF8F00);
|
||||
setColor(SELECTED_TEXT_BACKGROUND, 0xff757575);
|
||||
setColor(LINE_DIVIDER, Color.TRANSPARENT);
|
||||
setColor(LINE_NUMBER, 0xffaaaaaa);
|
||||
setColor(LINE_NUMBER_PANEL, 0xff000000);
|
||||
setColor(LINE_NUMBER_PANEL_TEXT, 0xffffffff);
|
||||
setColor(TEXT_NORMAL, 0xfff5f5f5);
|
||||
setColor(SELECTED_TEXT_BACKGROUND, 0xff757575);
|
||||
setColor(TEXT_SELECTED, 0xff424242);
|
||||
setColor(SELECTION_INSERT, 0xffff5252);
|
||||
setColor(SELECTION_HANDLE, 0xffff5252);
|
||||
@@ -148,7 +149,6 @@ public class SchemeAndroidIDE extends EditorColorScheme {
|
||||
setColor(BLOCK_LINE_CURRENT, 0xffffffff);
|
||||
setColor(COMPLETION_WND_BACKGROUND, 0xff757575);
|
||||
setColor(COMPLETION_WND_CORNER, 0xff9e9e9e);
|
||||
setColor(MATCHED_TEXT_BACKGROUND, 0xffFF8F00);
|
||||
setColor(NON_PRINTABLE_CHAR, 0xffdddddd);
|
||||
setColor(KEYWORD, 0xffff6060);
|
||||
setColor(OPERATOR, 0xff4fc3f7);
|
||||
@@ -200,4 +200,9 @@ public class SchemeAndroidIDE extends EditorColorScheme {
|
||||
setColor(FIXME_COMMENT, 0xffffab00);
|
||||
setColor(COMMENT, 0xffbdbdbd);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDark() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,24 +21,23 @@ 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.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.View.MeasureSpec
|
||||
import android.view.ViewGroup
|
||||
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.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
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
|
||||
@@ -46,13 +45,15 @@ 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.utils.ILogger
|
||||
import com.itsaky.androidide.databinding.LayoutPopupMenuItemBinding
|
||||
import com.itsaky.androidide.lsp.java.JavaLanguageServer
|
||||
import com.itsaky.androidide.lsp.models.DiagnosticItem
|
||||
import com.itsaky.androidide.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.text.Cursor
|
||||
import io.github.rosemoe.sora.widget.CodeEditor
|
||||
import io.github.rosemoe.sora.widget.EditorTouchEventHandler
|
||||
import io.github.rosemoe.sora.widget.base.EditorPopupWindow
|
||||
@@ -66,7 +67,7 @@ import kotlin.math.min
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
@SuppressLint("RestrictedApi")
|
||||
open class EditorActionsMenu(val editor: IDEEditor) :
|
||||
open class EditorActionsMenu constructor(val editor: IDEEditor) :
|
||||
EditorPopupWindow(editor, FEATURE_SHOW_OUTSIDE_VIEW_ALLOWED),
|
||||
ActionsRegistry.ActionExecListener,
|
||||
MenuBuilder.Callback {
|
||||
@@ -77,8 +78,7 @@ open class EditorActionsMenu(val editor: IDEEditor) :
|
||||
|
||||
private val touchHandler: EditorTouchEventHandler = editor.eventHandler
|
||||
private val receipts: MutableList<SubscriptionReceipt<*>> = mutableListOf()
|
||||
private val log = ILogger.newInstance(javaClass.simpleName)
|
||||
private val list: ListView = ListView(editor.context)
|
||||
private val list = RecyclerView(editor.context)
|
||||
private var mLastScroll: Long = 0
|
||||
private var mLastPosition: Int = 0
|
||||
|
||||
@@ -89,17 +89,20 @@ open class EditorActionsMenu(val editor: IDEEditor) :
|
||||
subscribe()
|
||||
applyBackground()
|
||||
|
||||
list.clipChildren = true
|
||||
list.clipToOutline = true
|
||||
list.isVerticalFadingEdgeEnabled = true
|
||||
list.isVerticalScrollBarEnabled = true
|
||||
list.setFadingEdgeLength(SizeUtils.dp2px(42f))
|
||||
list.divider = null
|
||||
list.layoutParams =
|
||||
ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
list.apply {
|
||||
clipChildren = true
|
||||
clipToOutline = true
|
||||
isVerticalFadingEdgeEnabled = true
|
||||
isVerticalScrollBarEnabled = true
|
||||
layoutParams =
|
||||
ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
|
||||
setFadingEdgeLength(SizeUtils.dp2px(42f))
|
||||
setPaddingRelative(paddingStart, paddingTop, SizeUtils.dp2px(16f), paddingBottom)
|
||||
}
|
||||
|
||||
popup.contentView = this.list
|
||||
popup.animationStyle = R.style.PopupAnimation
|
||||
@@ -180,7 +183,7 @@ open class EditorActionsMenu(val editor: IDEEditor) :
|
||||
protected open fun applyBackground() {
|
||||
val drawable = GradientDrawable()
|
||||
drawable.shape = GradientDrawable.RECTANGLE
|
||||
drawable.cornerRadius = 16f
|
||||
drawable.cornerRadius = SizeUtils.dp2px(28f).toFloat() // Recommeneded size is 28dp
|
||||
drawable.color =
|
||||
ColorStateList.valueOf(ContextCompat.getColor(editor.context, R.color.content_background))
|
||||
drawable.setStroke(
|
||||
@@ -241,11 +244,25 @@ open class EditorActionsMenu(val editor: IDEEditor) :
|
||||
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()
|
||||
val panelX = computePanelX(cursor, handleLeftX, handleRightX)
|
||||
setLocationAbsolutely(panelX, top)
|
||||
show()
|
||||
}
|
||||
|
||||
private fun computePanelX(cursor: Cursor, handleLeftX: Float, handleRightX: Float): Int {
|
||||
return if (cursor.isSelected) {
|
||||
((handleLeftX + handleRightX) / 2f).toInt()
|
||||
} else {
|
||||
var x = (handleLeftX - (width / 2f)).toInt()
|
||||
if (x <= 0) {
|
||||
x = (handleRightX + SizeUtils.dp2px(10f)).toInt()
|
||||
} else if (x >= editor.width) {
|
||||
x = editor.width - SizeUtils.dp2px(10f)
|
||||
}
|
||||
x
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun fillMenu() {
|
||||
getMenu().clear()
|
||||
|
||||
@@ -255,7 +272,7 @@ open class EditorActionsMenu(val editor: IDEEditor) :
|
||||
registry.registerActionExecListener(this)
|
||||
onFillMenu(registry, data)
|
||||
|
||||
this.list.adapter = ActionsListAdapter(getMenu(), editor.context)
|
||||
this.list.adapter = ActionsListAdapter(getMenu())
|
||||
}
|
||||
|
||||
protected open fun onFillMenu(registry: ActionsRegistry, data: ActionData) {
|
||||
@@ -273,7 +290,7 @@ open class EditorActionsMenu(val editor: IDEEditor) :
|
||||
editor
|
||||
) // For LSP actions, as they cannot access IDEEditor class
|
||||
data.put(File::class.java, editor.file)
|
||||
data.put(com.itsaky.androidide.lsp.models.DiagnosticItem::class.java, getDiagnosticAtCursor())
|
||||
data.put(DiagnosticItem::class.java, getDiagnosticAtCursor())
|
||||
data.put(com.itsaky.androidide.models.Range::class.java, editor.cursorRange)
|
||||
data.put(
|
||||
JavaLanguageServer::class.java,
|
||||
@@ -288,7 +305,7 @@ open class EditorActionsMenu(val editor: IDEEditor) :
|
||||
|
||||
protected open fun getMenu(): Menu = menu
|
||||
|
||||
private fun getDiagnosticAtCursor(): com.itsaky.androidide.lsp.models.DiagnosticItem? {
|
||||
private fun getDiagnosticAtCursor(): DiagnosticItem? {
|
||||
val start = editor.cursorRange.start
|
||||
return editor.languageClient?.getDiagnosticAt(editor.file, start.line, start.column)
|
||||
}
|
||||
@@ -300,29 +317,35 @@ open class EditorActionsMenu(val editor: IDEEditor) :
|
||||
}
|
||||
|
||||
override fun show() {
|
||||
|
||||
|
||||
if (editor.searcher.searching) {
|
||||
// Do not show if user is searching text
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (list.parent != null) {
|
||||
(list.parent as ViewGroup).removeView(list)
|
||||
}
|
||||
|
||||
this.list.layoutManager = LinearLayoutManager(editor.context, RecyclerView.HORIZONTAL, false)
|
||||
|
||||
fillMenu()
|
||||
|
||||
measureActionsList()
|
||||
|
||||
val height = list.measuredHeight
|
||||
val width = min(editor.width - SizeUtils.dp2px(32f), list.measuredWidth)
|
||||
setSize(width, height)
|
||||
super.show()
|
||||
}
|
||||
|
||||
private fun measureActionsList() {
|
||||
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
|
||||
)
|
||||
MeasureSpec.makeMeasureSpec(editor.width - dp16 * 2, MeasureSpec.AT_MOST),
|
||||
MeasureSpec.makeMeasureSpec((260 * editor.dpUnit).toInt() - dp16 * 2, MeasureSpec.AT_MOST)
|
||||
)
|
||||
setSize(findWidestItem(), this.list.measuredHeight)
|
||||
super.show()
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
@@ -330,10 +353,10 @@ open class EditorActionsMenu(val editor: IDEEditor) :
|
||||
var widest = 0
|
||||
val text =
|
||||
LayoutInflater.from(editor.context).inflate(R.layout.layout_popup_menu_item, null)
|
||||
as MaterialTextView
|
||||
as MaterialButton
|
||||
val dp30 = SizeUtils.dp2px(30f)
|
||||
val paddingHorizontal = text.paddingStart + text.paddingEnd
|
||||
val drawablePadding = text.compoundDrawablePadding
|
||||
val drawablePadding = text.iconPadding
|
||||
val extraWidth = dp30 * 2 // 30dp for start and end drawables both
|
||||
|
||||
for (i in 0 until getMenu().size()) {
|
||||
@@ -351,40 +374,35 @@ open class EditorActionsMenu(val editor: IDEEditor) :
|
||||
return widest
|
||||
}
|
||||
|
||||
private class ActionsListAdapter(val menu: Menu, val context: Context) : BaseAdapter() {
|
||||
private class ActionsListAdapter(val menu: Menu, val forceShowTitle: Boolean = false) : RecyclerView.Adapter<ActionsListAdapter.VH>() {
|
||||
|
||||
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 getItemCount(): Int {
|
||||
return menu.size()
|
||||
}
|
||||
|
||||
fun getItem(position: Int): MenuItem = menu.getItem(position)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
|
||||
return VH(
|
||||
LayoutPopupMenuItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: VH, position: Int) {
|
||||
val item = getItem(position)
|
||||
holder.binding.root.text = if (forceShowTitle) item.title else ""
|
||||
holder.binding.root.tooltipText = item.title
|
||||
holder.binding.root.icon = item.icon ?: run {
|
||||
holder.binding.root.text = item.title
|
||||
holder.binding.root.layoutParams.apply {
|
||||
width = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
}
|
||||
null
|
||||
}
|
||||
holder.binding.root.setOnClickListener { (item as MenuItemImpl).invoke() }
|
||||
}
|
||||
|
||||
inner class VH(val binding: LayoutPopupMenuItemBinding) : ViewHolder(binding.root)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menu: MenuBuilder, item: MenuItem): Boolean {
|
||||
@@ -400,7 +418,11 @@ open class EditorActionsMenu(val editor: IDEEditor) :
|
||||
|
||||
this.editor.post {
|
||||
TransitionManager.beginDelayedTransition(this.list, ChangeBounds())
|
||||
this.list.adapter = ActionsListAdapter(item.subMenu, editor.context)
|
||||
this.list.layoutManager = LinearLayoutManager(editor.context)
|
||||
this.list.adapter = ActionsListAdapter(item.subMenu, true)
|
||||
|
||||
measureActionsList()
|
||||
popup.update(findWidestItem(), this.list.measuredHeight)
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
@@ -14,21 +14,24 @@
|
||||
~ 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" />
|
||||
<com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
style="@style/Widget.Material3.Button.TextButton.Icon"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@drawable/bg_ripple"
|
||||
android:clickable="true"
|
||||
android:drawablePadding="16dp"
|
||||
android:drawableTint="@color/secondaryColor"
|
||||
android:focusable="true"
|
||||
android:gravity="start|center_vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:paddingBottom="4dp"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/primaryTextColor"
|
||||
app:iconPadding="0dp"
|
||||
app:iconTint="@color/secondaryColor"
|
||||
tools:icon="@drawable/ic_code" />
|
||||
Binary file not shown.
Reference in New Issue
Block a user