Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 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
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,14 +24,15 @@ 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.TooltipCategory
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
Expand Down Expand Up @@ -119,9 +120,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, view, TooltipCategory.CATEGORY_IDE, TooltipTag.PLUGIN_MANAGER_DOWNLOAD)
}
}
return true
Expand Down Expand Up @@ -177,25 +177,13 @@ 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
}
binding.emptyState.setOnLongClickListener {
showTooltip(it)
true
}
binding.recyclerView.setOnLongClickListener {
showTooltip(it)
true
val show: (View, String) -> Unit = { view, tag ->
view.displayTooltipOnLongPress(this, view, TooltipCategory.CATEGORY_IDE, tag)
}
show(binding.toolbar, TooltipTag.PLUGIN_MANAGER_TOOLBAR)
show(binding.fabInstallPlugin, TooltipTag.PLUGIN_MANAGER_FAB_INSTALL)
show(binding.emptyState, TooltipTag.PLUGIN_MANAGER_EMPTY_STATE)
show(binding.recyclerView, TooltipTag.PLUGIN_MANAGER_LIST)
}

private fun setupFeedbackButton() {
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 @@ -30,6 +27,7 @@ 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
Expand All @@ -50,20 +48,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 +56,10 @@ class PreferencesActivity : EdgeToEdgeIDEActivity() {
supportActionBar!!.setDisplayHomeAsUpEnabled(true)

binding.toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
binding.toolbar.setOnLongClickListener {
TooltipManager.showIdeCategoryTooltip(this, binding.toolbar, TooltipTag.PREFS_TOP)
true
}

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

// 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 @@ -162,11 +151,6 @@ class PreferencesActivity : EdgeToEdgeIDEActivity() {
_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,10 @@ 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.TooltipCategory
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 @@ -115,17 +116,24 @@ class PluginListAdapter(
btnMenu.setOnClickListener { view ->
showPopupMenu(view, plugin)
}
btnMenu.displayTooltipOnLongPress(
itemView.context,
btnMenu,
TooltipCategory.CATEGORY_IDE,
TooltipTag.PLUGIN_MANAGER_ITEM_MENU,
)

// Setup item click for details
root.setOnClickListener {
onActionClick(plugin, Action.DETAILS)
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,92 +18,146 @@
package com.itsaky.androidide.fragments

import android.os.Bundle
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceGroup
import androidx.preference.PreferenceGroupAdapter
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.transition.MaterialSharedAxis
import com.itsaky.androidide.idetooltips.TooltipTag.PREFS_DEVELOPER
import com.itsaky.androidide.idetooltips.TooltipTag.PREFS_EDITOR
import com.itsaky.androidide.idetooltips.TooltipTag.PREFS_EDITOR_XML
import com.itsaky.androidide.idetooltips.TooltipTag.PREFS_GENERAL
import com.itsaky.androidide.idetooltips.TooltipTag.PREFS_GRADLE
import com.itsaky.androidide.idetooltips.TooltipTag.PREFS_TERMUX
import com.itsaky.androidide.idetooltips.TooltipManager
import com.itsaky.androidide.idetooltips.TooltipTag.PREFS_TOP
import com.itsaky.androidide.preferences.IPreference
import com.itsaky.androidide.preferences.IPreferenceGroup
import com.itsaky.androidide.preferences.IPreferenceScreen
import com.itsaky.androidide.utils.onLongPress

class IDEPreferencesFragment : BasePreferenceFragment() {

private var children: List<IPreference> = emptyList()

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
return super.onCreateView(inflater, container, savedInstanceState)
}

override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
super.onCreatePreferences(savedInstanceState, rootKey)

if (context == null) {
return
}

@Suppress("DEPRECATION")
this.children = arguments?.getParcelableArrayList(EXTRA_CHILDREN) ?: emptyList()

preferenceScreen.removeAll()
addChildren(this.children, preferenceScreen)
}

private fun addChildren(children: List<IPreference>, pref: PreferenceGroup) {
for (child in children) {
val preference = child.onCreateView(requireContext())
if (child is IPreferenceScreen) {
preference.fragment = IDEPreferencesFragment::class.java.name
preference.extras.putParcelableArrayList(EXTRA_CHILDREN, ArrayList(child.children))


pref.addPreference(preference)
continue
}

if (child is IPreferenceGroup) {
pref.addPreference(preference as PreferenceCategory)
addChildren(child.children, preference)
continue
}


pref.addPreference(preference)
}
}

fun getCurrentScreenTooltip(): String {
val firstChildKey = children.firstOrNull()?.key
return when (firstChildKey) {
"idepref_configure" -> PREFS_TOP
"idepref_general_interface" -> PREFS_GENERAL
"idepref_editor_common" -> PREFS_EDITOR
"idepref_build_gradle" -> PREFS_GRADLE
"idepref_build_gradleCommands" -> PREFS_GRADLE
"ide.preferences.terminal.debugging" -> PREFS_TERMUX
"ide.prefs.developerOptions.debugging" -> PREFS_DEVELOPER
"idepref_xml_trimFinalNewLine" -> PREFS_EDITOR_XML
else -> PREFS_TOP
}
}

companion object {
const val EXTRA_CHILDREN = "ide.preferences.fragment.children"
}
private var children: List<IPreference> = emptyList()

/** Every preference in this screen, including nested categories' children, keyed by its key. */
private var tooltipTagsByKey: Map<String, String> = emptyMap()

/**
* This screen's own tag - the fallback for a long-press that lands on empty RecyclerView
* space (no row under the touch point) or on a row with no tooltipTag of its own.
*/
private var screenTooltipTag: String = PREFS_TOP

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
return super.onCreateView(inflater, container, savedInstanceState)
}

override fun onCreatePreferences(
savedInstanceState: Bundle?,
rootKey: String?,
) {
super.onCreatePreferences(savedInstanceState, rootKey)

if (context == null) {
return
}

@Suppress("DEPRECATION")
this.children = arguments?.getParcelableArrayList(EXTRA_CHILDREN) ?: emptyList()
this.tooltipTagsByKey = collectTooltipTags(this.children)
this.screenTooltipTag = arguments?.getString(EXTRA_SCREEN_TOOLTIP_TAG)?.takeIf { it.isNotEmpty() } ?: PREFS_TOP

preferenceScreen.removeAll()
addChildren(this.children, preferenceScreen)
}

override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
super.onViewCreated(view, savedInstanceState)

// Captured once: re-reading the `listView` property from inside the callback (which can
// fire after a delay) would hit a field PreferenceFragmentCompat clears in onDestroyView.
val recyclerView = listView

recyclerView.onLongPress { e ->
if (!isAdded) {
return@onLongPress
}
val ctx = context ?: return@onLongPress

val row = recyclerView.findChildViewUnder(e.x, e.y)
val tag = row?.let { resolveTooltipTag(recyclerView, it) } ?: screenTooltipTag
val anchor = row ?: recyclerView

anchor.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
TooltipManager.showIdeCategoryTooltip(ctx, anchor, tag)
}
}

/** The row's own tooltipTag, or null if there's no row at that position or it has none. */
private fun resolveTooltipTag(
recyclerView: RecyclerView,
row: View,
): String? {
val position = recyclerView.getChildAdapterPosition(row)
if (position == RecyclerView.NO_POSITION) {
return null
}

val key = (recyclerView.adapter as? PreferenceGroupAdapter)?.getItem(position)?.key ?: return null
return tooltipTagsByKey[key]?.takeIf { it.isNotEmpty() }
}

internal fun collectTooltipTags(children: List<IPreference>): Map<String, String> {
val map = mutableMapOf<String, String>()

fun visit(items: List<IPreference>) {
for (item in items) {
map[item.key] = item.tooltipTag
if (item is IPreferenceGroup && item !is IPreferenceScreen) {
visit(item.children)
}
}
}

visit(children)
return map
}

private fun addChildren(
children: List<IPreference>,
pref: PreferenceGroup,
) {
for (child in children) {
val preference = child.onCreateView(requireContext())
if (child is IPreferenceScreen) {
preference.fragment = IDEPreferencesFragment::class.java.name
preference.extras.putParcelableArrayList(EXTRA_CHILDREN, ArrayList(child.children))
preference.extras.putString(EXTRA_SCREEN_TOOLTIP_TAG, child.tooltipTag)

pref.addPreference(preference)
continue
}

if (child is IPreferenceGroup) {
pref.addPreference(preference as PreferenceCategory)
addChildren(child.children, preference)
continue
}

pref.addPreference(preference)
}
}

companion object {
const val EXTRA_CHILDREN = "ide.preferences.fragment.children"
const val EXTRA_SCREEN_TOOLTIP_TAG = "ide.preferences.fragment.screenTooltipTag"
}
}

Loading
Loading