diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt
index e285a1b11d..6c6315cfae 100644
--- a/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt
+++ b/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt
@@ -432,6 +432,17 @@ open class EditorHandlerActivity :
tag = action.retrieveTooltipTag(false),
)
},
+ onHover = { anchor ->
+ TooltipManager.cancelScheduledDismiss()
+ TooltipManager.showIdeCategoryTooltip(
+ context = this@EditorHandlerActivity,
+ anchorView = anchor,
+ tag = action.retrieveTooltipTag(false)
+ )
+ },
+ onHoverExit = {
+ TooltipManager.scheduleActiveTooltipDismiss()
+ },
shouldAddMargin = !isLast,
)
}
diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/LandscapeImmersiveController.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/LandscapeImmersiveController.kt
index 201c88b5ee..1864b4ca5c 100644
--- a/app/src/main/java/com/itsaky/androidide/activities/editor/LandscapeImmersiveController.kt
+++ b/app/src/main/java/com/itsaky/androidide/activities/editor/LandscapeImmersiveController.kt
@@ -2,6 +2,7 @@ package com.itsaky.androidide.activities.editor
import android.annotation.SuppressLint
import android.content.res.Configuration
+import android.view.InputDevice
import android.view.MotionEvent
import android.view.View
import android.view.View.OnLayoutChangeListener
@@ -122,6 +123,22 @@ class LandscapeImmersiveController(
}
}
+ /**
+ * Observes mouse hover events to manage the top bar's auto-hide behavior.
+ * It pauses the auto-hide timer while the cursor is over the bar (or its buttons),
+ * and resumes it when the cursor leaves.
+ */
+ private val topBarHoverObserver: (MotionEvent) -> Unit = { event ->
+ if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
+ when (event.actionMasked) {
+ MotionEvent.ACTION_HOVER_ENTER,
+ MotionEvent.ACTION_HOVER_MOVE -> onTopBarInteractionStarted()
+
+ MotionEvent.ACTION_HOVER_EXIT -> onTopBarInteractionEnded()
+ }
+ }
+ }
+
@SuppressLint("ClickableViewAccessibility")
fun bind() {
if (isBound) return
@@ -131,6 +148,7 @@ class LandscapeImmersiveController(
appBarContent.addOnLayoutChangeListener(appBarLayoutChangeListener)
bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback)
appBarContent.onTouchEventObserved = topBarTouchObserver
+ appBarContent.onHoverEventObserved = topBarHoverObserver
}
fun onPause() {
@@ -166,6 +184,7 @@ class LandscapeImmersiveController(
appBarContent.removeOnLayoutChangeListener(appBarLayoutChangeListener)
bottomSheetBehavior.removeBottomSheetCallback(bottomSheetCallback)
appBarContent.onTouchEventObserved = null
+ appBarContent.onHoverEventObserved = null
}
fun onConfigurationChanged(newConfig: Configuration) {
diff --git a/app/src/main/res/layout-land/fragment_saved_projects.xml b/app/src/main/res/layout-land/fragment_saved_projects.xml
index 90bed44b20..2f2240c9aa 100644
--- a/app/src/main/res/layout-land/fragment_saved_projects.xml
+++ b/app/src/main/res/layout-land/fragment_saved_projects.xml
@@ -52,6 +52,7 @@
android:layout_height="wrap_content"
android:minWidth="0dp"
android:layout_marginEnd="2dp"
+ android:tooltipText="@string/exit"
android:text="@string/exit" />
@@ -72,6 +74,7 @@
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:tooltipText="@string/open_from_folder"
android:text="@string/open_from_folder" />
diff --git a/app/src/main/res/layout/layout_project_filters_bar.xml b/app/src/main/res/layout/layout_project_filters_bar.xml
index 214db313ee..d32239eed2 100644
--- a/app/src/main/res/layout/layout_project_filters_bar.xml
+++ b/app/src/main/res/layout/layout_project_filters_bar.xml
@@ -31,5 +31,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
+ android:tooltipText="@string/sort_projects_label"
app:icon="@drawable/ic_sort" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_project_filters_sheet.xml b/app/src/main/res/layout/layout_project_filters_sheet.xml
index 35aa9e0d2f..b5feec552b 100644
--- a/app/src/main/res/layout/layout_project_filters_sheet.xml
+++ b/app/src/main/res/layout/layout_project_filters_sheet.xml
@@ -40,11 +40,12 @@
@@ -67,13 +68,13 @@
android:layout_marginEnd="12dp"
android:visibility="gone"
android:text="@string/reset_sorting"
+ android:tooltipText="@string/reset_sorting"
app:iconTint="@color/white"
app:icon="@drawable/ic_close" />
Unit,
onLongClick: () -> Unit,
+ onHover: ((View) -> Unit)? = null,
+ onHoverExit: (() -> Unit)? = null,
shouldAddMargin: Boolean
) {
val item = ImageButton(context).apply {
- tooltipText = hint
+ if (onHover == null) {
+ tooltipText = hint
+ }
contentDescription = hint
setImageDrawable(icon)
addCircleRipple()
@@ -57,6 +63,24 @@ class ProjectActionsToolbar @JvmOverloads constructor(
onLongClick()
true
}
+ var hoverRunnable: Runnable? = null
+ setOnHoverListener { view, event ->
+ if (!event.isFromSource(InputDevice.SOURCE_MOUSE)) return@setOnHoverListener false
+
+ when (event.actionMasked) {
+ MotionEvent.ACTION_HOVER_ENTER -> {
+ hoverRunnable?.let { view.removeCallbacks(it) }
+ hoverRunnable = Runnable { onHover?.invoke(view) }
+ view.postDelayed(hoverRunnable, 600L)
+ }
+ MotionEvent.ACTION_HOVER_EXIT -> {
+ hoverRunnable?.let { view.removeCallbacks(it) }
+ onHoverExit?.invoke()
+ }
+ }
+
+ false
+ }
}
binding.menuContainer.addView(item)
}
diff --git a/common/src/main/java/com/itsaky/androidide/ui/TouchObservingLinearLayout.kt b/common/src/main/java/com/itsaky/androidide/ui/TouchObservingLinearLayout.kt
index f9b6376f71..5fe064e146 100644
--- a/common/src/main/java/com/itsaky/androidide/ui/TouchObservingLinearLayout.kt
+++ b/common/src/main/java/com/itsaky/androidide/ui/TouchObservingLinearLayout.kt
@@ -11,9 +11,15 @@ class TouchObservingLinearLayout @JvmOverloads constructor(
) : LinearLayout(context, attrs) {
var onTouchEventObserved: ((MotionEvent) -> Unit)? = null
+ var onHoverEventObserved: ((MotionEvent) -> Unit)? = null
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
onTouchEventObserved?.invoke(ev)
return super.dispatchTouchEvent(ev)
}
+
+ override fun dispatchHoverEvent(event: MotionEvent): Boolean {
+ onHoverEventObserved?.invoke(event)
+ return super.dispatchHoverEvent(event)
+ }
}
diff --git a/idetooltips/src/main/java/com/itsaky/androidide/idetooltips/ToolTipManager.kt b/idetooltips/src/main/java/com/itsaky/androidide/idetooltips/ToolTipManager.kt
index b95e3803d4..8180003054 100644
--- a/idetooltips/src/main/java/com/itsaky/androidide/idetooltips/ToolTipManager.kt
+++ b/idetooltips/src/main/java/com/itsaky/androidide/idetooltips/ToolTipManager.kt
@@ -22,6 +22,10 @@ import android.webkit.WebViewClient
import android.widget.ImageButton
import android.widget.PopupWindow
import android.widget.TextView
+import android.os.Handler
+import android.os.Looper
+import android.view.InputDevice
+import android.view.MotionEvent
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat.getColor
import com.itsaky.androidide.activities.editor.HelpActivity
@@ -41,6 +45,10 @@ import java.io.File
object TooltipManager {
private const val TAG = "TooltipManager"
+ private const val DEFAULT_HOVER_DISMISS_DELAY_MS = 800L
+ private var activePopupWindow: PopupWindow? = null
+ private val dismissHandler = Handler(Looper.getMainLooper())
+ private var pendingDismiss: Runnable? = null
private val databaseTimestamp: Long = File(Environment.DOC_DB.absolutePath).lastModified()
private val debugDatabaseFile: File = File(android.os.Environment.getExternalStorageDirectory().toString() +
"/Download/documentation.db")
@@ -149,6 +157,27 @@ object TooltipManager {
}
}
+ fun dismissActiveTooltip() {
+ cancelScheduledDismiss()
+ activePopupWindow?.dismiss()
+ activePopupWindow = null
+ }
+
+ fun scheduleActiveTooltipDismiss(delayMs: Long = DEFAULT_HOVER_DISMISS_DELAY_MS) {
+ cancelScheduledDismiss()
+ val popup = activePopupWindow ?: return
+ pendingDismiss = Runnable {
+ if (activePopupWindow === popup) {
+ popup.dismiss()
+ }
+ }.also { dismissHandler.postDelayed(it, delayMs) }
+ }
+
+ fun cancelScheduledDismiss() {
+ pendingDismiss?.let { dismissHandler.removeCallbacks(it) }
+ pendingDismiss = null
+ }
+
// Displays a tooltip for category [TooltipCategory.CATEGORY_IDE] in a particular context
// (An Activity, Fragment, Dialog etc)
fun showIdeCategoryTooltip(context: Context, anchorView: View, tag: String) {
@@ -355,6 +384,16 @@ object TooltipManager {
popupWindow.setBackgroundDrawable(ColorDrawable(transparentColor))
popupView.setBackgroundResource(R.drawable.idetooltip_popup_background)
+ dismissActiveTooltip()
+
+ activePopupWindow = popupWindow
+ popupWindow.setOnDismissListener {
+ cancelScheduledDismiss()
+ if (activePopupWindow === popupWindow) {
+ activePopupWindow = null
+ }
+ }
+
popupWindow.isFocusable = true
popupWindow.isOutsideTouchable = true
if (anchorView.isInOverlayWindow()) {
@@ -391,6 +430,31 @@ object TooltipManager {
}
setColorFilter(iconTintColor)
}
+
+ val hoverGuard: (MotionEvent) -> Unit = label@{ event ->
+ if (!event.isFromSource(InputDevice.SOURCE_MOUSE)) return@label
+ when (event.actionMasked) {
+ MotionEvent.ACTION_HOVER_ENTER,
+ MotionEvent.ACTION_HOVER_MOVE -> cancelScheduledDismiss()
+ MotionEvent.ACTION_HOVER_EXIT -> scheduleActiveTooltipDismiss()
+ }
+ }
+
+ val hoverListener = View.OnHoverListener { _, event ->
+ hoverGuard(event)
+ false
+ }
+
+ installHoverGuard(
+ hoverListener = hoverListener,
+ popupView = popupView,
+ webView = webView,
+ seeMore = seeMore,
+ infoButton = infoButton,
+ feedbackButton = feedbackButton,
+ )
+
+ cancelScheduledDismiss()
}
/**
@@ -508,4 +572,19 @@ object TooltipManager {
popupWindow.showAtLocation(parentView, Gravity.NO_GRAVITY, x, y)
}
+ private fun installHoverGuard(
+ hoverListener: View.OnHoverListener,
+ popupView: View,
+ webView: WebView,
+ seeMore: View,
+ infoButton: View,
+ feedbackButton: View,
+ ) {
+ popupView.setOnHoverListener(hoverListener)
+ webView.setOnHoverListener(hoverListener)
+ seeMore.setOnHoverListener(hoverListener)
+ infoButton.setOnHoverListener(hoverListener)
+ feedbackButton.setOnHoverListener(hoverListener)
+ }
+
}
diff --git a/resources/src/main/res/values/strings.xml b/resources/src/main/res/values/strings.xml
index e119dd9d51..6f942d2a77 100644
--- a/resources/src/main/res/values/strings.xml
+++ b/resources/src/main/res/values/strings.xml
@@ -145,6 +145,7 @@
Save files and close project
Search projects by nameā¦
Sort projects
+ Toggle sorting
Reset
Sort
Sort by