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