diff --git a/actions/src/main/java/com/itsaky/androidide/actions/ActionItem.kt b/actions/src/main/java/com/itsaky/androidide/actions/ActionItem.kt index 3afed20ba3..362a1a2452 100644 --- a/actions/src/main/java/com/itsaky/androidide/actions/ActionItem.kt +++ b/actions/src/main/java/com/itsaky/androidide/actions/ActionItem.kt @@ -24,6 +24,7 @@ import android.graphics.drawable.Drawable import android.view.Menu import android.view.View import androidx.annotation.CallSuper +import com.itsaky.androidide.idetooltips.TooltipCategory import com.itsaky.androidide.utils.resolveAttr /** @@ -90,6 +91,14 @@ interface ActionItem { */ fun retrieveTooltipTag(isReadOnlyContext: Boolean): String = "" + /** + * Retrieves the tooltip category for this [ActionItem]. The default is + * [TooltipCategory.CATEGORY_IDE]; plugin-contributed actions override this + * to point at their own `plugin_` category so the lookup hits + * tooltip rows the plugin installed via [DocumentationExtension]. + */ + fun retrieveTooltipCategory(): String = TooltipCategory.CATEGORY_IDE + /** * The order of this action item. This is used only at some locations and not everywhere. * diff --git a/app/src/main/java/com/itsaky/androidide/actions/PluginActionItem.kt b/app/src/main/java/com/itsaky/androidide/actions/PluginActionItem.kt index 037f89ae34..7dce447e52 100644 --- a/app/src/main/java/com/itsaky/androidide/actions/PluginActionItem.kt +++ b/app/src/main/java/com/itsaky/androidide/actions/PluginActionItem.kt @@ -6,12 +6,15 @@ import android.content.Context import android.graphics.drawable.Drawable import androidx.core.content.ContextCompat import com.itsaky.androidide.plugins.extensions.MenuItem +import com.itsaky.androidide.plugins.manager.pluginCategory +import com.itsaky.androidide.plugins.manager.pluginTooltipTag class PluginActionItem( context: Context, private val menuItem: MenuItem, - override val order: Int + override val order: Int, + val pluginId: String ) : EditorActivityAction() { override val id: String = "plugin.${menuItem.id}" @@ -29,6 +32,11 @@ class PluginActionItem( visible = menuItem.isVisible } + override fun retrieveTooltipTag(isReadOnlyContext: Boolean): String = + menuItem.tooltipTag ?: pluginTooltipTag(pluginId, menuItem.id) + + override fun retrieveTooltipCategory(): String = pluginCategory(pluginId) + override suspend fun execAction(data: ActionData): Any { return try { // Execute the plugin's action callback on UI thread diff --git a/app/src/main/java/com/itsaky/androidide/actions/PluginSidebarActionItem.kt b/app/src/main/java/com/itsaky/androidide/actions/PluginSidebarActionItem.kt index 164201f608..8377105f56 100644 --- a/app/src/main/java/com/itsaky/androidide/actions/PluginSidebarActionItem.kt +++ b/app/src/main/java/com/itsaky/androidide/actions/PluginSidebarActionItem.kt @@ -7,6 +7,8 @@ import android.graphics.drawable.Drawable import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import com.itsaky.androidide.plugins.extensions.NavigationItem +import com.itsaky.androidide.plugins.manager.pluginCategory +import com.itsaky.androidide.plugins.manager.pluginTooltipTag import com.itsaky.androidide.plugins.manager.ui.PluginDrawableResolver import com.itsaky.androidide.R import kotlinx.coroutines.Dispatchers @@ -17,7 +19,7 @@ class PluginSidebarActionItem( private val context: Context, private val navigationItem: NavigationItem, baseOrder: Int, - pluginId: String? = null + val pluginId: String ) : SidebarActionItem { override val id: String = "plugin_sidebar_${navigationItem.id}" @@ -42,6 +44,11 @@ class PluginSidebarActionItem( } } + override fun retrieveTooltipTag(isReadOnlyContext: Boolean): String = + navigationItem.tooltipTag ?: pluginTooltipTag(pluginId, navigationItem.id) + + override fun retrieveTooltipCategory(): String = pluginCategory(pluginId) + override suspend fun execAction(data: ActionData): Boolean { return try { // Plugin actions might need UI thread access for dialogs/UI operations 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 c377bab644..785cda65e4 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 @@ -459,17 +459,19 @@ open class EditorHandlerActivity : hint = getToolbarContentDescription(action, data), onClick = { if (action.enabled) registry.executeAction(action, data) }, onLongClick = { - TooltipManager.showIdeCategoryTooltip( + TooltipManager.showTooltip( context = this, anchorView = content.projectActionsToolbar, + category = action.retrieveTooltipCategory(), tag = action.retrieveTooltipTag(false), ) }, onHover = { anchor -> TooltipManager.cancelScheduledDismiss() - TooltipManager.showIdeCategoryTooltip( + TooltipManager.showTooltip( context = this@EditorHandlerActivity, anchorView = anchor, + category = action.retrieveTooltipCategory(), tag = action.retrieveTooltipTag(false), requestFocus = false, ) diff --git a/app/src/main/java/com/itsaky/androidide/fragments/sidebar/EditorSidebarFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/sidebar/EditorSidebarFragment.kt index 42407e2b1e..84d61b1764 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/sidebar/EditorSidebarFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/sidebar/EditorSidebarFragment.kt @@ -22,6 +22,7 @@ import android.view.View import com.itsaky.androidide.activities.editor.EditorHandlerActivity import com.itsaky.androidide.databinding.FragmentEditorSidebarBinding import com.itsaky.androidide.fragments.FragmentWithBinding +import com.itsaky.androidide.idetooltips.TooltipCategory import com.itsaky.androidide.idetooltips.TooltipManager import com.itsaky.androidide.utils.EditorSidebarActions @@ -39,12 +40,17 @@ class EditorSidebarFragment : FragmentWithBinding( EditorSidebarActions.setup(this) } - fun setupTooltip(view: View, tooltipTag: String) { + fun setupTooltip( + view: View, + tooltipTag: String, + category: String = TooltipCategory.CATEGORY_IDE, + ) { (requireActivity() as? EditorHandlerActivity)?.let { activity -> view.setOnLongClickListener { view -> - TooltipManager.showIdeCategoryTooltip( + TooltipManager.showTooltip( context = view.context, anchorView = view, + category = category, tag = tooltipTag, ) true diff --git a/app/src/main/java/com/itsaky/androidide/utils/EditorActivityActions.kt b/app/src/main/java/com/itsaky/androidide/utils/EditorActivityActions.kt index aad49bc32b..0a3e898137 100644 --- a/app/src/main/java/com/itsaky/androidide/utils/EditorActivityActions.kt +++ b/app/src/main/java/com/itsaky/androidide/utils/EditorActivityActions.kt @@ -185,8 +185,9 @@ class EditorActivityActions { .forEach { plugin -> try { Log.d("plugin_debug", "Registering menu items for plugin: ${plugin.javaClass.simpleName}") + val pluginId = pluginManager.getPluginIdForInstance(plugin as com.itsaky.androidide.plugins.IPlugin) ?: "" plugin.getMainMenuItems().forEach { menuItem -> - val action = PluginActionItem(context, menuItem, order++) + val action = PluginActionItem(context, menuItem, order++, pluginId) registry.registerAction(action) } } catch (e: Exception) { diff --git a/app/src/main/java/com/itsaky/androidide/utils/EditorSidebarActions.kt b/app/src/main/java/com/itsaky/androidide/utils/EditorSidebarActions.kt index 4f9a597f1b..1a9540d29b 100644 --- a/app/src/main/java/com/itsaky/androidide/utils/EditorSidebarActions.kt +++ b/app/src/main/java/com/itsaky/androidide/utils/EditorSidebarActions.kt @@ -145,7 +145,7 @@ internal object EditorSidebarActions { if (view != null && action != null) { val tag = action.retrieveTooltipTag(false) - sidebarFragment.setupTooltip(view, tag) + sidebarFragment.setupTooltip(view, tag, action.retrieveTooltipCategory()) } } @@ -254,7 +254,7 @@ internal object EditorSidebarActions { } sideMenuItems.forEach { navItem -> - val action = PluginSidebarActionItem(context, navItem, order++, pluginId) + val action = PluginSidebarActionItem(context, navItem, order++, pluginId ?: "") registry.registerAction(action) } } diff --git a/plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/extensions/DocumentationExtension.kt b/plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/extensions/DocumentationExtension.kt index 564cdbea96..8115845fb2 100644 --- a/plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/extensions/DocumentationExtension.kt +++ b/plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/extensions/DocumentationExtension.kt @@ -97,6 +97,8 @@ data class PluginTooltipButton( * - Paths with a leading "/" ("/some/shared/page") are treated as * absolute within the local web server (slash stripped). * - Anything containing "://" is passed through unchanged. + * - When [directPath] is true, namespacing is skipped and [uri] is + * used as-is under the local web server root. * The stored path is then prefixed with "http://localhost:6174/" by the * tooltip system when the button is rendered. */ @@ -105,5 +107,13 @@ data class PluginTooltipButton( /** * Order of this button (lower numbers appear first). */ - val order: Int = 0 + val order: Int = 0, + + /** + * When true, [uri] is resolved as a direct path under the local web server + * (no `plugin//` prefix). Use this to point at a shared page + * (for example a global plugins overview at "i/plugins-adfa.html") while + * keeping the path configurable by the plugin author. Default is false. + */ + val directPath: Boolean = false ) \ No newline at end of file diff --git a/plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/extensions/UIExtension.kt b/plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/extensions/UIExtension.kt index f69588c63a..75871ef5f1 100644 --- a/plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/extensions/UIExtension.kt +++ b/plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/extensions/UIExtension.kt @@ -56,6 +56,14 @@ data class MenuItem( val isVisible: Boolean = true, val shortcut: String? = null, val subItems: List = emptyList(), + /** + * Optional tooltip tag to look up under the plugin's tooltip category + * (`plugin_`). When null, the IDE composes a tag using the + * convention `.`. Supplying the same tooltipTag on a + * NavigationItem and a MenuItem lets a single PluginTooltipEntry serve + * both the sidebar and the toolbar surfaces. + */ + val tooltipTag: String? = null, val action: () -> Unit ) @@ -83,6 +91,13 @@ data class NavigationItem( val isVisible: Boolean = true, val group: String? = null, val order: Int = 0, + /** + * Optional tooltip tag to look up under the plugin's tooltip category + * (`plugin_`). When null, the IDE composes a tag using the + * convention `.` so plugins do not need to manually + * namespace tags to avoid collisions across plugins. + */ + val tooltipTag: String? = null, val action: () -> Unit ) diff --git a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/PluginTooltipNaming.kt b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/PluginTooltipNaming.kt new file mode 100644 index 0000000000..44226f8edf --- /dev/null +++ b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/PluginTooltipNaming.kt @@ -0,0 +1,7 @@ +package com.itsaky.androidide.plugins.manager + +const val PLUGIN_CATEGORY_PREFIX: String = "plugin_" + +fun pluginCategory(pluginId: String): String = "$PLUGIN_CATEGORY_PREFIX$pluginId" + +fun pluginTooltipTag(pluginId: String, itemId: String): String = "$pluginId.$itemId" diff --git a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt index 734a984e13..a32285134e 100644 --- a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt +++ b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt @@ -744,7 +744,7 @@ class PluginManager private constructor( fun getAllPlugins(): List { return loadedPlugins.values.map { loadedPlugin -> PluginInfo( - metadata = loadedPlugin.manifest.toPluginMetadata(), + metadata = loadedPlugin.toPluginMetadata(), isEnabled = loadedPlugin.isEnabled, isLoaded = true ) @@ -1486,4 +1486,10 @@ data class LoadedPlugin( var isEnabled: Boolean = true, val iconDayPath: String? = null, val iconNightPath: String? = null -) \ No newline at end of file +) + +fun LoadedPlugin.toPluginMetadata(): PluginMetadata = + manifest.toPluginMetadata().copy( + iconDayPath = iconDayPath, + iconNightPath = iconNightPath, + ) \ No newline at end of file diff --git a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt index c4853a5a0f..c9f4a748bf 100644 --- a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt +++ b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt @@ -7,6 +7,7 @@ import android.database.sqlite.SQLiteDatabase import android.util.Log import com.itsaky.androidide.plugins.extensions.DocumentationExtension import com.itsaky.androidide.plugins.extensions.PluginTooltipEntry +import com.itsaky.androidide.plugins.manager.pluginCategory import com.itsaky.androidide.resources.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -23,7 +24,6 @@ class PluginDocumentationManager(private val context: Context) { companion object { private const val TAG = "PluginDocManager" - private const val PLUGIN_CATEGORY_PREFIX = "plugin_" } private val databaseName = "documentation.db" @@ -43,7 +43,6 @@ class PluginDocumentationManager(private val context: Context) { } } - private fun pluginCategory(pluginId: String) = "$PLUGIN_CATEGORY_PREFIX$pluginId" /** * Initialize plugin documentation system. @@ -99,7 +98,7 @@ class PluginDocumentationManager(private val context: Context) { for (entry in entries) { val tooltipId = insertTooltip(db, categoryId, entry) entry.buttons.sortedBy { it.order }.forEachIndexed { index, button -> - val resolvedUri = resolvePluginButtonUri(pluginId, button.uri) + val resolvedUri = resolvePluginButtonUri(pluginId, button.uri, button.directPath) insertTooltipButton(db, tooltipId, button.description, resolvedUri, index) } } @@ -472,10 +471,14 @@ class PluginDocumentationManager(private val context: Context) { return segments.joinToString("/") } - private fun resolvePluginButtonUri(pluginId: String, rawUri: String): String { + private fun resolvePluginButtonUri( + pluginId: String, + rawUri: String, + directPath: Boolean + ): String { if (rawUri.isEmpty()) return rawUri if (rawUri.contains("://")) return rawUri - val absolute = rawUri.startsWith("/") + val absolute = directPath || rawUri.startsWith("/") val normalized = normalizeLocalDocumentationPath(rawUri.trimStart('/')) return if (absolute) normalized else "plugin/$pluginId/$normalized" } diff --git a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeTooltipServiceImpl.kt b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeTooltipServiceImpl.kt index 223c0219c4..4719f9b615 100644 --- a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeTooltipServiceImpl.kt +++ b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeTooltipServiceImpl.kt @@ -4,6 +4,7 @@ import android.content.Context import android.view.View import com.itsaky.androidide.idetooltips.TooltipManager import com.itsaky.androidide.plugins.manager.core.PluginManager +import com.itsaky.androidide.plugins.manager.pluginCategory import com.itsaky.androidide.plugins.services.IdeTooltipService /** @@ -18,7 +19,7 @@ class IdeTooltipServiceImpl( private val activityProvider: PluginManager.ActivityProvider? ) : IdeTooltipService { - private val pluginCategory = "plugin_$pluginId" + private val pluginCategory = pluginCategory(pluginId) /** * Returns a context suitable for inflating the tooltip layout.