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")
debugLog("Response preview: ${response.take(200)}...")
// Read response // Parse JSON response
val responseCode = connection.responseCode val treeItems = JsonParser.fromJson(response, Array<JsonObject>::class.java)
debugLog("Response code: $responseCode") debugLog("Parsed ${treeItems.size} items from JSON")
if (responseCode == HttpURLConnection.HTTP_OK) { // Filter for directories (trees) with mode "040000"
val reader = BufferedReader(InputStreamReader(connection.inputStream)) val pluginDirectories = mutableListOf<Pair<String, String>>()
val response = reader.use { it.readText() } for (jsonObject in treeItems) {
debugLog("Response length: ${response.length} characters") if (jsonObject["type"].asString == "tree" && jsonObject["mode"].asString == "040000") {
debugLog("Response preview: ${response.take(200)}...") val folderId = jsonObject["id"].asString
val folderPath = jsonObject["path"].asString
// Parse JSON response pluginDirectories.add(folderId to folderPath)
val treeItems = JsonParser.fromJson(response, Array<JsonObject>::class.java) debugLog("Found directory: $folderPath (ID: $folderId)")
debugLog("Parsed ${treeItems.size} items from JSON")
// 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 // Fetch plugin properties in parallel
val pluginFutures = mutableListOf<Future<GitLabPlugin>>() val pluginFutures = mutableListOf<Future<GitLabPlugin>>()
for ((folderId, folderPath) in pluginDirectories) { for ((folderId, folderPath) in pluginDirectories) {
val future = executorService.submit(Callable<GitLabPlugin> { val future = executorService.submit(Callable<GitLabPlugin> {
try {
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")
} }
} })
} else { pluginFutures.add(future)
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}") // Collect results
// Update on UI thread for (future in pluginFutures) {
SwingUtilities.invokeLater { try {
onComplete(plugins) plugins.add(future.get())
} catch (e: Exception) {
debugLog("Error getting plugin result: ${e.message}")
}
} }
} 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,11 +31,7 @@ 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 {

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;")
.replace(">", "&gt;")
val html = """
<html>
<body style="margin:0; padding:6px; width:${maxWidth}px; color:$textColor; background-color:$bgColor; word-wrap:break-word;">
$safeText
</body>
</html>
""".trimIndent()
// Calculate the approximate width of the text val (window, label) = if (customToolTipWindow == null || customToolTipLabel == null) {
val textWidth = fontMetrics.stringWidth(text) val lbl = JLabel().apply {
border = BorderFactory.createLineBorder(Color.BLACK)
// Calculate the number of lines required based on the text width and max tooltip width isOpaque = true
val numberOfLines = Math.ceil(textWidth.toDouble() / maxWidth).toInt() background = KondoKit.plugin.Companion.TOOLTIP_BACKGROUND
foreground = Color.WHITE
// Calculate the required height of the tooltip font = tooltipFont
val requiredHeight = numberOfLines * lineHeight + 6 // Adding some padding horizontalAlignment = SwingConstants.LEFT
verticalAlignment = SwingConstants.TOP
if (customToolTipWindow == null) {
customToolTipWindow = JWindow().apply {
val bgColor = Helpers.colorToHex(KondoKit.plugin.Companion.TOOLTIP_BACKGROUND)
val textColor = Helpers.colorToHex(KondoKit.plugin.Companion.secondaryColor)
contentPane = JLabel("<html><div style='color: $textColor; background-color: $bgColor; padding: 3px; word-break: break-all;'>$text</div></html>").apply {
border = BorderFactory.createLineBorder(Color.BLACK)
isOpaque = true
background = KondoKit.plugin.Companion.TOOLTIP_BACKGROUND
foreground = Color.WHITE
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 {