package KondoKit import KondoKit.Helpers.formatHtmlLabelText import KondoKit.Helpers.formatNumber import KondoKit.Helpers.getSpriteId import KondoKit.Helpers.showAlert import KondoKit.views.HiscoresView import KondoKit.views.LootTrackerView import KondoKit.views.ReflectiveEditorView import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite import KondoKit.Themes.Theme import KondoKit.Themes.ThemeType import KondoKit.Themes.getTheme import KondoKit.components.ProgressBar import KondoKit.components.ScrollablePanel import KondoKit.components.ToggleSwitch import KondoKit.components.* import KondoKit.views.XPTrackerView import KondoKit.plugin.StateManager.focusedView import KondoKit.views.Constants.COMBAT_LVL_SPRITE import KondoKit.views.HiscoresView.createHiscoreSearchView import KondoKit.views.HiscoresView.hiScoreView import KondoKit.views.LootTrackerView.BAG_ICON import KondoKit.views.LootTrackerView.createLootTrackerView import KondoKit.views.LootTrackerView.lootTrackerView import KondoKit.views.LootTrackerView.npcDeathSnapshots import KondoKit.views.LootTrackerView.onPostClientTick import KondoKit.views.LootTrackerView.takeGroundSnapshot import KondoKit.views.ReflectiveEditorView.addPlugins import KondoKit.views.ReflectiveEditorView.createReflectiveEditorView import KondoKit.views.ReflectiveEditorView.reflectiveEditorView import KondoKit.views.XPTrackerView.createXPTrackerView import KondoKit.views.XPTrackerView.createXPWidget import KondoKit.views.XPTrackerView.initialXP import KondoKit.views.XPTrackerView.resetXPTracker import KondoKit.views.XPTrackerView.totalXPWidget import KondoKit.views.XPTrackerView.updateWidget import KondoKit.views.XPTrackerView.wrappedWidget import KondoKit.views.XPTrackerView.xpTrackerView import KondoKit.views.XPTrackerView.xpWidgets import plugin.Plugin import plugin.api.* import plugin.api.API.* import plugin.api.FontColor.fromColor import rt4.* import rt4.DisplayMode import rt4.GameShell.canvas import rt4.GameShell.frame import rt4.client.js5Archive8 import rt4.client.mainLoadState import java.awt.* import java.awt.Font import java.awt.event.ActionListener import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import javax.swing.* @Target(AnnotationTarget.FIELD) @Retention(AnnotationRetention.RUNTIME) annotation class Exposed(val description: String = "") class plugin : Plugin() { companion object { val WIDGET_SIZE = Dimension(220, 50) val TOTAL_XP_WIDGET_SIZE = Dimension(220, 30) val IMAGE_SIZE = Dimension(25, 23) // Default Theme Colors var WIDGET_COLOR = Color(30, 30, 30) var TITLE_BAR_COLOR = Color(21, 21, 21) var VIEW_BACKGROUND_COLOR = Color(40, 40, 40) var primaryColor = Color(165, 165, 165) // Color for "XP Gained:" var secondaryColor = Color(255, 255, 255) // Color for "0" var POPUP_BACKGROUND = Color(45, 45, 45) var POPUP_FOREGROUND = Color(220, 220, 220) var TOOLTIP_BACKGROUND = Color(50,50,50) var SCROLL_BAR_COLOR = Color(64, 64, 64) var PROGRESS_BAR_FILL = Color(61, 56, 49) var NAV_TINT: Color? = null var NAV_GREYSCALE = false var BOOST = 1f var appliedTheme = ThemeType.RUNELITE @Exposed("Theme colors for KondoKit, requires a relaunch to apply.") var theme = ThemeType.RUNELITE @Exposed("Default: true, Use Local JSON or the prices from the Live/Stable server API") var useLiveGEPrices = true @Exposed("Used to calculate Combat Actions until next level.") var playerXPMultiplier = 5 @Exposed("Start minimized/collapsed by default") var launchMinimized = false @Exposed("Default 16 on Windows, 0 Linux/macOS. If Kondo is not " + "perfectly snapped to the edge of the game due to window chrome you can update this to fix it") var uiOffset = 0 @Exposed("Stretched/Scaled Fixed Mode Support") var useScaledFixed = false const val FIXED_WIDTH = 765 const val FIXED_HEIGHT = 503 private const val NAVBAR_WIDTH = 30 private const val MAIN_CONTENT_WIDTH = 242 private const val WRENCH_ICON = 907 private const val LOOT_ICON = 777 private const val MAG_SPRITE = 1423 const val LVL_ICON = 898 private lateinit var cardLayout: CardLayout private lateinit var mainContentPanel: JPanel private var rightPanelWrapper: JScrollPane? = null private var accumulatedTime = 0L private var reloadInterfaces = false private const val TICK_INTERVAL = 600L private var pluginsReloaded = false private var loginScreen = 160 private var lastLogin = "" private var initialized = false private var lastClickTime = 0L private var lastUIOffset = 0 private var themeName = "RUNELITE" private const val HIDDEN_VIEW = "HIDDEN" private var altCanvas: AltCanvas? = null private val drawActions = mutableListOf<() -> Unit>() fun registerDrawAction(action: () -> Unit) { synchronized(drawActions) { drawActions.add(action) } } } override fun Init() { // Disable Font AA System.setProperty("sun.java2d.opengl", "false") System.setProperty("awt.useSystemAAFontSettings", "off") System.setProperty("swing.aatext", "false") } override fun OnLogin() { if (lastLogin != "" && lastLogin != Player.usernameInput.toString()) { // if we logged in with a new character // we need to reset the trackers xpTrackerView?.let { resetXPTracker(it) } } lastLogin = Player.usernameInput.toString() } override fun OnMiniMenuCreate(currentEntries: Array?) { if (currentEntries != null) { for ((index, entry) in currentEntries.withIndex()) { if (entry.type == MiniMenuType.PLAYER && index == currentEntries.size - 1) { val input = entry.subject // Trim spaces, clean up tags, and remove the level info val cleanedInput = input .trim() // Remove any leading/trailing spaces .replace(Regex(""), "") // Remove color tags .replace(Regex(""), "") // Remove image tags .replace(Regex("\\(level: \\d+\\)"), "") // Remove level text e.g. (level: 44) .trim() // Trim again to remove extra spaces after removing level text // Proceed with the full cleaned username InsertMiniMenuEntry("Lookup", entry.subject, searchHiscore(cleanedInput)) } } } } override fun OnPluginsReloaded(): Boolean { if (!initialized) return true // Ensure Swing updates happen on the EDT to avoid flicker SwingUtilities.invokeLater { updateDisplaySettings() frame.remove(rightPanelWrapper) frame.layout = BorderLayout() rightPanelWrapper?.let { frame.add(it, BorderLayout.EAST) } frame.revalidate() frame.repaint() } pluginsReloaded = true reloadInterfaces = true return true } override fun OnXPUpdate(skillId: Int, xp: Int) { if (!initialXP.containsKey(skillId)) { initialXP[skillId] = xp return } val previousXpSnapshot = initialXP[skillId] ?: xp if (xp == initialXP[skillId]) return val ensureOnEdt = Runnable { var xpWidget = xpWidgets[skillId] if (xpWidget != null) { updateWidget(xpWidget, xp) } else { xpWidget = createXPWidget(skillId, previousXpSnapshot) xpWidgets[skillId] = xpWidget val wrapped = wrappedWidget(xpWidget.container) // Attach per-widget remove menu val popupMenu = XPTrackerView.removeXPWidgetMenu(wrapped, skillId) val rightClickListener = object : MouseAdapter() { override fun mousePressed(e: MouseEvent) { if (e.isPopupTrigger) popupMenu.show(e.component, e.x, e.y) } override fun mouseReleased(e: MouseEvent) { if (e.isPopupTrigger) popupMenu.show(e.component, e.x, e.y) } } Helpers.addMouseListenerToAll(wrapped, rightClickListener) wrapped.addMouseListener(rightClickListener) xpTrackerView?.add(wrapped) xpTrackerView?.add(Box.createVerticalStrut(5)) if(focusedView == XPTrackerView.VIEW_NAME) { xpTrackerView?.revalidate() xpTrackerView?.repaint() } updateWidget(xpWidget, xp) } } if (SwingUtilities.isEventDispatchThread()) { ensureOnEdt.run() } else { SwingUtilities.invokeLater(ensureOnEdt) } } override fun Draw(timeDelta: Long) { if (GlRenderer.enabled && GlRenderer.canvasWidth != GameShell.canvasWidth) { GlRenderer.canvasWidth = GameShell.canvasWidth GlRenderer.setViewportBounds(0, 0, GameShell.canvasWidth, GameShell.canvasHeight) } if (pluginsReloaded) { // Rebuild the reflective editor UI on the EDT and in one batch SwingUtilities.invokeLater { reflectiveEditorView?.let { addPlugins(it) } } pluginsReloaded = false } if (reloadInterfaces){ InterfaceList.method3712(true) // Gets the resize working correctly reloadInterfaces = false } accumulatedTime += timeDelta if (accumulatedTime >= TICK_INTERVAL) { lootTrackerView?.let { onPostClientTick(it) } accumulatedTime = 0L } // Draw synced actions (that require to be done between glBegin and glEnd) if (drawActions.isNotEmpty()) { synchronized(drawActions) { val actionsCopy = drawActions.toList() drawActions.clear() for (action in actionsCopy) { action() } } } // Init in the draw call so we know we are between glBegin and glEnd for HD if(!initialized && mainLoadState >= loginScreen) { initKondoUI() } } override fun LateDraw(timeDelta: Long) { if (!initialized) return if(GameShell.fullScreenFrame != null) { DisplayMode.setWindowMode(true, 0, FIXED_WIDTH, FIXED_HEIGHT) showAlert("Fullscreen is not supported by KondoKit. Disable the plugin first.", "Error", JOptionPane.INFORMATION_MESSAGE ) return } if(!useScaledFixed) return if(GetWindowMode() == WindowMode.FIXED){ moveAltCanvasToFront() } else { moveCanvasToFront() } altCanvas?.updateGameImage() // Update the game image as needed } override fun Update() { val widgets = xpWidgets.values val totalXP = totalXPWidget widgets.forEach { xpWidget -> val elapsedTime = (System.currentTimeMillis() - xpWidget.startTime) / 1000.0 / 60.0 / 60.0 val xpPerHour = if (elapsedTime > 0) (xpWidget.totalXpGained / elapsedTime).toInt() else 0 val formattedXpPerHour = formatNumber(xpPerHour) xpWidget.xpPerHourLabel.text = formatHtmlLabelText("XP /hr: ", primaryColor, formattedXpPerHour, secondaryColor) xpWidget.container.repaint() } totalXP?.let { totalXPWidget -> val elapsedTime = (System.currentTimeMillis() - totalXPWidget.startTime) / 1000.0 / 60.0 / 60.0 val totalXPPerHour = if (elapsedTime > 0) (totalXPWidget.totalXpGained / elapsedTime).toInt() else 0 val formattedTotalXpPerHour = formatNumber(totalXPPerHour) totalXPWidget.xpPerHourLabel.text = formatHtmlLabelText("XP /hr: ", primaryColor, formattedTotalXpPerHour, secondaryColor) totalXPWidget.container.repaint() } } override fun OnKillingBlowNPC(npcID: Int, x: Int, z: Int) { val preDeathSnapshot = takeGroundSnapshot(Pair(x,z)) npcDeathSnapshots[npcID] = LootTrackerView.GroundSnapshot(preDeathSnapshot, Pair(x, z), 0) } private fun allSpritesLoaded() : Boolean { // Check all skill sprites try{ for (i in 0 until 24) { if(!js5Archive8.isFileReady(getSpriteId(i))){ return false } } val otherIcons = arrayOf(LVL_ICON, MAG_SPRITE, LOOT_ICON, WRENCH_ICON, COMBAT_LVL_SPRITE, BAG_ICON) for (icon in otherIcons) { if(!js5Archive8.isFileReady(icon)){ return false } } } catch (e : Exception){ return false } return true } private fun updateDisplaySettings() { val mode = GetWindowMode() val currentScrollPaneWidth = if (mainContentPanel.isVisible) NAVBAR_WIDTH + MAIN_CONTENT_WIDTH else NAVBAR_WIDTH lastUIOffset = uiOffset if(mode != WindowMode.FIXED) { destroyAltCanvas() } else if (useScaledFixed && altCanvas == null) { initAltCanvas() } when (mode) { WindowMode.FIXED -> { if (frame.width < FIXED_WIDTH + currentScrollPaneWidth + uiOffset) { frame.setSize(FIXED_WIDTH + currentScrollPaneWidth + uiOffset, frame.height) } val difference = frame.width - (uiOffset + currentScrollPaneWidth) if (useScaledFixed) { GameShell.leftMargin = 0 val canvasWidth = difference + uiOffset / 2 val canvasHeight = frame.height - canvas.y // Restricting height to frame height altCanvas?.size = Dimension(canvasWidth, canvasHeight) altCanvas?.setLocation(0, canvas.y) canvas.setLocation(0, canvas.y) } else { val difference = frame.width - (FIXED_WIDTH + uiOffset + currentScrollPaneWidth) GameShell.leftMargin = difference / 2 } } WindowMode.RESIZABLE -> { GameShell.canvasWidth = frame.width - (currentScrollPaneWidth + uiOffset) } } rightPanelWrapper?.preferredSize = Dimension(currentScrollPaneWidth, frame.height) rightPanelWrapper?.isDoubleBuffered = true rightPanelWrapper?.revalidate() rightPanelWrapper?.repaint() } fun OnKondoValueUpdated(){ StoreData("kondoUseRemoteGE", useLiveGEPrices) StoreData("kondoTheme", theme.toString()) if(appliedTheme != theme) { showAlert( "KondoKit Theme changes require a relaunch.", "KondoKit", JOptionPane.INFORMATION_MESSAGE ) } StoreData("kondoPlayerXPMultiplier", playerXPMultiplier) LootTrackerView.gePriceMap = LootTrackerView.loadGEPrices() StoreData("kondoLaunchMinimized", launchMinimized) StoreData("kondoUIOffset", uiOffset) StoreData("kondoScaledFixed", useScaledFixed) if(lastUIOffset != uiOffset){ reloadInterfaces = true } updateDisplaySettings() } private fun initAltCanvas(){ if(GetWindowMode() != WindowMode.FIXED || altCanvas != null) return if (frame != null) { altCanvas = AltCanvas().apply { preferredSize = Dimension(FIXED_WIDTH, FIXED_HEIGHT) } altCanvas?.let { frame.add(it) } moveAltCanvasToFront() frame.setComponentZOrder(rightPanelWrapper, 2) } } private fun destroyAltCanvas(){ if (altCanvas == null) return moveCanvasToFront() frame.remove(altCanvas) altCanvas = null } private fun moveAltCanvasToFront(){ if (altCanvas == null) return frame.setComponentZOrder(canvas, 2) frame.setComponentZOrder(altCanvas, 1) frame.setComponentZOrder(rightPanelWrapper, 0) } private fun moveCanvasToFront(){ if (altCanvas == null) return frame.setComponentZOrder(altCanvas, 2) frame.setComponentZOrder(canvas, 1) frame.setComponentZOrder(rightPanelWrapper, 0) } private fun searchHiscore(username: String): Runnable { return Runnable { setActiveView(HiscoresView.VIEW_NAME) val customSearchField = hiScoreView?.let { HiscoresView.CustomSearchField(it) } customSearchField?.searchPlayer(username) ?: run { println("searchView is null or CustomSearchField creation failed.") } } } private fun restoreSettings(){ themeName = (GetData("kondoTheme") as? String) ?: "RUNELITE" useLiveGEPrices = (GetData("kondoUseRemoteGE") as? Boolean) ?: true playerXPMultiplier = (GetData("kondoPlayerXPMultiplier") as? Int) ?: 5 val osName = System.getProperty("os.name").toLowerCase() uiOffset = (GetData("kondoUIOffset") as? Int) ?: if (osName.contains("win")) 16 else 0 launchMinimized = (GetData("kondoLaunchMinimized") as? Boolean) ?: false useScaledFixed = (GetData("kondoScaledFixed") as? Boolean) ?: false } private fun initKondoUI(){ DrawText(FontType.LARGE, fromColor(Color(16777215)), TextModifier.CENTER, "KondoKit Loading Sprites...", GameShell.canvasWidth/2, GameShell.canvasHeight/2) if(!allSpritesLoaded()) return val frame: Frame? = GameShell.frame if (frame != null) { restoreSettings() theme = ThemeType.valueOf(themeName) applyTheme(getTheme(theme)) appliedTheme = theme configureLookAndFeel() cardLayout = CardLayout() mainContentPanel = JPanel(cardLayout).apply { border = BorderFactory.createEmptyBorder(0, 0, 0, 0) // Removes any default border or padding background = VIEW_BACKGROUND_COLOR preferredSize = Dimension(MAIN_CONTENT_WIDTH, frame.height) isOpaque = true } // Register Views createXPTrackerView() createHiscoreSearchView() createLootTrackerView() createReflectiveEditorView() mainContentPanel.add(ScrollablePanel(xpTrackerView!!), XPTrackerView.VIEW_NAME) mainContentPanel.add(ScrollablePanel(hiScoreView!!), HiscoresView.VIEW_NAME) mainContentPanel.add(ScrollablePanel(lootTrackerView!!), LootTrackerView.VIEW_NAME) mainContentPanel.add(ScrollablePanel(reflectiveEditorView!!), ReflectiveEditorView.VIEW_NAME) val navPanel = Panel().apply { layout = BoxLayout(this, BoxLayout.Y_AXIS) background = WIDGET_COLOR preferredSize = Dimension(NAVBAR_WIDTH, frame.height) } navPanel.add(createNavButton(LVL_ICON, XPTrackerView.VIEW_NAME)) navPanel.add(createNavButton(MAG_SPRITE, HiscoresView.VIEW_NAME)) navPanel.add(createNavButton(LOOT_ICON, LootTrackerView.VIEW_NAME)) navPanel.add(createNavButton(WRENCH_ICON, ReflectiveEditorView.VIEW_NAME)) val rightPanel = Panel(BorderLayout()).apply { add(mainContentPanel, BorderLayout.CENTER) add(navPanel, BorderLayout.EAST) } rightPanelWrapper = JScrollPane(rightPanel).apply { preferredSize = Dimension(NAVBAR_WIDTH + MAIN_CONTENT_WIDTH, frame.height) background = VIEW_BACKGROUND_COLOR border = BorderFactory.createEmptyBorder() horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_NEVER verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_NEVER } val desiredView = if (launchMinimized) HIDDEN_VIEW else XPTrackerView.VIEW_NAME // Commit layout synchronously on the EDT to avoid initial misplacement val commit = Runnable { frame.layout = BorderLayout() rightPanelWrapper?.let { frame.add(it, BorderLayout.EAST) } setActiveView(desiredView) frame.validate() frame.repaint() } if (SwingUtilities.isEventDispatchThread()) { commit.run() } else { try { javax.swing.SwingUtilities.invokeAndWait(commit) } catch (e: Exception) { // Fallback to async if invokeAndWait fails for any reason SwingUtilities.invokeLater(commit) } } initialized = true pluginsReloaded = true } } private fun setActiveView(viewName: String) { val runUpdate: () -> Unit = { // Track visibility change to decide if we need to resize/reload interfaces val wasVisible = mainContentPanel.isVisible // Handle the visibility of the main content panel and card switch if (viewName == HIDDEN_VIEW) { mainContentPanel.isVisible = false } else { if (!mainContentPanel.isVisible) { mainContentPanel.isVisible = true } cardLayout.show(mainContentPanel, viewName) } val visibilityChanged = wasVisible != mainContentPanel.isVisible // Batch painting to avoid intermediate repaints rightPanelWrapper?.ignoreRepaint = true try { if (visibilityChanged) { // Only touch layout and client interfaces if width actually changes updateDisplaySettings() reloadInterfaces = true rightPanelWrapper?.revalidate() frame?.validate() } else { // Just a card switch; avoid full frame revalidate mainContentPanel.revalidate() } } finally { rightPanelWrapper?.ignoreRepaint = false } // Targeted repaint for snappy feedback if (visibilityChanged) { rightPanelWrapper?.repaint() frame?.repaint() } else { mainContentPanel.repaint() } focusedView = viewName } if (SwingUtilities.isEventDispatchThread()) { runUpdate() } else { SwingUtilities.invokeLater { runUpdate() } } } private fun createNavButton(spriteId: Int, viewName: String): JPanel { val bufferedImageSprite = getBufferedImageFromSprite(GetSprite(spriteId), NAV_TINT, NAV_GREYSCALE, BOOST) val buttonSize = Dimension(NAVBAR_WIDTH, 32) val imageSize = Dimension((bufferedImageSprite.width / 1.2f).toInt(), (bufferedImageSprite.height / 1.2f).toInt()) val cooldownDuration = 100L val actionListener = ActionListener { val currentTime = System.currentTimeMillis() if (currentTime - lastClickTime < cooldownDuration) { return@ActionListener } lastClickTime = currentTime if (focusedView == viewName) { setActiveView("HIDDEN") } else { setActiveView(viewName) } } // ImageCanvas with forced size val imageCanvas = ImageCanvas(bufferedImageSprite).apply { background = WIDGET_COLOR preferredSize = imageSize maximumSize = imageSize minimumSize = imageSize } // Wrapping the ImageCanvas in another JPanel to prevent stretching val imageCanvasWrapper = JPanel().apply { layout = GridBagLayout() // Keeps the layout of the wrapped panel minimal preferredSize = imageSize maximumSize = imageSize minimumSize = imageSize isOpaque = false // No background for the wrapper add(imageCanvas) // Adding ImageCanvas directly, layout won't stretch it } val panelButton = JPanel().apply { layout = GridBagLayout() preferredSize = buttonSize maximumSize = buttonSize minimumSize = buttonSize background = WIDGET_COLOR isOpaque = true val gbc = GridBagConstraints().apply { anchor = GridBagConstraints.CENTER fill = GridBagConstraints.NONE // Prevents stretching } add(imageCanvasWrapper, gbc) // Hover and click behavior val hoverListener = object : MouseAdapter() { override fun mouseEntered(e: MouseEvent?) { background = WIDGET_COLOR.darker() imageCanvas.fillColor = WIDGET_COLOR.darker() imageCanvas.repaint() repaint() } override fun mouseExited(e: MouseEvent?) { background = WIDGET_COLOR imageCanvas.fillColor = WIDGET_COLOR imageCanvas.repaint() repaint() } override fun mouseClicked(e: MouseEvent?) { actionListener.actionPerformed(null) } } addMouseListener(hoverListener) imageCanvas.addMouseListener(hoverListener) } return panelButton } private fun configureLookAndFeel(){ loadFont() try { UIManager.setLookAndFeel("javax.swing.plaf.nimbus.NimbusLookAndFeel") // Modify the UI properties to match theme UIManager.put("control", VIEW_BACKGROUND_COLOR) UIManager.put("info", VIEW_BACKGROUND_COLOR) UIManager.put("nimbusBase", WIDGET_COLOR) UIManager.put("nimbusBlueGrey", TITLE_BAR_COLOR) UIManager.put("nimbusDisabledText", primaryColor) UIManager.put("nimbusSelectedText", secondaryColor) UIManager.put("text", secondaryColor) UIManager.put("nimbusFocus", TITLE_BAR_COLOR) UIManager.put("nimbusInfoBlue", POPUP_BACKGROUND) UIManager.put("nimbusLightBackground", WIDGET_COLOR) UIManager.put("nimbusSelectionBackground", PROGRESS_BAR_FILL) UIManager.put("Button.background", WIDGET_COLOR) UIManager.put("Button.foreground", secondaryColor) UIManager.put("CheckBox.background", VIEW_BACKGROUND_COLOR) UIManager.put("CheckBox.foreground", secondaryColor) UIManager.put("CheckBox.icon", UIManager.getIcon("CheckBox.icon")) UIManager.put("ComboBox.background", WIDGET_COLOR) UIManager.put("ComboBox.foreground", secondaryColor) UIManager.put("ComboBox.selectionBackground", PROGRESS_BAR_FILL) UIManager.put("ComboBox.selectionForeground", primaryColor) UIManager.put("ComboBox.buttonBackground", WIDGET_COLOR) UIManager.put("Spinner.background", WIDGET_COLOR) UIManager.put("Spinner.foreground", secondaryColor) UIManager.put("Spinner.border", BorderFactory.createLineBorder(TITLE_BAR_COLOR)) UIManager.put("TextField.background", WIDGET_COLOR) UIManager.put("TextField.foreground", secondaryColor) UIManager.put("TextField.caretForeground", secondaryColor) UIManager.put("TextField.border", BorderFactory.createLineBorder(TITLE_BAR_COLOR)) UIManager.put("ScrollBar.thumb", WIDGET_COLOR) UIManager.put("ScrollBar.track", VIEW_BACKGROUND_COLOR) UIManager.put("ScrollBar.thumbHighlight", TITLE_BAR_COLOR) UIManager.put("ProgressBar.foreground", PROGRESS_BAR_FILL) UIManager.put("ProgressBar.background", WIDGET_COLOR) UIManager.put("ProgressBar.border", BorderFactory.createLineBorder(TITLE_BAR_COLOR)) UIManager.put("ToolTip.background", VIEW_BACKGROUND_COLOR) UIManager.put("ToolTip.foreground", secondaryColor) UIManager.put("ToolTip.border", BorderFactory.createLineBorder(TITLE_BAR_COLOR)) // Update component tree UI to apply the new theme SwingUtilities.updateComponentTreeUI(frame) frame.background = Color.BLACK } catch (e : Exception) { e.printStackTrace() } } private fun loadFont(): Font? { val fontStream = Helpers.openResource("res/runescape_small.ttf") return if (fontStream != null) { try { val font = Font.createFont(Font.TRUETYPE_FONT, fontStream) val ge = GraphicsEnvironment.getLocalGraphicsEnvironment() ge.registerFont(font) // Register the font in the graphics environment font } catch (e: Exception) { e.printStackTrace() null } } else { println("Font not found!") null } } object StateManager { var focusedView: String = "" } private fun applyTheme(theme: Theme) { WIDGET_COLOR = theme.widgetColor TITLE_BAR_COLOR = theme.titleBarColor VIEW_BACKGROUND_COLOR = theme.viewBackgroundColor primaryColor = theme.primaryColor secondaryColor = theme.secondaryColor POPUP_BACKGROUND = theme.popupBackground POPUP_FOREGROUND = theme.popupForeground TOOLTIP_BACKGROUND = theme.tooltipBackground SCROLL_BAR_COLOR = theme.scrollBarColor PROGRESS_BAR_FILL = theme.progressBarFill NAV_TINT = theme.navTint NAV_GREYSCALE = theme.navGreyScale BOOST = theme.boost } }