From 4fe040eb65c3f6fa7a4a1788901a4a1567516f02 Mon Sep 17 00:00:00 2001 From: John Trujillo Date: Fri, 12 Dec 2025 15:41:49 -0500 Subject: [PATCH 1/4] feat: enable text selection in logs and move help to context menu Updates log viewer to support native text selection and adds tooltip action to the selection toolbar --- .../actions/file/ShowTooltipAction.kt | 14 ++++++++++- .../fragments/EmptyStateFragment.kt | 23 ++++++++++++++++--- .../fragments/output/BuildOutputFragment.kt | 6 ++--- .../itsaky/androidide/editor/ui/IDEEditor.kt | 15 ++++++++++++ 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/itsaky/androidide/actions/file/ShowTooltipAction.kt b/app/src/main/java/com/itsaky/androidide/actions/file/ShowTooltipAction.kt index 3ba994a607..62ed053ddc 100644 --- a/app/src/main/java/com/itsaky/androidide/actions/file/ShowTooltipAction.kt +++ b/app/src/main/java/com/itsaky/androidide/actions/file/ShowTooltipAction.kt @@ -30,7 +30,7 @@ import com.itsaky.androidide.idetooltips.TooltipTag class ShowTooltipAction(private val context: Context, override val order: Int) : EditorRelatedAction() { - override val id: String = "ide.editor.code.text.format" + override val id: String = "ide.editor.code.text.show_tooltip" override var location: ActionItem.Location = ActionItem.Location.EDITOR_TEXT_ACTIONS private var htmlString: String = "" @@ -39,6 +39,18 @@ class ShowTooltipAction(private val context: Context, override val order: Int) : icon = ContextCompat.getDrawable(context, R.drawable.ic_action_help) } + override fun prepare(data: ActionData) { + super.prepare(data) + val editor = data.getEditor() + if (editor == null) { + visible = false + enabled = false + return + } + visible = editor.cursor.isSelected + enabled = visible + } + override suspend fun execAction(data: ActionData): Any { val editor = data.getEditor()!! val cursor = editor.text.cursor diff --git a/app/src/main/java/com/itsaky/androidide/fragments/EmptyStateFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/EmptyStateFragment.kt index f00ccd6ad2..b900f163bd 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/EmptyStateFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/EmptyStateFragment.kt @@ -1,6 +1,7 @@ package com.itsaky.androidide.fragments import android.annotation.SuppressLint +import android.graphics.Color import android.os.Bundle import android.view.GestureDetector import android.view.LayoutInflater @@ -13,6 +14,7 @@ import androidx.lifecycle.repeatOnLifecycle import androidx.viewbinding.ViewBinding import com.itsaky.androidide.databinding.FragmentEmptyStateBinding import com.itsaky.androidide.editor.ui.EditorLongPressEvent +import com.itsaky.androidide.editor.ui.IDEEditor import com.itsaky.androidide.idetooltips.TooltipManager import com.itsaky.androidide.utils.viewLifecycleScope import com.itsaky.androidide.viewmodel.EmptyStateFragmentViewModel @@ -40,16 +42,30 @@ abstract class EmptyStateFragment : FragmentWithBinding { @Volatile private var cachedIsEmpty: Boolean = true + open val currentEditor: IDEEditor? get() = null + /** * Called when a long press is detected on the fragment's root view. * Subclasses must implement this to define the action (e.g., show a tooltip). */ - protected abstract fun onFragmentLongPressed() + open fun onFragmentLongPressed(x: Float = -1f, y: Float = -1f) { + currentEditor?.let { editor -> + if (x >= 0 && y >= 0) { + editor.setSelectionFromPoint(x, y) + } + } + onFragmentLongPressed() + } + + open fun onFragmentLongPressed() { + val currentEditor = currentEditor ?: return + currentEditor.selectCurrentWord() + } private val gestureListener = object : GestureDetector.SimpleOnGestureListener() { override fun onLongPress(e: MotionEvent) { - onFragmentLongPressed() + onFragmentLongPressed(e.x, e.y) } } @@ -160,6 +176,7 @@ abstract class EmptyStateFragment : FragmentWithBinding { // This method will be called when an EditorLongPressEvent is posted @Subscribe(threadMode = ThreadMode.MAIN) fun onEditorLongPressed(event: EditorLongPressEvent) { - onFragmentLongPressed() + val motionEvent = event.motionEvent + onFragmentLongPressed(motionEvent.x, motionEvent.y) } } diff --git a/app/src/main/java/com/itsaky/androidide/fragments/output/BuildOutputFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/output/BuildOutputFragment.kt index b440808f55..6dba1355f4 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/output/BuildOutputFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/output/BuildOutputFragment.kt @@ -20,12 +20,10 @@ import android.os.Bundle import android.view.View import com.blankj.utilcode.util.ThreadUtils import com.itsaky.androidide.R -import com.itsaky.androidide.idetooltips.TooltipTag +import com.itsaky.androidide.editor.ui.IDEEditor class BuildOutputFragment : NonEditableEditorFragment() { - override fun onFragmentLongPressed() { - showTooltipDialog(TooltipTag.PROJECT_BUILD_OUTPUT) - } + override val currentEditor: IDEEditor? get() = editor private val unsavedLines: MutableList = ArrayList() diff --git a/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt b/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt index 6429ffbcc9..f127c39856 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt @@ -1131,4 +1131,19 @@ constructor( ) { editorFeatures.setSelectionAround(line, column) } + + fun setSelectionFromPoint(x: Float, y: Float) { + if (isReleased) return + + try { + val packedPos = getPointPosition(x, y) + + val line = (packedPos ushr 32).toInt() + val column = (packedPos and 0xffffffffL).toInt() + + setSelection(line, column) + } catch (e: Exception) { + log.error("Error setting selection from point", e) + } + } } From 01540fc4d80c3c435858788c2ef8aba845acf8a2 Mon Sep 17 00:00:00 2001 From: John Trujillo Date: Fri, 12 Dec 2025 16:11:36 -0500 Subject: [PATCH 2/4] refactor: Add validation for coordinates and prevent a possible NPE and a double long-press --- .../com/itsaky/androidide/actions/file/ShowTooltipAction.kt | 2 +- .../java/com/itsaky/androidide/fragments/EmptyStateFragment.kt | 1 + .../src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/itsaky/androidide/actions/file/ShowTooltipAction.kt b/app/src/main/java/com/itsaky/androidide/actions/file/ShowTooltipAction.kt index 62ed053ddc..67e5a4eabc 100644 --- a/app/src/main/java/com/itsaky/androidide/actions/file/ShowTooltipAction.kt +++ b/app/src/main/java/com/itsaky/androidide/actions/file/ShowTooltipAction.kt @@ -47,7 +47,7 @@ class ShowTooltipAction(private val context: Context, override val order: Int) : enabled = false return } - visible = editor.cursor.isSelected + visible = editor.cursor?.isSelected == true enabled = visible } diff --git a/app/src/main/java/com/itsaky/androidide/fragments/EmptyStateFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/EmptyStateFragment.kt index b900f163bd..8e1255b9b1 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/EmptyStateFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/EmptyStateFragment.kt @@ -65,6 +65,7 @@ abstract class EmptyStateFragment : FragmentWithBinding { private val gestureListener = object : GestureDetector.SimpleOnGestureListener() { override fun onLongPress(e: MotionEvent) { + if (currentEditor?.isReadOnlyContext == true) return onFragmentLongPressed(e.x, e.y) } } diff --git a/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt b/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt index f127c39856..5b8742f22f 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt @@ -1140,6 +1140,7 @@ constructor( val line = (packedPos ushr 32).toInt() val column = (packedPos and 0xffffffffL).toInt() + if (line < 0 || column < 0) return setSelection(line, column) } catch (e: Exception) { From 1f9c787fc37ee14b2a6ae0e0559a799b5eb21aef Mon Sep 17 00:00:00 2001 From: John Trujillo Date: Fri, 12 Dec 2025 17:42:51 -0500 Subject: [PATCH 3/4] feat: Attach the corresponding tag for each log tab --- .../itsaky/androidide/actions/file/ShowTooltipAction.kt | 9 +++++---- .../androidide/fragments/output/BuildOutputFragment.kt | 2 ++ .../androidide/fragments/output/LogViewFragment.kt | 8 ++++---- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/itsaky/androidide/actions/file/ShowTooltipAction.kt b/app/src/main/java/com/itsaky/androidide/actions/file/ShowTooltipAction.kt index 67e5a4eabc..340a34fd05 100644 --- a/app/src/main/java/com/itsaky/androidide/actions/file/ShowTooltipAction.kt +++ b/app/src/main/java/com/itsaky/androidide/actions/file/ShowTooltipAction.kt @@ -62,10 +62,11 @@ class ShowTooltipAction(private val context: Context, override val order: Int) : } val word = editor.text.substring(cursor.left, cursor.right) if (cursor.isSelected) { - val tag = if (category == TooltipCategory.CATEGORY_XML && editor.isXmlAttribute()) { - word.substringAfterLast(":") - } else { - word + val useEditorTag = editor.tag != null + val tag = when { + useEditorTag -> editor.tag.toString() + category == TooltipCategory.CATEGORY_XML && editor.isXmlAttribute() -> word.substringAfterLast(":") + else -> word } TooltipManager.showTooltip( context = context, diff --git a/app/src/main/java/com/itsaky/androidide/fragments/output/BuildOutputFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/output/BuildOutputFragment.kt index 6dba1355f4..cd67c67f04 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/output/BuildOutputFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/output/BuildOutputFragment.kt @@ -21,6 +21,7 @@ import android.view.View import com.blankj.utilcode.util.ThreadUtils import com.itsaky.androidide.R import com.itsaky.androidide.editor.ui.IDEEditor +import com.itsaky.androidide.idetooltips.TooltipTag class BuildOutputFragment : NonEditableEditorFragment() { override val currentEditor: IDEEditor? get() = editor @@ -33,6 +34,7 @@ class BuildOutputFragment : NonEditableEditorFragment() { ) { super.onViewCreated(view, savedInstanceState) editor?.includeDebugInfoOnCopy = true + editor?.tag = TooltipTag.PROJECT_BUILD_OUTPUT emptyStateViewModel.setEmptyMessage(getString(R.string.msg_emptyview_buildoutput)) if (unsavedLines.isNotEmpty()) { for (line in unsavedLines) { diff --git a/app/src/main/java/com/itsaky/androidide/fragments/output/LogViewFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/output/LogViewFragment.kt index 0aa3091fd9..403d4c2475 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/output/LogViewFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/output/LogViewFragment.kt @@ -28,6 +28,7 @@ import com.itsaky.androidide.editor.language.treesitter.LogLanguage import com.itsaky.androidide.editor.language.treesitter.TreeSitterLanguageProvider import com.itsaky.androidide.editor.schemes.IDEColorScheme import com.itsaky.androidide.editor.schemes.IDEColorSchemeProvider +import com.itsaky.androidide.editor.ui.IDEEditor import com.itsaky.androidide.fragments.EmptyStateFragment import com.itsaky.androidide.fragments.output.LogViewFragment.Companion.LOG_FREQUENCY import com.itsaky.androidide.fragments.output.LogViewFragment.Companion.MAX_CHUNK_SIZE @@ -126,6 +127,8 @@ abstract class LogViewFragment : } } + override val currentEditor: IDEEditor? get() = _binding?.editor + fun appendLog(line: LogLine) { val lineString = if (isSimpleFormattingEnabled()) { @@ -222,6 +225,7 @@ abstract class LogViewFragment : editor.typefaceText = jetbrainsMono() editor.isEnsurePosAnimEnabled = false editor.includeDebugInfoOnCopy = true + editor.tag = tooltipTag editor.cursorAnimator = object : CursorAnimator { override fun markStartPos() {} @@ -280,8 +284,4 @@ abstract class LogViewFragment : emptyStateViewModel.setEmpty(true) } } - - override fun onFragmentLongPressed() { - showTooltipDialog(tooltipTag) - } } From 0f7f905e115b179adc08a9c7653ec1b174327f5e Mon Sep 17 00:00:00 2001 From: John Trujillo Date: Fri, 12 Dec 2025 17:44:12 -0500 Subject: [PATCH 4/4] fix: Remove unused import --- .../java/com/itsaky/androidide/fragments/EmptyStateFragment.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/itsaky/androidide/fragments/EmptyStateFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/EmptyStateFragment.kt index 8e1255b9b1..b99add1f02 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/EmptyStateFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/EmptyStateFragment.kt @@ -1,7 +1,6 @@ package com.itsaky.androidide.fragments import android.annotation.SuppressLint -import android.graphics.Color import android.os.Bundle import android.view.GestureDetector import android.view.LayoutInflater