Fix tooltip labels

This commit is contained in:
downthecrop 2025-11-17 11:28:23 -08:00
parent 5bb81c7bd3
commit 5240963743
7 changed files with 135 additions and 156 deletions

View file

@ -4,105 +4,85 @@ import KondoKit.pluginmanager.GitLabPlugin
import KondoKit.pluginmanager.PluginProperties import KondoKit.pluginmanager.PluginProperties
import KondoKit.util.HttpFetcher import KondoKit.util.HttpFetcher
import KondoKit.pluginmanager.GitLabConfig import KondoKit.pluginmanager.GitLabConfig
import KondoKit.util.HttpStatusException
import KondoKit.util.JsonParser import KondoKit.util.JsonParser
import com.google.gson.JsonObject import com.google.gson.JsonObject
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.util.concurrent.* import java.util.concurrent.*
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
object GitLabPluginFetcher { object GitLabPluginFetcher {
private const val TAG = "GitLabPluginFetcher"
// Thread pool for concurrent plugin property fetching // Thread pool for concurrent plugin property fetching
private val executorService = Executors.newFixedThreadPool(5) private val executorService = Executors.newFixedThreadPool(5)
// Debug logging function // Debug logging function
private fun debugLog(message: String) { private fun debugLog(message: String) = PluginLogger.debug(TAG, message)
if (GitLabConfig.DEBUG) {
println("[GitLabPluginFetcher] $message")
}
}
// Function to fetch plugins from GitLab // Function to fetch plugins from GitLab
fun fetchGitLabPlugins(onComplete: (List<GitLabPlugin>) -> Unit) { fun fetchGitLabPlugins(onComplete: (List<GitLabPlugin>) -> Unit) {
Thread { Thread {
val plugins = mutableListOf<GitLabPlugin>()
try { try {
debugLog("Starting to fetch GitLab plugins...") debugLog("Starting to fetch GitLab plugins...")
val plugins = mutableListOf<GitLabPlugin>()
val apiUrl = GitLabConfig.getRepositoryTreeUrl() val apiUrl = GitLabConfig.getRepositoryTreeUrl()
debugLog("API URL: $apiUrl") debugLog("API URL: $apiUrl")
val connection = HttpFetcher.openGetConnection(apiUrl) val response = HttpFetcher.fetchString(apiUrl)
debugLog("Response length: ${response.length} characters")
// Read response debugLog("Response preview: ${response.take(200)}...")
val responseCode = connection.responseCode
debugLog("Response code: $responseCode") // Parse JSON response
val treeItems = JsonParser.fromJson(response, Array<JsonObject>::class.java)
if (responseCode == HttpURLConnection.HTTP_OK) { debugLog("Parsed ${treeItems.size} items from JSON")
val reader = BufferedReader(InputStreamReader(connection.inputStream))
val response = reader.use { it.readText() } // Filter for directories (trees) with mode "040000"
debugLog("Response length: ${response.length} characters") val pluginDirectories = mutableListOf<Pair<String, String>>()
debugLog("Response preview: ${response.take(200)}...") for (jsonObject in treeItems) {
if (jsonObject["type"].asString == "tree" && jsonObject["mode"].asString == "040000") {
// Parse JSON response val folderId = jsonObject["id"].asString
val treeItems = JsonParser.fromJson(response, Array<JsonObject>::class.java) val folderPath = jsonObject["path"].asString
debugLog("Parsed ${treeItems.size} items from JSON") pluginDirectories.add(folderId to folderPath)
debugLog("Found directory: $folderPath (ID: $folderId)")
// Filter for directories (trees) with mode "040000"
val pluginDirectories = mutableListOf<Pair<String, String>>()
for (jsonObject in treeItems) {
if (jsonObject["type"].asString == "tree" && jsonObject["mode"].asString == "040000") {
val folderId = jsonObject["id"].asString
val folderPath = jsonObject["path"].asString
pluginDirectories.add(folderId to folderPath)
debugLog("Found directory: $folderPath (ID: $folderId)")
}
} }
debugLog("Found ${pluginDirectories.size} plugin directories") }
debugLog("Found ${pluginDirectories.size} plugin directories")
// Fetch plugin properties in parallel
val pluginFutures = mutableListOf<Future<GitLabPlugin>>() // Fetch plugin properties in parallel
for ((folderId, folderPath) in pluginDirectories) { val pluginFutures = mutableListOf<Future<GitLabPlugin>>()
val future = executorService.submit(Callable<GitLabPlugin> { for ((folderId, folderPath) in pluginDirectories) {
try { val future = executorService.submit(Callable<GitLabPlugin> {
val pluginProperties = fetchPluginProperties(folderPath)
debugLog("Successfully fetched properties for: $folderPath")
GitLabPlugin(folderId, folderPath, pluginProperties, null)
} catch (e: Exception) {
debugLog("Error fetching plugin.properties for $folderPath: ${e.message}")
GitLabPlugin(folderId, folderPath, null, "Error fetching plugin.properties")
}
})
pluginFutures.add(future)
}
// Collect results
for (future in pluginFutures) {
try { try {
plugins.add(future.get()) val pluginProperties = fetchPluginProperties(folderPath)
debugLog("Successfully fetched properties for: $folderPath")
GitLabPlugin(folderId, folderPath, pluginProperties, null)
} catch (e: Exception) { } catch (e: Exception) {
debugLog("Error getting plugin result: ${e.message}") debugLog("Error fetching plugin.properties for $folderPath: ${e.message}")
GitLabPlugin(folderId, folderPath, null, "Error fetching plugin.properties")
} }
})
pluginFutures.add(future)
}
// Collect results
for (future in pluginFutures) {
try {
plugins.add(future.get())
} catch (e: Exception) {
debugLog("Error getting plugin result: ${e.message}")
} }
} else {
debugLog("HTTP error: $responseCode")
val errorReader = BufferedReader(InputStreamReader(connection.errorStream))
val errorResponse = errorReader.use { it.readText() }
debugLog("Error response: $errorResponse")
}
debugLog("Completed fetching plugins. Total plugins: ${plugins.size}")
// Update on UI thread
SwingUtilities.invokeLater {
onComplete(plugins)
} }
} catch (e: HttpStatusException) {
debugLog("HTTP error ${e.statusCode} fetching plugin tree: ${e.body.take(200)}")
} catch (e: Exception) { } catch (e: Exception) {
debugLog("Exception occurred: ${e.message}") debugLog("Exception occurred: ${e.message}")
e.printStackTrace() e.printStackTrace()
SwingUtilities.invokeLater { }
onComplete(emptyList())
} debugLog("Completed fetching plugins. Total plugins: ${plugins.size}")
// Update on UI thread
SwingUtilities.invokeLater {
onComplete(plugins)
} }
}.start() }.start()
} }
@ -113,25 +93,16 @@ object GitLabPluginFetcher {
val pluginUrl = GitLabConfig.getRawFileUrl(pluginFilePath) val pluginUrl = GitLabConfig.getRawFileUrl(pluginFilePath)
debugLog("Fetching plugin.properties from: $pluginUrl") debugLog("Fetching plugin.properties from: $pluginUrl")
val connection = HttpFetcher.openGetConnection(pluginUrl) try {
val rawContent = HttpFetcher.fetchString(pluginUrl)
// Read response
val responseCode = connection.responseCode
debugLog("plugin.properties response code: $responseCode")
if (responseCode == HttpURLConnection.HTTP_OK) {
val reader = BufferedReader(InputStreamReader(connection.inputStream))
val rawContent = reader.use { it.readText() }
debugLog("plugin.properties content length: ${rawContent.length} characters") debugLog("plugin.properties content length: ${rawContent.length} characters")
debugLog("plugin.properties content preview: ${rawContent.take(200)}...") debugLog("plugin.properties content preview: ${rawContent.take(200)}...")
return parseProperties(rawContent) return parseProperties(rawContent)
} else { } catch (e: HttpStatusException) {
debugLog("Failed to fetch plugin.properties. Response code: $responseCode") debugLog("Failed to fetch plugin.properties. Response code: ${e.statusCode}")
val errorReader = BufferedReader(InputStreamReader(connection.errorStream)) debugLog("Error response for plugin.properties: ${e.body.take(200)}")
val errorResponse = errorReader.use { it.readText() } throw Exception("Plugin file not found for folder: $folderPath", e)
debugLog("Error response for plugin.properties: $errorResponse")
throw Exception("Plugin file not found for folder: $folderPath")
} }
} }

View file

@ -18,6 +18,7 @@ import javax.swing.SwingUtilities
* Manages downloading and installing plugins from GitLab with concurrent support * Manages downloading and installing plugins from GitLab with concurrent support
*/ */
object PluginDownloadManager { object PluginDownloadManager {
private const val TAG = "PluginDownloadManager"
private const val MAX_CONCURRENT_DOWNLOADS = 3 private const val MAX_CONCURRENT_DOWNLOADS = 3
// Thread pool for concurrent downloads // Thread pool for concurrent downloads
@ -30,12 +31,8 @@ object PluginDownloadManager {
} }
// Debug logging function // Debug logging function
private fun debugLog(message: String) { private fun debugLog(message: String) = PluginLogger.debug(TAG, message)
if (GitLabConfig.DEBUG) {
println("[PluginDownloadManager] $message")
}
}
// Get download URL for debugging/logging purposes // Get download URL for debugging/logging purposes
fun getDownloadUrlForLogging(plugin: GitLabPlugin): String { fun getDownloadUrlForLogging(plugin: GitLabPlugin): String {
return GitLabConfig.getArchiveUrl(plugin.path) return GitLabConfig.getArchiveUrl(plugin.path)

View file

@ -0,0 +1,13 @@
package KondoKit.pluginmanager
/**
* Shared lightweight logger so pluginmanager classes don't each redefine
* their own debug helper tied to GitLabConfig.DEBUG.
*/
object PluginLogger {
fun debug(tag: String, message: String) {
if (GitLabConfig.DEBUG) {
println("[$tag] $message")
}
}
}

View file

@ -435,50 +435,50 @@ class SettingsPanel(private val plugin: Plugin) : JPanel() {
companion object { companion object {
var customToolTipWindow: JWindow? = null var customToolTipWindow: JWindow? = null
private var customToolTipLabel: JLabel? = null
fun showCustomToolTip(text: String, component: JComponent) { fun showCustomToolTip(text: String, component: JComponent) {
val tooltipFont = ViewConstants.FONT_RUNESCAPE_SMALL_PLAIN_16 val tooltipFont = ViewConstants.FONT_RUNESCAPE_SMALL_PLAIN_16
val maxWidth = 150 val maxWidth = 150
// Create a dummy JLabel to get FontMetrics for the font used in the tooltip val bgColor = Helpers.colorToHex(KondoKit.plugin.Companion.TOOLTIP_BACKGROUND)
val dummyLabel = JLabel() val textColor = Helpers.colorToHex(KondoKit.plugin.Companion.secondaryColor)
dummyLabel.font = tooltipFont // Constrain width via HTML so Swing's renderer wraps the text naturally.
val fontMetrics = dummyLabel.getFontMetrics(tooltipFont) val safeText = text
val lineHeight = fontMetrics.height .replace("&", "&amp;")
.replace("<", "&lt;")
// Calculate the approximate width of the text .replace(">", "&gt;")
val textWidth = fontMetrics.stringWidth(text) val html = """
<html>
// Calculate the number of lines required based on the text width and max tooltip width <body style="margin:0; padding:6px; width:${maxWidth}px; color:$textColor; background-color:$bgColor; word-wrap:break-word;">
val numberOfLines = Math.ceil(textWidth.toDouble() / maxWidth).toInt() $safeText
</body>
// Calculate the required height of the tooltip </html>
val requiredHeight = numberOfLines * lineHeight + 6 // Adding some padding """.trimIndent()
if (customToolTipWindow == null) { val (window, label) = if (customToolTipWindow == null || customToolTipLabel == null) {
customToolTipWindow = JWindow().apply { val lbl = JLabel().apply {
val bgColor = Helpers.colorToHex(KondoKit.plugin.Companion.TOOLTIP_BACKGROUND) border = BorderFactory.createLineBorder(Color.BLACK)
val textColor = Helpers.colorToHex(KondoKit.plugin.Companion.secondaryColor) isOpaque = true
contentPane = JLabel("<html><div style='color: $textColor; background-color: $bgColor; padding: 3px; word-break: break-all;'>$text</div></html>").apply { background = KondoKit.plugin.Companion.TOOLTIP_BACKGROUND
border = BorderFactory.createLineBorder(Color.BLACK) foreground = Color.WHITE
isOpaque = true font = tooltipFont
background = KondoKit.plugin.Companion.TOOLTIP_BACKGROUND horizontalAlignment = SwingConstants.LEFT
foreground = Color.WHITE verticalAlignment = SwingConstants.TOP
font = tooltipFont
maximumSize = Dimension(maxWidth, Int.MAX_VALUE)
preferredSize = Dimension(maxWidth, requiredHeight)
}
pack()
} }
val win = JWindow().apply { contentPane.add(lbl) }
customToolTipWindow = win
customToolTipLabel = lbl
win to lbl
} else { } else {
// Update the tooltip text customToolTipWindow!! to customToolTipLabel!!
val label = customToolTipWindow!!.contentPane as JLabel
val bgColor = Helpers.colorToHex(KondoKit.plugin.Companion.TOOLTIP_BACKGROUND)
val textColor = Helpers.colorToHex(KondoKit.plugin.Companion.secondaryColor)
label.text = "<html><div style='color: $textColor; background-color: $bgColor; padding: 3px; word-break: break-all;'>$text</div></html>"
label.preferredSize = Dimension(maxWidth, requiredHeight)
customToolTipWindow!!.pack()
} }
label.text = html
label.font = tooltipFont
label.preferredSize = null // let HTML + pack decide size from width constraint
window.pack()
// Position the tooltip near the component // Position the tooltip near the component
val locationOnScreen = component.locationOnScreen val locationOnScreen = component.locationOnScreen
customToolTipWindow!!.setLocation(locationOnScreen.x, locationOnScreen.y + 15) customToolTipWindow!!.setLocation(locationOnScreen.x, locationOnScreen.y + 15)

View file

@ -18,15 +18,23 @@ object GEPriceService {
val grand_exchange_price: String? val grand_exchange_price: String?
) )
private fun <T> mapToPriceMap(
items: Array<T>,
idSelector: (T) -> String?,
priceSelector: (T) -> String?
): Map<String, String> {
return items.mapNotNull { item ->
val id = idSelector(item)
val price = priceSelector(item)
if (id != null && price != null) id to price else null
}.toMap()
}
fun loadRemotePrices(): Map<String, String> { fun loadRemotePrices(): Map<String, String> {
return try { return try {
val payload = HttpFetcher.fetchString(REMOTE_GE_URL) val payload = HttpFetcher.fetchString(REMOTE_GE_URL)
val items = JsonParser.fromJson<Array<RemoteGEItem>>(payload) val items = JsonParser.fromJson<Array<RemoteGEItem>>(payload)
items.mapNotNull { item -> mapToPriceMap(items, { it.item_id }, { it.value })
val id = item.item_id
val value = item.value
if (id != null && value != null) id to value else null
}.toMap()
} catch (e: Exception) { } catch (e: Exception) {
emptyMap() emptyMap()
} }
@ -36,11 +44,7 @@ object GEPriceService {
return try { return try {
val json = jsonProvider() ?: return emptyMap() val json = jsonProvider() ?: return emptyMap()
val items = JsonParser.fromJson<Array<LocalGEItem>>(json) val items = JsonParser.fromJson<Array<LocalGEItem>>(json)
items.mapNotNull { item -> mapToPriceMap(items, { it.id }, { it.grand_exchange_price })
val id = item.id
val price = item.grand_exchange_price
if (id != null && price != null) id to price else null
}.toMap()
} catch (e: Exception) { } catch (e: Exception) {
emptyMap() emptyMap()
} }

View file

@ -15,6 +15,7 @@ import KondoKit.util.setFixedSize
import KondoKit.views.XPTrackerView.wrappedWidget import KondoKit.views.XPTrackerView.wrappedWidget
import KondoKit.ui.components.PopupMenuComponent import KondoKit.ui.components.PopupMenuComponent
import KondoKit.ui.components.ProgressBar import KondoKit.ui.components.ProgressBar
import KondoKit.ui.components.LabelComponent
import KondoKit.ui.components.WidgetPanel import KondoKit.ui.components.WidgetPanel
import KondoKit.plugin.Companion.TITLE_BAR_COLOR import KondoKit.plugin.Companion.TITLE_BAR_COLOR
import KondoKit.plugin.Companion.TOOLTIP_BACKGROUND import KondoKit.plugin.Companion.TOOLTIP_BACKGROUND
@ -171,12 +172,8 @@ object LootTrackerView : View, OnPostClientTickCallback, OnKillingBlowNPCCallbac
} }
} }
private fun createLabel(text: String): JLabel { private fun createLabel(text: String): LabelComponent =
return JLabel(text).apply { LabelComponent(text, alignment = JLabel.LEFT)
font = ViewConstants.FONT_RUNESCAPE_SMALL_16
horizontalAlignment = JLabel.LEFT
}
}
private fun addItemToLootPanel(lootTrackerView: JPanel, drop: Item, npcName: String) { private fun addItemToLootPanel(lootTrackerView: JPanel, drop: Item, npcName: String) {
findLootItemsPanel(lootTrackerView, npcName)?.let { lootPanel -> findLootItemsPanel(lootTrackerView, npcName)?.let { lootPanel ->

View file

@ -16,6 +16,7 @@ import KondoKit.util.attachPopupMenu
import KondoKit.util.XPTable import KondoKit.util.XPTable
import KondoKit.ui.components.PopupMenuComponent import KondoKit.ui.components.PopupMenuComponent
import KondoKit.ui.components.ProgressBar import KondoKit.ui.components.ProgressBar
import KondoKit.ui.components.LabelComponent
import KondoKit.ui.components.WidgetPanel import KondoKit.ui.components.WidgetPanel
import KondoKit.ui.components.XPWidget import KondoKit.ui.components.XPWidget
import KondoKit.plugin.Companion.IMAGE_SIZE import KondoKit.plugin.Companion.IMAGE_SIZE
@ -91,17 +92,13 @@ object XPTrackerView : View, OnUpdateCallback, OnXPUpdateCallback {
} }
} }
private fun createMetricLabel(title: String, initialValue: String = "0", topPadding: Int = 0): JLabel { private fun createMetricLabel(title: String, initialValue: String = "0", topPadding: Int = 0): LabelComponent =
return JLabel(formatHtmlLabelText("$title ", primaryColor, initialValue, secondaryColor)).apply { LabelComponent(alignment = JLabel.LEFT).apply {
horizontalAlignment = JLabel.LEFT updateHtmlText("$title ", primaryColor, initialValue, secondaryColor)
font = widgetFont
if (topPadding > 0) { if (topPadding > 0) {
border = BorderFactory.createEmptyBorder(topPadding, 0, 0, 0) border = BorderFactory.createEmptyBorder(topPadding, 0, 0, 0)
} else {
border = BorderFactory.createEmptyBorder(0, 0, 0, 0)
} }
} }
}
private fun getSkillIcon(skillId: Int): BufferedImage { private fun getSkillIcon(skillId: Int): BufferedImage {
return skillIconCache[skillId] ?: getBufferedImageFromSprite(API.GetSprite(getSpriteId(skillId))).also { return skillIconCache[skillId] ?: getBufferedImageFromSprite(API.GetSprite(getSpriteId(skillId))).also {