Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
aa73225
ADFA-5088: Add per-row tooltip long-press to Preferences
davidschachterADFA Aug 13, 2026
1896332
ADFA-5088: Add per-item tooltip tag constants for Preferences
davidschachterADFA Aug 13, 2026
b2f729a
ADFA-5088: Tag General, Editor, Java, and XML preference items
davidschachterADFA Aug 13, 2026
369b1f3
ADFA-5088: Tag Build & Run items and each Gradle flag individually
davidschachterADFA Aug 13, 2026
2a0be9a
ADFA-5088: Tag Terminal, Git, Developer Options, Plugin Manager, About
davidschachterADFA Aug 13, 2026
0338392
ADFA-5088: Remove now-unused coarse tooltip tag constants
davidschachterADFA Aug 13, 2026
2669e95
ADFA-5088: Add docdb SQL script for new Preferences tooltip tags
davidschachterADFA Aug 13, 2026
e9ae4ff
ADFA-5088: Remove accidentally-committed vim swap file
davidschachterADFA Aug 13, 2026
d3cef4f
ADFA-5088: Give each Plugin Manager widget its own tooltip tag
davidschachterADFA Aug 13, 2026
ffa1743
ADFA-5088: Add docdb SQL script for Plugin Manager tooltip tags
davidschachterADFA Aug 13, 2026
bac9781
ADFA-5088: Add unit tests for collectTooltipTags
davidschachterADFA Aug 13, 2026
781dd05
ADFA-5088: Fix review findings in the preference tooltips SQL script
davidschachterADFA Aug 13, 2026
4ab13fd
ADFA-5088: Idempotent upserts and dead-tag cleanup in the Plugin Mana…
davidschachterADFA Aug 13, 2026
089219b
ADFA-5088: Fail fast on a bad or empty Brotli payload in the docdb SQL
davidschachterADFA Aug 13, 2026
58db4ad
Merge branch 'stage' into task/ADFA-5088-preference-tooltips
davidschachterADFA Aug 13, 2026
e430a22
ADFA-5088: Route docdb Brotli payloads through an owner-only workdir
davidschachterADFA Aug 13, 2026
f513641
Merge remote-tracking branch 'origin/task/ADFA-5088-preference-toolti…
davidschachterADFA Aug 13, 2026
3bc3628
ADFA-5088: Fix tooltip long-press coverage gaps from the per-row refa…
davidschachterADFA Aug 13, 2026
df577ab
ADFA-5088: Reuse displayTooltipOnLongPress in Plugin Manager instead …
davidschachterADFA Aug 13, 2026
174096f
ADFA-5088: Don't run a tooltip lookup for an empty tag
davidschachterADFA Aug 13, 2026
9e948d9
ADFA-5088: Link new Tooltips to their Content pages, drop banner comm…
davidschachterADFA Aug 13, 2026
28dc81a
ADFA-5088: Guard the SQL scripts' workdir, document getProperties()
davidschachterADFA Aug 13, 2026
e07cfb9
Merge branch 'stage' into task/ADFA-5088-preference-tooltips
davidschachterADFA Aug 13, 2026
89a8b0b
Merge branch 'stage' into task/ADFA-5088-preference-tooltips
davidschachterADFA Aug 13, 2026
f1a3395
Merge remote-tracking branch 'origin/stage' into task/ADFA-5088-prefe…
davidschachterADFA Aug 13, 2026
15dcf21
Merge branch 'stage' into task/ADFA-5088-preference-tooltips
davidschachterADFA Aug 13, 2026
7f5232b
ADFA-5088: Fix long-press tooltip gaps found by /code-review xhigh
davidschachterADFA Aug 14, 2026
6831991
ADFA-5088: Add KDoc to collectTooltipTags
davidschachterADFA Aug 14, 2026
ab5436c
ADFA-5088: Fix second-round /code-review xhigh findings
davidschachterADFA Aug 14, 2026
e04e880
Merge branch 'stage' into task/ADFA-5088-preference-tooltips
davidschachterADFA Aug 14, 2026
c52e3d5
ADFA-5088: Fix PR review findings from jatezzz
davidschachterADFA Aug 14, 2026
00bea3f
Merge remote-tracking branch 'origin/task/ADFA-5088-preference-toolti…
davidschachterADFA Aug 14, 2026
82170e0
ADFA-5088: Reindent the 8 preference-ext files flagged by jatezzz
davidschachterADFA Aug 14, 2026
07acfc4
ADFA-5088: Reinstate the workdir guard portably, no longer GNU-only
davidschachterADFA Aug 14, 2026
218db8a
ADFA-5088: Fall back to PREFS_TOP on a whitespace-only screen tag
davidschachterADFA Aug 14, 2026
a29d880
ADFA-5088: Add unit tests for pluginSettingsPreferences
davidschachterADFA Aug 14, 2026
ac6456f
ADFA-5088: Fix third-round /code-review xhigh findings
davidschachterADFA Aug 15, 2026
04f8702
ADFA-5088: Fix fourth-round /code-review max findings
davidschachterADFA Aug 15, 2026
a7aa7da
ADFA-5088: Fix fifth-round /code-review max findings
davidschachterADFA Aug 15, 2026
680bf90
ADFA-5088: Fix sixth-round /code-review findings
davidschachterADFA Aug 15, 2026
2103ecc
ADFA-5088: Address remaining CodeRabbit suggestions
davidschachterADFA Aug 16, 2026
ce817e9
ADFA-5088: Fix seventh-round /code-review max findings
davidschachterADFA Aug 16, 2026
1963ba3
Merge branch 'stage' into task/ADFA-5088-preference-tooltips
davidschachterADFA Aug 16, 2026
e0f6806
ADFA-5088: Fix eighth-round /code-review max findings
davidschachterADFA Aug 17, 2026
d94db47
ADFA-5088: Fix ninth-round /code-review high findings
davidschachterADFA Aug 17, 2026
c7dd886
ADFA-5088: Fix tenth-round /code-review high findings
davidschachterADFA Aug 17, 2026
04df234
ADFA-5088: Fix eleventh-round /code-review high findings
davidschachterADFA Aug 17, 2026
38c19b7
Merge branch 'stage' into task/ADFA-5088-preference-tooltips
davidschachterADFA Aug 17, 2026
ec63707
ADFA-5088: Fix twelfth-round /code-review high findings
davidschachterADFA Aug 17, 2026
fbd23ec
Merge branch 'task/ADFA-5088-preference-tooltips' into task/ADFA-5088…
davidschachterADFA Aug 17, 2026
0dcafde
ADFA-5088: Fix thirteenth-round /code-review high findings
davidschachterADFA Aug 17, 2026
9c2bc33
ADFA-5088: Fix fourteenth-round /code-review high findings
davidschachterADFA Aug 17, 2026
37deb40
ADFA-5088: Fix fifteenth-round /code-review high findings
davidschachterADFA Aug 17, 2026
ddb45f1
ADFA-5088: Address CodeRabbit KDoc suggestion on IDEPreferencesToolti…
davidschachterADFA Aug 17, 2026
c580939
Merge branch 'stage' into task/ADFA-5088-preference-tooltips
davidschachterADFA Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,21 @@ import com.itsaky.androidide.R
import com.itsaky.androidide.adapters.PluginListAdapter
import com.itsaky.androidide.app.EdgeToEdgeIDEActivity
import com.itsaky.androidide.databinding.ActivityPluginManagerBinding
import com.itsaky.androidide.idetooltips.TooltipManager
import com.itsaky.androidide.idetooltips.TooltipTag
import com.itsaky.androidide.plugins.PluginInfo
import com.itsaky.androidide.ui.models.PluginManagerUiEffect
import com.itsaky.androidide.ui.models.PluginManagerUiEvent
import com.itsaky.androidide.utils.DURATION_INDEFINITE
import com.itsaky.androidide.utils.DialogUtils.showRestartPrompt
import com.itsaky.androidide.utils.UrlManager
import com.itsaky.androidide.utils.displayTooltipOnLongPress
import com.itsaky.androidide.utils.errorIcon
import com.itsaky.androidide.utils.flashError
import com.itsaky.androidide.utils.flashSuccess
import com.itsaky.androidide.utils.flashbarBuilder
import com.itsaky.androidide.utils.getFileName
import com.itsaky.androidide.utils.onLongPress
import com.itsaky.androidide.utils.showIdeCategoryTooltipIfPresent
import com.itsaky.androidide.utils.showOnUiThread
import com.itsaky.androidide.viewmodels.PluginManagerViewModel
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -119,9 +121,8 @@ class PluginManagerActivity : EdgeToEdgeIDEActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_plugin_manager, menu)
binding.toolbar.post {
binding.toolbar.findViewById<View>(R.id.action_discover_plugins)?.setOnLongClickListener { view ->
TooltipManager.showIdeCategoryTooltip(this, view, TooltipTag.PLUGIN_MANAGER)
true
binding.toolbar.findViewById<View>(R.id.action_discover_plugins)?.let { view ->
view.displayTooltipOnLongPress(this, TooltipTag.PLUGIN_MANAGER_DOWNLOAD)
}
}
return true
Expand Down Expand Up @@ -177,24 +178,26 @@ class PluginManagerActivity : EdgeToEdgeIDEActivity() {
}

private fun setupTooltipLongPress() {
val showTooltip: (View) -> Unit = { view ->
TooltipManager.showIdeCategoryTooltip(this, view, TooltipTag.PLUGIN_MANAGER)
}
binding.toolbar.setOnLongClickListener {
showTooltip(it)
true
}
binding.fabInstallPlugin.setOnLongClickListener {
showTooltip(it)
true
val show: (View, String) -> Unit = { view, tag ->
view.displayTooltipOnLongPress(this, tag)
}
binding.emptyState.setOnLongClickListener {
showTooltip(it)
true
}
binding.recyclerView.setOnLongClickListener {
showTooltip(it)
true
show(binding.toolbar, TooltipTag.PLUGIN_MANAGER_TOOLBAR)
show(binding.fabInstallPlugin, TooltipTag.PLUGIN_MANAGER_FAB_INSTALL)
show(binding.emptyState, TooltipTag.PLUGIN_MANAGER_EMPTY_STATE)

// A plain setOnLongClickListener on a RecyclerView never fires - it overrides
// onTouchEvent() for scroll handling and doesn't run the base View long-press
// detection - so empty list space needs the same GestureDetector-based approach
// used for IDEPreferencesFragment's RecyclerView. Skipped when a row is under the
// touch point since each row already shows its own tooltip (PluginListAdapter).
binding.recyclerView.onLongPress { e ->
// _binding directly, not the checkNotNull-backed binding getter: the ~500ms long-press
// timer can fire after the activity (and its binding) is destroyed.
val recyclerView = _binding?.recyclerView ?: return@onLongPress
if (recyclerView.findChildViewUnder(e.x, e.y) != null) {
return@onLongPress
}
showIdeCategoryTooltipIfPresent(this, recyclerView, TooltipTag.PLUGIN_MANAGER_LIST)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@
package com.itsaky.androidide.activities

import android.os.Bundle
import android.view.GestureDetector
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.View
import androidx.core.graphics.Insets
import androidx.core.os.BundleCompat
Expand All @@ -29,10 +26,12 @@ import com.itsaky.androidide.R
import com.itsaky.androidide.app.EdgeToEdgeIDEActivity
import com.itsaky.androidide.databinding.ActivityPreferencesBinding
import com.itsaky.androidide.fragments.IDEPreferencesFragment
import com.itsaky.androidide.idetooltips.TooltipManager
import com.itsaky.androidide.idetooltips.TooltipTag
import com.itsaky.androidide.preferences.PluginSettingsEntryPreference
import com.itsaky.androidide.preferences.addRootPreferences
import com.itsaky.androidide.preferences.pluginSettingsPreferences
import com.itsaky.androidide.utils.onLongPress
import com.itsaky.androidide.utils.showIdeCategoryTooltipIfPresent
import com.itsaky.androidide.preferences.IDEPreferences as prefs

class PreferencesActivity : EdgeToEdgeIDEActivity() {
Expand All @@ -50,20 +49,6 @@ class PreferencesActivity : EdgeToEdgeIDEActivity() {
*/
private var contributedPreferences: List<PluginSettingsEntryPreference>? = null

private val gestureDetector by lazy {
GestureDetector(
this,
object : GestureDetector.SimpleOnGestureListener() {
override fun onLongPress(e: MotionEvent) {
binding.root.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
val currentFragment = supportFragmentManager.findFragmentById(binding.fragmentContainer.id) as? IDEPreferencesFragment
val tooltipTag = currentFragment?.getCurrentScreenTooltip() ?: ""
TooltipManager.showIdeCategoryTooltip(this@PreferencesActivity, binding.root, tooltipTag)
}
},
)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

Expand All @@ -72,6 +57,21 @@ class PreferencesActivity : EdgeToEdgeIDEActivity() {
supportActionBar!!.setDisplayHomeAsUpEnabled(true)

binding.toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
binding.toolbar.setOnLongClickListener {
// No manual haptic feedback: the platform already fires it for a listener returning true.
showScreenTooltip(_binding?.toolbar, playHapticFeedback = false)
true
}

// Fallback for a long-press that lands below a short screen's content: the RecyclerView is
// wrap_content-sized to its own rows, so on a short screen (e.g. Git, About) it doesn't fill
// this NestedScrollView, and the remaining space would otherwise have no tooltip at all.
// A plain setOnLongClickListener never fires here - NestedScrollView overrides onTouchEvent()
// for its own drag/fling handling, same as RecyclerView - so this needs the GestureDetector-
// based View.onLongPress instead.
binding.fragmentContainerParent.onLongPress {
showScreenTooltip(_binding?.fragmentContainerParent, playHapticFeedback = true)
}

feedbackButtonManager =
FeedbackButtonManager(
Expand Down Expand Up @@ -107,6 +107,7 @@ class PreferencesActivity : EdgeToEdgeIDEActivity() {
IDEPreferencesFragment.EXTRA_CHILDREN,
ArrayList(prefs.children),
)
args.putString(IDEPreferencesFragment.EXTRA_SCREEN_TOOLTIP_TAG, prefs.tooltipTag)

// A fresh instance every time: arguments cannot be set on a fragment whose state was saved.
loadFragment(IDEPreferencesFragment().also { it.arguments = args })
Expand Down Expand Up @@ -157,16 +158,31 @@ class PreferencesActivity : EdgeToEdgeIDEActivity() {
super.loadFragment(fragment, binding.fragmentContainer.id)
}

/** The tag of whichever [IDEPreferencesFragment] screen is currently on top, or [TooltipTag.PREFS_TOP]. */
private fun currentScreenTooltipTag(): String {
val fragment = supportFragmentManager.findFragmentById(binding.fragmentContainer.id)
return (fragment as? IDEPreferencesFragment)?.screenTooltipTag ?: TooltipTag.PREFS_TOP
}

/**
* Shows the current screen's tooltip anchored to [view], or does nothing if it's null - both
* call sites pass `_binding?.someView` directly rather than the checkNotNull-backed [binding]
* getter, since a long-press timer (or a listener callback) can fire after the activity (and
* its binding) has already been destroyed.
*/
private fun showScreenTooltip(
view: View?,
playHapticFeedback: Boolean,
) {
view ?: return
showIdeCategoryTooltipIfPresent(this, view, currentScreenTooltipTag(), playHapticFeedback)
}

override fun onDestroy() {
super.onDestroy()
_binding = null
}

override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
gestureDetector.onTouchEvent(ev)
return super.dispatchTouchEvent(ev)
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
contributedPreferences?.let {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import com.bumptech.glide.Glide
import com.bumptech.glide.signature.ObjectKey
import com.itsaky.androidide.R
import com.itsaky.androidide.databinding.ItemPluginBinding
import com.itsaky.androidide.idetooltips.TooltipManager
import com.itsaky.androidide.idetooltips.TooltipTag
import com.itsaky.androidide.plugins.PluginInfo
import com.itsaky.androidide.utils.displayTooltipOnLongPress
import com.itsaky.androidide.utils.isSystemInDarkMode
import java.io.File

Expand Down Expand Up @@ -55,6 +55,13 @@ class PluginListAdapter(
inner class PluginViewHolder(
private val binding: ItemPluginBinding,
) : RecyclerView.ViewHolder(binding.root) {
init {
// Both the anchor views and their tags are fixed per view holder, not per bound plugin -
// register once here instead of re-registering an identical listener on every bind().
binding.btnMenu.displayTooltipOnLongPress(itemView.context, TooltipTag.PLUGIN_MANAGER_ITEM_MENU)
binding.root.displayTooltipOnLongPress(itemView.context, TooltipTag.PLUGIN_MANAGER_ITEM)
}

fun bind(plugin: PluginInfo) {
binding.apply {
pluginName.text = plugin.metadata.name
Expand Down Expand Up @@ -120,12 +127,6 @@ class PluginListAdapter(
root.setOnClickListener {
onActionClick(plugin, Action.DETAILS)
}

// Long-press for Plugin Manager tooltip
root.setOnLongClickListener {
TooltipManager.showIdeCategoryTooltip(it.context, it, TooltipTag.PLUGIN_MANAGER)
true
}
}
}

Expand Down
Loading
Loading