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 23fc24ddbc..e45ad1557c 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 @@ -781,7 +781,7 @@ class PluginManager private constructor( pluginId, "tooltip" ) { - IdeTooltipServiceImpl(context) + IdeTooltipServiceImpl(context, pluginId, activityProvider) } // Editor tab service for plugin editor tab integration @@ -904,7 +904,7 @@ class PluginManager private constructor( pluginId, "tooltip" ) { - IdeTooltipServiceImpl(context) + IdeTooltipServiceImpl(context, pluginId, activityProvider) } // Editor tab service for plugin editor tab integration 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 1c1368c1b2..3872e24498 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 @@ -10,155 +10,58 @@ import com.itsaky.androidide.resources.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.File -import androidx.core.database.sqlite.transaction +import kotlin.coroutines.cancellation.CancellationException /** - * Manages plugin documentation in an isolated database. - * This ensures plugin documentation is independent from the main app's documentation - * and won't be affected by app updates that change the database schema. + * Manages plugin documentation by writing into the main documentation.db. + * Plugin entries are stored in the existing Tooltips/TooltipCategories/TooltipButtons tables, + * differentiated by a "plugin_" category prefix so they never conflict with + * built-in documentation. */ class PluginDocumentationManager(private val context: Context) { companion object { private const val TAG = "PluginDocManager" + private const val PLUGIN_CATEGORY_PREFIX = "plugin_" } - private val databaseVersion = 1 - private val databaseName = "plugin_documentation.db" + private val databaseName = "documentation.db" - // Database schema creation statements - private val createCategoriesTable = """ - CREATE TABLE IF NOT EXISTS PluginTooltipCategories ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - category TEXT NOT NULL UNIQUE - ) - """ - - private val createTooltipsTable = """ - CREATE TABLE IF NOT EXISTS PluginTooltips ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - categoryId INTEGER NOT NULL, - tag TEXT NOT NULL, - summary TEXT NOT NULL, - detail TEXT, - FOREIGN KEY(categoryId) REFERENCES PluginTooltipCategories(id), - UNIQUE(categoryId, tag) - ) - """ - - private val createButtonsTable = """ - CREATE TABLE IF NOT EXISTS PluginTooltipButtons ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - tooltipId INTEGER NOT NULL, - description TEXT NOT NULL, - uri TEXT NOT NULL, - buttonNumberId INTEGER NOT NULL, - FOREIGN KEY(tooltipId) REFERENCES PluginTooltips(id) ON DELETE CASCADE - ) - """ - - private val createTrackingTable = """ - CREATE TABLE IF NOT EXISTS PluginTracking ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - pluginId TEXT NOT NULL, - tooltipId INTEGER NOT NULL, - categoryId INTEGER NOT NULL, - installedAt INTEGER NOT NULL, - UNIQUE(pluginId, tooltipId) - ) - """ - - private val createMetadataTable = """ - CREATE TABLE IF NOT EXISTS PluginMetadata ( - pluginId TEXT PRIMARY KEY, - lastUpdated INTEGER NOT NULL, - version TEXT - ) - """ - - /** - * Get or create the plugin documentation database. - */ private suspend fun getPluginDatabase(): SQLiteDatabase? = withContext(Dispatchers.IO) { try { - val dbPath = context.getDatabasePath(databaseName).absolutePath - val dbFile = File(dbPath) - - val isNewDatabase = !dbFile.exists() - - // Open or create the database - val db = SQLiteDatabase.openOrCreateDatabase(dbFile, null) - - if (isNewDatabase) { - Log.d(TAG, "Creating new plugin documentation database at: $dbPath") - initializeDatabase(db) - } else { - // Check if tables exist, initialize if not - if (!tablesExist(db)) { - Log.d(TAG, "Database exists but tables missing, initializing...") - initializeDatabase(db) - } + val dbFile = context.getDatabasePath(databaseName) + if (!dbFile.exists()) { + Log.w(TAG, "documentation.db not yet available at: ${dbFile.absolutePath}") + return@withContext null } - - db + SQLiteDatabase.openDatabase(dbFile.absolutePath, null, SQLiteDatabase.OPEN_READWRITE) } catch (e: Exception) { - Log.e(TAG, "Failed to open/create plugin documentation database", e) + if (e is CancellationException) throw e + Log.e(TAG, "Failed to open documentation.db for plugin writes", e) null } } - /** - * Check if required tables exist in the database. - */ - private fun tablesExist(db: SQLiteDatabase): Boolean { - val cursor = db.rawQuery( - "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name IN (?, ?, ?, ?, ?)", - arrayOf("PluginTooltipCategories", "PluginTooltips", "PluginTooltipButtons", "PluginTracking", "PluginMetadata") - ) - val exists = cursor.moveToFirst() && cursor.getInt(0) == 5 - cursor.close() - return exists - } + private fun pluginCategory(pluginId: String) = "$PLUGIN_CATEGORY_PREFIX$pluginId" /** - * Initialize the database with required tables. + * Initialize plugin documentation system. + * Also cleans up the legacy plugin_documentation.db if present. */ - private fun initializeDatabase(db: SQLiteDatabase) { - db.transaction { - try { - execSQL(createCategoriesTable) - execSQL(createTooltipsTable) - execSQL(createButtonsTable) - execSQL(createTrackingTable) - execSQL(createMetadataTable) - - // Create indices for better performance - execSQL("CREATE INDEX IF NOT EXISTS idx_tooltips_category ON PluginTooltips(categoryId)") - execSQL("CREATE INDEX IF NOT EXISTS idx_buttons_tooltip ON PluginTooltipButtons(tooltipId)") - execSQL("CREATE INDEX IF NOT EXISTS idx_tracking_plugin ON PluginTracking(pluginId)") - - Log.d(TAG, "Plugin documentation database initialized successfully") - } catch (e: Exception) { - Log.e(TAG, "Failed to initialize plugin documentation database", e) - throw e - } finally { + suspend fun initialize() = withContext(Dispatchers.IO) { + val legacyDb = context.getDatabasePath("plugin_documentation.db") + if (legacyDb.exists()) { + if (legacyDb.delete()) { + Log.d(TAG, "Removed legacy plugin_documentation.db") + } else { + Log.w(TAG, "Failed to remove legacy plugin_documentation.db") } } - } - - /** - * Initialize plugin database if needed. - * This can be called on app startup to ensure the database exists. - */ - suspend fun initialize() = withContext(Dispatchers.IO) { - val db = getPluginDatabase() - db?.close() Log.d(TAG, "Plugin documentation system initialized") } /** - * Install documentation from a plugin. - * Inserts tooltips into the isolated plugin documentation database. + * Install documentation from a plugin into documentation.db. */ suspend fun installPluginDocumentation( pluginId: String, @@ -176,7 +79,6 @@ class PluginDocumentationManager(private val context: Context) { return@withContext false } - val category = plugin.getTooltipCategory() val entries = plugin.getTooltipEntries() if (entries.isEmpty()) { @@ -185,36 +87,26 @@ class PluginDocumentationManager(private val context: Context) { return@withContext true } - Log.d(TAG, "Installing ${entries.size} tooltip entries for plugin $pluginId (category: $category)") + Log.d(TAG, "Installing ${entries.size} tooltip entries for plugin $pluginId") db.beginTransaction() try { - // First, remove any existing documentation for this plugin removePluginDocumentationInternal(db, pluginId) - // Insert or get category ID - val categoryId = insertOrGetCategoryId(db, category) + val categoryId = insertOrGetCategoryId(db, pluginCategory(pluginId)) - // Insert tooltips and track them for (entry in entries) { val tooltipId = insertTooltip(db, categoryId, entry) - - // Track this tooltip for later cleanup - trackPluginTooltip(db, pluginId, tooltipId, categoryId) - - // Insert buttons for this tooltip entry.buttons.sortedBy { it.order }.forEachIndexed { index, button -> insertTooltipButton(db, tooltipId, button.description, button.uri, index) } } - // Update plugin metadata - updatePluginMetadata(db, pluginId) - db.setTransactionSuccessful() Log.d(TAG, "Successfully installed documentation for plugin $pluginId") true } catch (e: Exception) { + if (e is CancellationException) throw e Log.e(TAG, "Failed to install documentation for plugin $pluginId", e) false } finally { @@ -224,7 +116,7 @@ class PluginDocumentationManager(private val context: Context) { } /** - * Remove all documentation for a plugin. + * Remove all documentation for a plugin from documentation.db. */ suspend fun removePluginDocumentation( pluginId: String, @@ -242,14 +134,11 @@ class PluginDocumentationManager(private val context: Context) { db.beginTransaction() try { removePluginDocumentationInternal(db, pluginId) - - // Remove plugin metadata - db.delete("PluginMetadata", "pluginId = ?", arrayOf(pluginId)) - db.setTransactionSuccessful() Log.d(TAG, "Successfully removed documentation for plugin $pluginId") true } catch (e: Exception) { + if (e is CancellationException) throw e Log.e(TAG, "Failed to remove documentation for plugin $pluginId", e) false } finally { @@ -259,13 +148,15 @@ class PluginDocumentationManager(private val context: Context) { } private fun removePluginDocumentationInternal(db: SQLiteDatabase, pluginId: String) { - // Get all tooltip IDs for this plugin - val cursor = db.query( - "PluginTracking", - arrayOf("tooltipId"), - "pluginId = ?", - arrayOf(pluginId), - null, null, null + val category = pluginCategory(pluginId) + + val cursor = db.rawQuery( + """ + SELECT T.id FROM Tooltips AS T + INNER JOIN TooltipCategories AS TC ON T.categoryId = TC.id + WHERE TC.category = ? + """.trimIndent(), + arrayOf(category) ) val tooltipIds = mutableListOf() @@ -275,32 +166,18 @@ class PluginDocumentationManager(private val context: Context) { cursor.close() if (tooltipIds.isNotEmpty()) { - // Delete buttons for these tooltips val placeholders = tooltipIds.joinToString(",") { "?" } - db.delete( - "PluginTooltipButtons", - "tooltipId IN ($placeholders)", - tooltipIds.map { it.toString() }.toTypedArray() - ) - - // Delete the tooltips themselves - db.delete( - "PluginTooltips", - "id IN ($placeholders)", - tooltipIds.map { it.toString() }.toTypedArray() - ) + val args = tooltipIds.map { it.toString() }.toTypedArray() + db.delete("TooltipButtons", "tooltipId IN ($placeholders)", args) + db.delete("Tooltips", "id IN ($placeholders)", args) } - // Remove tracking entries - db.delete("PluginTracking", "pluginId = ?", arrayOf(pluginId)) - - // Note: We don't delete categories as they might be shared with other plugins + db.delete("TooltipCategories", "category = ?", arrayOf(category)) } private fun insertOrGetCategoryId(db: SQLiteDatabase, category: String): Long { - // Check if category already exists val cursor = db.query( - "PluginTooltipCategories", + "TooltipCategories", arrayOf("id"), "category = ?", arrayOf(category), @@ -314,11 +191,10 @@ class PluginDocumentationManager(private val context: Context) { } cursor.close() - // Insert new category val values = ContentValues().apply { put("category", category) } - return db.insert("PluginTooltipCategories", null, values) + return db.insert("TooltipCategories", null, values) } private fun insertTooltip( @@ -327,9 +203,9 @@ class PluginDocumentationManager(private val context: Context) { entry: PluginTooltipEntry ): Long { val disclaimer = context.getString(R.string.plugin_documentation_third_party_disclaimer) - // Check if tooltip with same tag already exists in this category + val existingCursor = db.query( - "PluginTooltips", + "Tooltips", arrayOf("id"), "categoryId = ? AND tag = ?", arrayOf(categoryId.toString(), entry.tag), @@ -340,48 +216,23 @@ class PluginDocumentationManager(private val context: Context) { val existingId = existingCursor.getLong(0) existingCursor.close() - // Update existing tooltip with disclaimer val updateValues = ContentValues().apply { put("summary", entry.summary + disclaimer) put("detail", if (entry.detail.isNotBlank()) entry.detail + disclaimer else "") } - db.update("PluginTooltips", updateValues, "id = ?", arrayOf(existingId.toString())) - - // Delete old buttons (we'll re-insert them) - db.delete("PluginTooltipButtons", "tooltipId = ?", arrayOf(existingId.toString())) - + db.update("Tooltips", updateValues, "id = ?", arrayOf(existingId.toString())) + db.delete("TooltipButtons", "tooltipId = ?", arrayOf(existingId.toString())) return existingId } existingCursor.close() - // Insert new tooltip with disclaimer val values = ContentValues().apply { put("categoryId", categoryId) put("tag", entry.tag) put("summary", entry.summary + disclaimer) put("detail", if (entry.detail.isNotBlank()) entry.detail + disclaimer else "") } - return db.insert("PluginTooltips", null, values) - } - - private fun trackPluginTooltip( - db: SQLiteDatabase, - pluginId: String, - tooltipId: Long, - categoryId: Long - ) { - val values = ContentValues().apply { - put("pluginId", pluginId) - put("tooltipId", tooltipId) - put("categoryId", categoryId) - put("installedAt", System.currentTimeMillis()) - } - db.insertWithOnConflict( - "PluginTracking", - null, - values, - SQLiteDatabase.CONFLICT_REPLACE - ) + return db.insert("Tooltips", null, values) } private fun insertTooltipButton( @@ -397,93 +248,32 @@ class PluginDocumentationManager(private val context: Context) { put("uri", uri) put("buttonNumberId", order) } - db.insert("PluginTooltipButtons", null, values) - } - - private fun updatePluginMetadata(db: SQLiteDatabase, pluginId: String) { - val values = ContentValues().apply { - put("pluginId", pluginId) - put("lastUpdated", System.currentTimeMillis()) - put("version", "1.0") // Can be updated to track actual plugin version - } - db.insertWithOnConflict( - "PluginMetadata", - null, - values, - SQLiteDatabase.CONFLICT_REPLACE - ) - } - - /** - * Get all plugin categories currently in the database. - */ - suspend fun getPluginCategories(pluginId: String): List = withContext(Dispatchers.IO) { - val db = getPluginDatabase() ?: return@withContext emptyList() - - val categories = mutableListOf() - try { - val cursor = db.rawQuery(""" - SELECT DISTINCT TC.category - FROM PluginTooltipCategories TC - INNER JOIN PluginTracking PTT ON TC.id = PTT.categoryId - WHERE PTT.pluginId = ? - """, arrayOf(pluginId)) - - while (cursor.moveToNext()) { - categories.add(cursor.getString(0)) - } - cursor.close() - } catch (e: Exception) { - Log.e(TAG, "Failed to get plugin categories", e) - } finally { - db.close() - } - - categories + db.insert("TooltipButtons", null, values) } /** - * Check if the plugin documentation database exists and is accessible. + * Check if the plugin documentation database is accessible. */ suspend fun isDatabaseAvailable(): Boolean = withContext(Dispatchers.IO) { - val dbPath = context.getDatabasePath(databaseName).absolutePath - if (!File(dbPath).exists()) { - return@withContext false - } - - try { - val db = SQLiteDatabase.openDatabase(dbPath, null, SQLiteDatabase.OPEN_READONLY) - db.close() - true - } catch (e: Exception) { - Log.e(TAG, "Database exists but cannot be opened", e) - false - } + context.getDatabasePath(databaseName).exists() } /** - * Check if documentation for a specific plugin exists in the database. + * Check if documentation for a specific plugin exists in documentation.db. */ suspend fun isPluginDocumentationInstalled(pluginId: String): Boolean = withContext(Dispatchers.IO) { val db = getPluginDatabase() ?: return@withContext false try { - val cursor = db.query( - "PluginTracking", - arrayOf("COUNT(*) as count"), - "pluginId = ?", - arrayOf(pluginId), - null, null, null + val cursor = db.rawQuery( + "SELECT COUNT(*) FROM TooltipCategories WHERE category = ?", + arrayOf(pluginCategory(pluginId)) ) - - val hasDocumentation = if (cursor.moveToFirst()) { - cursor.getInt(0) > 0 - } else { - false - } + val installed = cursor.moveToFirst() && cursor.getInt(0) > 0 cursor.close() - hasDocumentation + installed } catch (e: Exception) { + if (e is CancellationException) throw e Log.e(TAG, "Failed to check plugin documentation for $pluginId", e) false } finally { @@ -493,20 +283,17 @@ class PluginDocumentationManager(private val context: Context) { /** * Verify and recreate plugin documentation if missing. - * This should be called when a plugin is loaded to ensure its documentation is available. */ suspend fun verifyAndRecreateDocumentation( pluginId: String, plugin: DocumentationExtension ): Boolean = withContext(Dispatchers.IO) { if (!isDatabaseAvailable()) { - Log.d(TAG, "Documentation database does not exist, initializing for $pluginId...") - initialize() + Log.d(TAG, "documentation.db not available yet for $pluginId, skipping") + return@withContext false } - val isInstalled = isPluginDocumentationInstalled(pluginId) - - if (!isInstalled) { + if (!isPluginDocumentationInstalled(pluginId)) { Log.d(TAG, "Plugin documentation missing for $pluginId, recreating...") return@withContext installPluginDocumentation(pluginId, plugin) } @@ -517,18 +304,15 @@ class PluginDocumentationManager(private val context: Context) { /** * Verify and recreate documentation for all plugins that support it. - * This can be called after database updates or on app startup. */ suspend fun verifyAllPluginDocumentation( plugins: Map ): Int = withContext(Dispatchers.IO) { - if (plugins.isEmpty()) { - return@withContext 0 - } + if (plugins.isEmpty()) return@withContext 0 if (!isDatabaseAvailable()) { - Log.d(TAG, "Documentation database does not exist, initializing...") - initialize() + Log.d(TAG, "documentation.db not available yet, skipping verification") + return@withContext 0 } var recreatedCount = 0 @@ -542,6 +326,7 @@ class PluginDocumentationManager(private val context: Context) { } } } catch (e: Exception) { + if (e is CancellationException) throw e Log.e(TAG, "Failed to verify/recreate documentation for $pluginId", e) } } @@ -552,4 +337,4 @@ class PluginDocumentationManager(private val context: Context) { recreatedCount } -} \ No newline at end of file +} 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 181325c6e4..223c0219c4 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 @@ -2,71 +2,51 @@ package com.itsaky.androidide.plugins.manager.services import android.content.Context import android.view.View -import com.itsaky.androidide.plugins.manager.tooltip.PluginTooltipManager +import com.itsaky.androidide.idetooltips.TooltipManager +import com.itsaky.androidide.plugins.manager.core.PluginManager import com.itsaky.androidide.plugins.services.IdeTooltipService /** * Implementation of the tooltip service for plugins. - * Provides a clean API for plugins to show tooltips. + * Delegates to the main TooltipManager, using "plugin_" as the category + * so plugin entries are stored alongside built-in documentation in documentation.db + * without conflicting with them. */ class IdeTooltipServiceImpl( - private val context: Context + private val context: Context, + private val pluginId: String, + private val activityProvider: PluginManager.ActivityProvider? ) : IdeTooltipService { - private val themedContext: Context by lazy { - // Wrap the context with a proper Material theme to ensure tooltip inflation works - try { - androidx.appcompat.view.ContextThemeWrapper( - context, - com.google.android.material.R.style.Theme_Material3_DynamicColors_DayNight - ) - } catch (e: Exception) { - // Fallback to regular Material theme if dynamic colors not available - try { - androidx.appcompat.view.ContextThemeWrapper( - context, - com.google.android.material.R.style.Theme_Material3_DayNight - ) - } catch (e: Exception) { - context - } - } - } + private val pluginCategory = "plugin_$pluginId" - companion object { - private const val LOG_TAG = "IdeTooltipService" - // Default category for plugin tooltips if not specified - private const val DEFAULT_PLUGIN_CATEGORY = "plugin" + /** + * Returns a context suitable for inflating the tooltip layout. + * Prefers the live Activity (correct Material3 theme + dark mode configuration). + * Falls back to the app context. + */ + private fun resolvedContext(): Context { + activityProvider?.getCurrentActivity()?.let { activity -> + if (!activity.isFinishing && !activity.isDestroyed) return activity + } + return context } override fun showTooltip(anchorView: View, category: String, tag: String) { try { - // Use the PluginTooltipManager for isolated plugin documentation - PluginTooltipManager.showTooltip( - context = themedContext, - anchorView = anchorView, - category = category, - tag = tag - ) + TooltipManager.showTooltip(resolvedContext(), anchorView, pluginCategory, tag) } catch (e: android.view.InflateException) { - android.util.Log.e(LOG_TAG, "Failed to inflate tooltip layout: $category.$tag", e) + android.util.Log.e("IdeTooltipService", "Failed to inflate tooltip layout: $pluginCategory.$tag", e) } catch (e: Exception) { - android.util.Log.e(LOG_TAG, "Failed to show tooltip: $category.$tag", e) + android.util.Log.e("IdeTooltipService", "Failed to show tooltip: $pluginCategory.$tag", e) } } override fun showTooltip(anchorView: View, tag: String) { try { - // Use the PluginTooltipManager with default plugin category - PluginTooltipManager.showTooltip( - context = themedContext, - anchorView = anchorView, - category = DEFAULT_PLUGIN_CATEGORY, - tag = tag - ) + TooltipManager.showTooltip(resolvedContext(), anchorView, pluginCategory, tag) } catch (e: Exception) { - android.util.Log.e(LOG_TAG, "Failed to show tooltip: $tag", e) + android.util.Log.e("IdeTooltipService", "Failed to show tooltip: $pluginCategory.$tag", e) } } - -} \ No newline at end of file +} diff --git a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/tooltip/PluginTooltipManager.kt b/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/tooltip/PluginTooltipManager.kt deleted file mode 100644 index 84d9de9d73..0000000000 --- a/plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/tooltip/PluginTooltipManager.kt +++ /dev/null @@ -1,452 +0,0 @@ -package com.itsaky.androidide.plugins.manager.tooltip - -import android.annotation.SuppressLint -import android.app.Activity -import android.content.Context -import android.database.sqlite.SQLiteDatabase -import android.graphics.Color -import android.text.Html -import android.util.Log -import android.view.Gravity -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.content.ContextWrapper -import android.webkit.WebView -import android.webkit.WebViewClient -import android.widget.ImageButton -import android.widget.PopupWindow -import android.widget.TextView -import androidx.core.content.ContextCompat.getColor -import androidx.core.graphics.drawable.toDrawable -import com.google.android.material.color.MaterialColors -import com.itsaky.androidide.idetooltips.IDETooltipItem -import com.itsaky.androidide.utils.isSystemInDarkMode -import com.itsaky.androidide.utils.toCssHex -import com.itsaky.androidide.idetooltips.R -import com.itsaky.androidide.resources.R as ResR -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.io.File - -/** - * Manages tooltips specifically for plugins. - * This is isolated from the main app's TooltipManager to prevent - * conflicts when the app's documentation database schema changes. - */ -object PluginTooltipManager { - private const val TAG = "PluginTooltipManager" - - private const val QUERY_TOOLTIP = """ - SELECT T.rowid, T.id, T.summary, T.detail - FROM PluginTooltips AS T, PluginTooltipCategories AS TC - WHERE T.categoryId = TC.id - AND T.tag = ? - AND TC.category = ? - """ - - private const val QUERY_TOOLTIP_BUTTONS = """ - SELECT description, uri - FROM PluginTooltipButtons - WHERE tooltipId = ? - ORDER BY buttonNumberId - """ - - /** - * Get the plugin documentation database path. - */ - private fun getPluginDatabasePath(context: Context): String { - return context.getDatabasePath("plugin_documentation.db").absolutePath - } - - - - /** - * Retrieve a tooltip from the plugin documentation database. - */ - suspend fun getTooltip(context: Context, category: String, tag: String): IDETooltipItem? { - Log.d(TAG, "Getting tooltip for category='$category', tag='$tag'") - - return withContext(Dispatchers.IO) { - val dbPath = getPluginDatabasePath(context) - - // Check if database exists - if (!File(dbPath).exists()) { - Log.w(TAG, "Plugin documentation database does not exist at: $dbPath") - return@withContext null - } - - var rowId: Int - var tooltipId: Int - var summary: String - var detail: String - val buttons = ArrayList>() - - try { - val db = SQLiteDatabase.openDatabase(dbPath, null, SQLiteDatabase.OPEN_READONLY) - - // Query tooltip - var cursor = db.rawQuery(QUERY_TOOLTIP, arrayOf(tag, category)) - - when (cursor.count) { - 0 -> { - Log.w(TAG, "No tooltip found for category='$category', tag='$tag'") - cursor.close() - db.close() - return@withContext null - } - - 1 -> { - // Expected case, continue processing - } - - else -> { - Log.e( - TAG, - "Multiple tooltips found for category='$category', tag='$tag' (found ${cursor.count} rows)" - ) - cursor.close() - db.close() - return@withContext null - } - } - - cursor.moveToFirst() - - rowId = cursor.getInt(0) - tooltipId = cursor.getInt(1) - summary = cursor.getString(2) - detail = cursor.getString(3) ?: "" - - cursor.close() - - // Query buttons - cursor = db.rawQuery(QUERY_TOOLTIP_BUTTONS, arrayOf(tooltipId.toString())) - - while (cursor.moveToNext()) { - buttons.add( - Pair( - cursor.getString(0), - "http://localhost:6174/" + cursor.getString(1) - ) - ) - } - - Log.d(TAG, "Retrieved ${buttons.size} buttons for tooltip $tooltipId") - - cursor.close() - db.close() - - } catch (e: Exception) { - Log.e(TAG, "Error getting tooltip for category='$category', tag='$tag'", e) - return@withContext null - } - - IDETooltipItem( - rowId = rowId, - id = tooltipId, - category = category, - tag = tag, - summary = summary, - detail = detail, - buttons = buttons, - lastChange = "Plugin Documentation" - ) - } - } - - /** - * Show a tooltip for a plugin. - */ - fun showTooltip(context: Context, anchorView: View, category: String, tag: String) { - CoroutineScope(Dispatchers.Main).launch { - val tooltipItem = getTooltip(context, category, tag) - - if (tooltipItem != null) { - showPluginTooltip( - context = context, - anchorView = anchorView, - level = 0, - tooltipItem = tooltipItem - ) - } else { - Log.e(TAG, "Failed to retrieve tooltip for $category.$tag") - } - } - } - - /** - * Show the plugin tooltip popup. - */ - private fun showPluginTooltip( - context: Context, - anchorView: View, - level: Int, - tooltipItem: IDETooltipItem - ) { - setupAndShowTooltipPopup( - context = context, - anchorView = anchorView, - level = level, - tooltipItem = tooltipItem, - onActionButtonClick = { popupWindow, urlContent -> - popupWindow.dismiss() - // Handle button click - could open documentation or perform action - Log.d(TAG, "Button clicked: ${urlContent.first}") - }, - onSeeMoreClicked = { popupWindow, nextLevel, item -> - popupWindow.dismiss() - showPluginTooltip(context, anchorView, nextLevel, item) - } - ) - } - - private tailrec fun Context.findActivity(): Activity? = when (this) { - is Activity -> this - is ContextWrapper -> baseContext.findActivity() - else -> null - } - - private fun canShowPopup(context: Context, view: View): Boolean { - if (!view.isAttachedToWindow || view.windowToken == null) { - return false - } - - val activity = context.findActivity() ?: view.context.findActivity() - return activity == null || (!activity.isFinishing && !activity.isDestroyed) - } - - /** - * Internal helper to create and show the tooltip popup window. - */ - @SuppressLint("SetJavaScriptEnabled") - private fun setupAndShowTooltipPopup( - context: Context, - anchorView: View, - level: Int, - showFeedBackIcon: Boolean = false, - tooltipItem: IDETooltipItem, - onActionButtonClick: (popupWindow: PopupWindow, url: Pair) -> Unit, - onSeeMoreClicked: (popupWindow: PopupWindow, nextLevel: Int, tooltipItem: IDETooltipItem) -> Unit, - ) { - if (!canShowPopup(context, anchorView)) { - Log.w(TAG, "Cannot show tooltip: activity destroyed or view detached") - return - } - - val inflater = LayoutInflater.from(context) - val popupView = inflater.inflate(R.layout.ide_tooltip_window, null) - val popupWindow = PopupWindow( - popupView, - ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - - val seeMore = popupView.findViewById(R.id.see_more) - val webView = popupView.findViewById(R.id.webview) - - val isDarkMode = context.isSystemInDarkMode() - val bodyColorHex = - getColor( - context, - if (isDarkMode) ResR.color.tooltip_text_color_dark - else ResR.color.tooltip_text_color_light, - ).toCssHex() - val linkColorHex = - getColor( - context, - if (isDarkMode) ResR.color.brand_color - else ResR.color.tooltip_link_color_light, - ).toCssHex() - - val tooltipHtmlContent = when (level) { - 0 -> tooltipItem.summary - 1 -> { - val detailContent = tooltipItem.detail.ifBlank { "" } - if (tooltipItem.buttons.isNotEmpty()) { - val buttonsSeparator = context.getString(R.string.tooltip_buttons_separator) - val linksHtml = tooltipItem.buttons.joinToString(buttonsSeparator) { (label, url) -> - context.getString(R.string.tooltip_links_html_template, url, linkColorHex, label) - } - if (detailContent.isNotBlank()) { - context.getString(R.string.tooltip_detail_links_template, detailContent, linksHtml) - } else { - linksHtml - } - } else { - detailContent - } - } - - else -> "" - } - - Log.d(TAG, "Level: $level, Content: ${tooltipHtmlContent.take(100)}...") - - val styledHtml = - context.getString(R.string.tooltip_html_template, bodyColorHex, tooltipHtmlContent, linkColorHex) - - webView.webViewClient = object : WebViewClient() { - override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean { - url?.let { clickedUrl -> - popupWindow.dismiss() - // Find the button label for this URL to use as title - val buttonLabel = tooltipItem.buttons.find { it.second == clickedUrl }?.first - ?: tooltipItem.tag - onActionButtonClick(popupWindow, Pair(clickedUrl, buttonLabel)) - } - return true - } - } - - webView.settings.javaScriptEnabled = true - webView.setBackgroundColor(Color.TRANSPARENT) - webView.loadDataWithBaseURL(null, styledHtml, "text/html", "UTF-8", null) - - seeMore.setOnClickListener { - popupWindow.dismiss() - val nextLevel = when { - level == 0 -> 1 - else -> level + 1 - } - Log.d(TAG, "See More clicked: level $level -> $nextLevel") - onSeeMoreClicked(popupWindow, nextLevel, tooltipItem) - } - - val shouldShowSeeMore = when { - level == 0 && (tooltipItem.detail.isNotBlank() || tooltipItem.buttons.isNotEmpty()) -> true - else -> false - } - seeMore.visibility = if (shouldShowSeeMore) View.VISIBLE else View.GONE - - val transparentColor = getColor(context, android.R.color.transparent) - popupWindow.setBackgroundDrawable(transparentColor.toDrawable()) - popupView.setBackgroundResource(R.drawable.idetooltip_popup_background) - - popupWindow.isFocusable = true - popupWindow.isOutsideTouchable = true - popupWindow.showAtLocation(anchorView, Gravity.CENTER, 0, 0) - - val infoButton = popupView.findViewById(R.id.icon_info) - val feedbackButton = popupView.findViewById(R.id.feedback_button) - feedbackButton.visibility = if (showFeedBackIcon) View.VISIBLE else View.INVISIBLE - if (infoButton != null) { - // Make the info icon fully visible - infoButton.alpha = 1.0f - - // Apply a darker tint color to make it more visible - val tintColor = MaterialColors.getColor( - context, - com.google.android.material.R.attr.colorOnSurfaceVariant, - "Color attribute not found in theme" - ) - infoButton.setColorFilter(tintColor) - - infoButton.setOnClickListener { - onInfoButtonClicked(context, anchorView, popupWindow, tooltipItem) - } - } - } - - /** - * Handles the click on the info icon in the tooltip. - */ - private fun onInfoButtonClicked( - context: Context, - anchorView: View, - popupWindow: PopupWindow, - tooltip: IDETooltipItem - ) { - // Dismiss the current tooltip popup - popupWindow.dismiss() - - // Show debug info in a new popup window after a short delay - anchorView.postDelayed({ - showDebugInfoPopup(context, anchorView, tooltip) - }, 100) - } - - /** - * Shows debug info in a popup window similar to the tooltip. - */ - private fun showDebugInfoPopup( - context: Context, - anchorView: View, - tooltip: IDETooltipItem - ) { - if (!canShowPopup(context, anchorView)) { - Log.w(TAG, "Cannot show debug info popup: activity destroyed or view detached") - return - } - - val inflater = LayoutInflater.from(context) - val popupView = inflater.inflate(R.layout.ide_tooltip_window, null) - val debugPopup = PopupWindow( - popupView, - ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - - // Hide the see more button and info icon for debug popup - popupView.findViewById(R.id.see_more)?.visibility = View.GONE - popupView.findViewById(R.id.icon_info)?.visibility = View.GONE - - // Get the WebView to display debug info - val webView = popupView.findViewById(R.id.webview) - - val isDarkMode = context.isSystemInDarkMode() - val bodyColorHex = - getColor( - context, - if (isDarkMode) ResR.color.tooltip_text_color_dark - else ResR.color.tooltip_text_color_light, - ).toCssHex() - val linkColorHex = - getColor( - context, - if (isDarkMode) ResR.color.brand_color - else ResR.color.tooltip_link_color_light, - ).toCssHex() - - val buttonsFormatted = if (tooltip.buttons.isEmpty()) { - "None" - } else { - val separator = context.getString(R.string.tooltip_debug_button_separator) - tooltip.buttons.joinToString(separator) { - context.getString(R.string.tooltip_debug_button_item_template_simple, it.first) - } - } - - val debugHtml = context.getString( - R.string.tooltip_debug_plugin_html, - tooltip.lastChange, - tooltip.rowId, - tooltip.id, - tooltip.category, - tooltip.tag, - Html.escapeHtml(tooltip.summary), - Html.escapeHtml(tooltip.detail), - buttonsFormatted - ) - - val styledHtml = context.getString(R.string.tooltip_html_template, bodyColorHex, debugHtml, linkColorHex) - - webView.settings.javaScriptEnabled = false // No need for JS in debug view - webView.setBackgroundColor(Color.TRANSPARENT) - webView.loadDataWithBaseURL(null, styledHtml, "text/html", "UTF-8", null) - - // Set popup properties - val transparentColor = getColor(context, android.R.color.transparent) - debugPopup.setBackgroundDrawable(transparentColor.toDrawable()) - popupView.setBackgroundResource(R.drawable.idetooltip_popup_background) - - debugPopup.isFocusable = true - debugPopup.isOutsideTouchable = true - - // Show the debug popup - debugPopup.showAtLocation(anchorView, Gravity.CENTER, 0, 0) - } - -} \ No newline at end of file