From aab884258f64b0f0889703316a6b429a09df7493 Mon Sep 17 00:00:00 2001 From: Oluwadara Abijo Date: Thu, 13 Aug 2026 11:59:39 +0100 Subject: [PATCH 1/3] fix(ADFA-4852): Fix dialog dismissal on long-press --- .../actions/filetree/NewFileAction.kt | 5 - .../actions/filetree/RenameAction.kt | 60 ++++---- .../editor/ProjectHandlerActivity.kt | 2 +- .../androidide/utils/DialogExtensions.kt | 97 ++++++------ .../itsaky/androidide/utils/ViewExtensions.kt | 143 ++++++++++-------- 5 files changed, 162 insertions(+), 145 deletions(-) diff --git a/app/src/main/java/com/itsaky/androidide/actions/filetree/NewFileAction.kt b/app/src/main/java/com/itsaky/androidide/actions/filetree/NewFileAction.kt index 7beba57b3f..bd5c19e234 100644 --- a/app/src/main/java/com/itsaky/androidide/actions/filetree/NewFileAction.kt +++ b/app/src/main/java/com/itsaky/androidide/actions/filetree/NewFileAction.kt @@ -24,7 +24,6 @@ import com.itsaky.androidide.actions.ActionData import com.itsaky.androidide.actions.FileActionManager import com.itsaky.androidide.actions.observers.FileActionObserver import com.itsaky.androidide.actions.requireFile -import com.itsaky.androidide.adapters.viewholders.FileTreeViewHolder import com.itsaky.androidide.databinding.LayoutCreateFileJavaBinding import com.itsaky.androidide.eventbus.events.file.FileCreationEvent import com.itsaky.androidide.idetooltips.TooltipTag @@ -208,10 +207,6 @@ class NewFileAction( .showWithLongPressTooltip( context = context, tooltipTag = TooltipTag.PROJECT_FOLDER_NEWTYPE, - binding.typeClass, - binding.typeActivity, - binding.typeInterface, - binding.typeEnum, ) } diff --git a/app/src/main/java/com/itsaky/androidide/actions/filetree/RenameAction.kt b/app/src/main/java/com/itsaky/androidide/actions/filetree/RenameAction.kt index bf765963e0..71357eb3a5 100644 --- a/app/src/main/java/com/itsaky/androidide/actions/filetree/RenameAction.kt +++ b/app/src/main/java/com/itsaky/androidide/actions/filetree/RenameAction.kt @@ -57,39 +57,45 @@ class RenameAction( builder.setTitle(R.string.rename_file) builder.setMessage(R.string.msg_rename_file) builder.setView(binding.root) + builder.setCancelable(false) builder.setNegativeButton(android.R.string.cancel, null) builder.setPositiveButton(R.string.rename_file) { dialogInterface, _ -> - val fileManagerViewModel: FileManagerViewModel by context.viewModels() - val name: String = binding.name.editText?.text.toString().trim() - when { - name.isEmpty() -> { - flashError(R.string.msg_invalid_name) - return@setPositiveButton - } - name.length > 40 -> { - flashError(R.string.file_name_too_long) - return@setPositiveButton - } - } + val fileManagerViewModel: FileManagerViewModel by context.viewModels() + val name: String = + binding.name.editText + ?.text + .toString() + .trim() + when { + name.isEmpty() -> { + flashError(R.string.msg_invalid_name) + return@setPositiveButton + } - dialogInterface.dismiss() - fileManagerViewModel.renameFile(file, name, context) { renamed -> - if (!renamed) return@renameFile + name.length > 40 -> { + flashError(R.string.file_name_too_long) + return@setPositiveButton + } + } - val parent = lastHeld?.parent + dialogInterface.dismiss() + fileManagerViewModel.renameFile(file, name, context) { renamed -> + if (!renamed) return@renameFile - if (parent != null) { - requestCollapseNode(parent, false) - requestExpandNode(parent) - } else { - requestFileListing() - } - } + val parent = lastHeld?.parent + + if (parent != null) { + requestCollapseNode(parent, false) + requestExpandNode(parent) + } else { + requestFileListing() + } + } } - builder.showWithLongPressTooltip( - context = context, - tooltipTag = TooltipTag.PROJECT_RENAME_DIALOG - ) + builder.showWithLongPressTooltip( + context = context, + tooltipTag = TooltipTag.PROJECT_RENAME_DIALOG, + ) } } diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt index e8e0494c11..b63a3e6540 100644 --- a/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt +++ b/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt @@ -927,7 +927,7 @@ abstract class ProjectHandlerActivity : BaseEditorActivity() { builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.dismiss() } val dialog = builder.create() - dialog.onLongPress { view -> + dialog.onLongPress(includeEditTexts = true) { view -> if ( view is EditText ) { diff --git a/app/src/main/java/com/itsaky/androidide/utils/DialogExtensions.kt b/app/src/main/java/com/itsaky/androidide/utils/DialogExtensions.kt index 4a6734c185..ad0987f9ca 100644 --- a/app/src/main/java/com/itsaky/androidide/utils/DialogExtensions.kt +++ b/app/src/main/java/com/itsaky/androidide/utils/DialogExtensions.kt @@ -5,7 +5,6 @@ import android.app.Activity import android.content.Context import android.graphics.Rect import android.view.MotionEvent -import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager import android.widget.AdapterView @@ -16,60 +15,58 @@ import com.itsaky.androidide.idetooltips.TooltipManager @SuppressLint("ClickableViewAccessibility") fun MaterialAlertDialogBuilder.showWithLongPressTooltip( - context: Context, - tooltipTag: String, - vararg customViews: View + context: Context, + tooltipTag: String, ): AlertDialog { - val dialog = this.create() + val dialog = this.create() - fun longPressAction() { - dialog.dismiss() - val anchor = (context as? Activity)?.window?.decorView ?: return - TooltipManager.showIdeCategoryTooltip( - context = context, - anchorView = anchor, - tag = tooltipTag, - ) - } + fun longPressAction() { + val anchor = (context as? Activity)?.window?.decorView ?: return + TooltipManager.showIdeCategoryTooltip( + context = context, + anchorView = anchor, + tag = tooltipTag, + ) + } - dialog.onLongPress { - longPressAction() - true - } + dialog.onLongPress { + longPressAction() + true + } - dialog.listView?.onItemLongClickListener = - AdapterView.OnItemLongClickListener { _, _, _, _ -> - longPressAction() - true - } + dialog.listView?.onItemLongClickListener = + AdapterView.OnItemLongClickListener { _, _, _, _ -> + longPressAction() + true + } - val customPanel: ViewGroup? = dialog.findViewById(androidx.appcompat.R.id.customPanel) + val customPanel: ViewGroup? = dialog.findViewById(androidx.appcompat.R.id.customPanel) - customPanel?.forEachViewRecursively { view -> - if (view is EditText) { - dialog.setOnShowListener { - view.requestFocus() - val imm = - context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT) - } + customPanel?.forEachViewRecursively { view -> + if (view is EditText) { + dialog.setOnShowListener { + view.requestFocus() + val imm = + context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT) + } - dialog.window?.decorView?.setOnTouchListener { v, event -> - if (event.action == MotionEvent.ACTION_DOWN) { - val outRect = Rect() - view.getGlobalVisibleRect(outRect) - if (!outRect.contains(event.rawX.toInt(), event.rawY.toInt())) { - view.clearFocus() - val imm = - view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.hideSoftInputFromWindow(view.windowToken, 0) - } - } - false - } - } - } + dialog.window?.decorView?.setOnTouchListener { v, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + val outRect = Rect() + view.getGlobalVisibleRect(outRect) + if (!outRect.contains(event.rawX.toInt(), event.rawY.toInt())) { + view.clearFocus() + val imm = + view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(view.windowToken, 0) + } + } + false + } + } + } - dialog.show() - return dialog -} \ No newline at end of file + dialog.show() + return dialog +} diff --git a/common/src/main/java/com/itsaky/androidide/utils/ViewExtensions.kt b/common/src/main/java/com/itsaky/androidide/utils/ViewExtensions.kt index 7671bec043..09bf92d883 100644 --- a/common/src/main/java/com/itsaky/androidide/utils/ViewExtensions.kt +++ b/common/src/main/java/com/itsaky/androidide/utils/ViewExtensions.kt @@ -9,6 +9,7 @@ import android.view.MotionEvent import android.view.View import android.view.ViewConfiguration import android.view.ViewGroup +import android.widget.EditText import android.widget.ListView import androidx.appcompat.app.AlertDialog import androidx.core.view.forEach @@ -30,75 +31,84 @@ fun View.forEachViewRecursively(action: (View) -> Unit) { } fun View.applyLongPressRecursively( - exclude: List = emptyList(), - listener: (View) -> Boolean + exclude: List = emptyList(), + includeEditTexts: Boolean = false, + listener: (View) -> Boolean, ) { - if (this is ListView || this in exclude) return + if (this is ListView || (this is EditText && !includeEditTexts) || this in exclude) return - setOnLongClickListener { listener(it) } + setOnLongClickListener { listener(it) } - if (this is ViewGroup) { - forEach { it.applyLongPressRecursively(exclude, listener) } - } + if (this is ViewGroup) { + forEach { it.applyLongPressRecursively(exclude, includeEditTexts, listener) } + } } fun RecyclerView.onLongPress(listener: (MotionEvent) -> Unit) { - val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() { - override fun onLongPress(e: MotionEvent) { - listener(e) - } - }) - - addOnItemTouchListener(object : RecyclerView.SimpleOnItemTouchListener() { - override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean { - gestureDetector.onTouchEvent(e) - return false - } - }) + val gestureDetector = + GestureDetector( + context, + object : GestureDetector.SimpleOnGestureListener() { + override fun onLongPress(e: MotionEvent) { + listener(e) + } + }, + ) + + addOnItemTouchListener( + object : RecyclerView.SimpleOnItemTouchListener() { + override fun onInterceptTouchEvent( + rv: RecyclerView, + e: MotionEvent, + ): Boolean { + gestureDetector.onTouchEvent(e) + return false + } + }, + ) } - @SuppressLint("ClickableViewAccessibility") fun View.setupGestureHandling( - onLongPress: (View) -> Unit, - onDrag: (View) -> Unit + onLongPress: (View) -> Unit, + onDrag: (View) -> Unit, ) { - val handler = Handler(Looper.getMainLooper()) - var isTooltipStarted = false - var startTime = 0L - - setOnTouchListener { view, event -> - when (event.action) { - MotionEvent.ACTION_DOWN -> { - isTooltipStarted = false - startTime = System.currentTimeMillis() - - // Trigger long press after 800ms - handler.postDelayed({ - if (!isTooltipStarted) { - isTooltipStarted = true - view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) - onLongPress(view) - } - }, LONG_PRESS_TIMEOUT_MS) - } - - MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { - handler.removeCallbacksAndMessages(null) - - if (!isTooltipStarted) { - val holdDuration = System.currentTimeMillis() - startTime - if (holdDuration >= HOLD_DURATION_MS) { - // Medium hold for drag (600-800ms) - onDrag(view) - } else { - view.performClick() - } - } - } - } - true - } + val handler = Handler(Looper.getMainLooper()) + var isTooltipStarted = false + var startTime = 0L + + setOnTouchListener { view, event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> { + isTooltipStarted = false + startTime = System.currentTimeMillis() + + // Trigger long press after 800ms + handler.postDelayed({ + if (!isTooltipStarted) { + isTooltipStarted = true + view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + onLongPress(view) + } + }, LONG_PRESS_TIMEOUT_MS) + } + + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + handler.removeCallbacksAndMessages(null) + + if (!isTooltipStarted) { + val holdDuration = System.currentTimeMillis() - startTime + if (holdDuration >= HOLD_DURATION_MS) { + // Medium hold for drag (600-800ms) + onDrag(view) + } else { + view.performClick() + } + } + } + } + true + } } /** @@ -108,16 +118,22 @@ fun View.setupGestureHandling( * is long-pressed. It works by recursively attaching a long-press listener to the * dialog's decor view and all its children. * + * @param includeEditTexts Whether the listener is also attached to text fields. Off by + * default so the platform select/paste gesture keeps working; enable it + * only when the listener implements its own text actions. * @param listener A lambda function that will be invoked when a long-press event occurs. * The lambda receives the [View] that was long-pressed as its argument * and should return `true` if the listener has consumed the event, `false` otherwise. */ -fun AlertDialog.onLongPress(listener: (View) -> Boolean) { +fun AlertDialog.onLongPress( + includeEditTexts: Boolean = false, + listener: (View) -> Boolean, +) { if (this.isShowing) { - this.window?.decorView?.applyLongPressRecursively(emptyList(), listener) + this.window?.decorView?.applyLongPressRecursively(emptyList(), includeEditTexts, listener) } else { this.setOnShowListener { - this.window?.decorView?.applyLongPressRecursively(emptyList(), listener) + this.window?.decorView?.applyLongPressRecursively(emptyList(), includeEditTexts, listener) } } } @@ -208,7 +224,10 @@ fun View.handleLongClicksAndDrag( longPressFired = false return@setOnTouchListener true } - else -> false + + else -> { + false + } } } } From bd3a3cf88da8e26d41aa49abd3f1b1b302b52c66 Mon Sep 17 00:00:00 2001 From: Oluwadara Abijo Date: Thu, 13 Aug 2026 13:18:57 +0100 Subject: [PATCH 2/3] docs(ADFA-4852): Add KDoc to function --- .../com/itsaky/androidide/utils/ViewExtensions.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/common/src/main/java/com/itsaky/androidide/utils/ViewExtensions.kt b/common/src/main/java/com/itsaky/androidide/utils/ViewExtensions.kt index 09bf92d883..4186c618e1 100644 --- a/common/src/main/java/com/itsaky/androidide/utils/ViewExtensions.kt +++ b/common/src/main/java/com/itsaky/androidide/utils/ViewExtensions.kt @@ -30,6 +30,21 @@ fun View.forEachViewRecursively(action: (View) -> Unit) { } } +/** + * Attaches a long-press listener to this view and, recursively, to every view in its + * subtree. [ListView]s and views in [exclude] are skipped along with their subtrees. + * + * Text fields are skipped by default: a long press inside an [EditText] is the platform + * text-editing gesture (select/paste), and hijacking it would e.g. dismiss a dialog when + * the user long-presses its input field to paste + * + * @param exclude Views whose subtrees are left untouched. + * @param includeEditTexts Whether to also attach the listener to [EditText]s. Enable only + * when the listener implements its own text actions, like find-in-project's + * SearchFieldToolbar. + * @param listener Invoked with the long-pressed view; returns `true` if it consumed the + * event, `false` to let the view's default long-press behavior run. + */ fun View.applyLongPressRecursively( exclude: List = emptyList(), includeEditTexts: Boolean = false, From d933e5a7453979aba1c06d4335b6403a1f631aba Mon Sep 17 00:00:00 2001 From: Oluwadara Abijo Date: Thu, 13 Aug 2026 13:19:29 +0100 Subject: [PATCH 3/3] refactor(ADFA-4852): Show dialog before configuring its views --- .../main/java/com/itsaky/androidide/utils/DialogExtensions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/itsaky/androidide/utils/DialogExtensions.kt b/app/src/main/java/com/itsaky/androidide/utils/DialogExtensions.kt index ad0987f9ca..ede0a296bb 100644 --- a/app/src/main/java/com/itsaky/androidide/utils/DialogExtensions.kt +++ b/app/src/main/java/com/itsaky/androidide/utils/DialogExtensions.kt @@ -19,6 +19,7 @@ fun MaterialAlertDialogBuilder.showWithLongPressTooltip( tooltipTag: String, ): AlertDialog { val dialog = this.create() + dialog.show() fun longPressAction() { val anchor = (context as? Activity)?.window?.decorView ?: return @@ -67,6 +68,5 @@ fun MaterialAlertDialogBuilder.showWithLongPressTooltip( } } - dialog.show() return dialog }