package KondoKit import KondoKit.Constants.COMBAT_LVL_SPRITE import KondoKit.Helpers.formatHtmlLabelText import KondoKit.Helpers.formatNumber import KondoKit.Helpers.getSpriteId import KondoKit.Helpers.showAlert import KondoKit.HiscoresView.createHiscoreSearchView import KondoKit.HiscoresView.hiScoreView import KondoKit.LootTrackerView.BAG_ICON import KondoKit.LootTrackerView.createLootTrackerView import KondoKit.LootTrackerView.lootTrackerView import KondoKit.LootTrackerView.npcDeathSnapshots import KondoKit.LootTrackerView.onPostClientTick import KondoKit.LootTrackerView.takeGroundSnapshot import KondoKit.ReflectiveEditorView.addPlugins import KondoKit.ReflectiveEditorView.createReflectiveEditorView import KondoKit.ReflectiveEditorView.reflectiveEditorView import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite import KondoKit.Themes.Theme import KondoKit.Themes.ThemeType import KondoKit.Themes.getTheme import KondoKit.XPTrackerView.createXPTrackerView import KondoKit.XPTrackerView.createXPWidget import KondoKit.XPTrackerView.initialXP import KondoKit.XPTrackerView.resetXPTracker import KondoKit.XPTrackerView.totalXPWidget import KondoKit.XPTrackerView.updateWidget import KondoKit.XPTrackerView.wrappedWidget import KondoKit.XPTrackerView.xpTrackerView import KondoKit.XPTrackerView.xpWidgets import KondoKit.plugin.StateManager.focusedView 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.* import java.awt.geom.AffineTransform import java.awt.image.AffineTransformOp import java.awt.image.BufferedImage import java.awt.image.VolatileImage 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 private const val FIXED_WIDTH = 765 private 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 tickInterval = 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 const val HIDDEN_VIEW = "HIDDEN" private var altCanvas: AltCanvas? = null private val drawActions = mutableListOf<() -> Unit>() fun registerDrawAction(action: () -> Unit) { synchronized(drawActions) { drawActions.add(action) } } } 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 } 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() } class AltCanvas : Canvas() { private var gameImage: VolatileImage? = null private var op: AffineTransformOp? = null private var transform: AffineTransform? = null private var flippedImage: BufferedImage? = null // Only used in HD private var bufferImage = BufferedImage(FIXED_WIDTH, FIXED_HEIGHT, BufferedImage.TYPE_INT_BGR) private var lastImageWidth = -1 private var lastImageHeight = -1 init { isFocusable = true requestFocusInWindow() addMouseListener(object : MouseAdapter() { override fun mousePressed(e: MouseEvent) = relayMouseEvent(e) override fun mouseReleased(e: MouseEvent) = relayMouseEvent(e) override fun mouseClicked(e: MouseEvent) = relayMouseEvent(e) }) addMouseMotionListener(object : MouseMotionAdapter() { override fun mouseMoved(e: MouseEvent) = relayMouseEvent(e) override fun mouseDragged(e: MouseEvent) = relayMouseEvent(e) }) addKeyListener(object : KeyAdapter() { override fun keyPressed(e: KeyEvent) = relayKeyEvent(e) { it.keyPressed(e) } override fun keyReleased(e: KeyEvent) = relayKeyEvent(e) { it.keyReleased(e) } override fun keyTyped(e: KeyEvent) = relayKeyEvent(e) { it.keyTyped(e) } }) } override fun update(g: Graphics) = paint(g) override fun addNotify() { super.addNotify() validateGameImage() } private fun validateGameImage() { val gc = GraphicsEnvironment.getLocalGraphicsEnvironment().defaultScreenDevice.defaultConfiguration gameImage?.let { when (it.validate(gc)) { VolatileImage.IMAGE_INCOMPATIBLE -> createGameImage(gc) VolatileImage.IMAGE_RESTORED -> renderGameImage() } } ?: createGameImage(gc) } private fun createGameImage(gc: GraphicsConfiguration) { gameImage = gc.createCompatibleVolatileImage(FIXED_WIDTH, FIXED_HEIGHT, Transparency.OPAQUE) renderGameImage() } private fun renderGameImage() { gameImage?.createGraphics()?.apply { color = Color.RED fillRect(0, 0, gameImage!!.width, gameImage!!.height) color = Color.BLACK drawString("Game Frame", 20, 20) dispose() } } override fun paint(g: Graphics) { val g2d = g as Graphics2D g2d.color = Color.BLACK g2d.fillRect(0, 0, width, height) gameImage?.let { image -> val scale = minOf(width.toDouble() / image.width, height.toDouble() / image.height) val x = ((width - image.width * scale) / 2).toInt() val y = ((height - image.height * scale) / 2).toInt() g2d.drawImage(image, x, y, (image.width * scale).toInt(), (image.height * scale).toInt(), null) Toolkit.getDefaultToolkit().sync() } } private fun relayMouseEvent(e: MouseEvent) { requestFocusInWindow() val scale = minOf(width.toDouble() / gameImage!!.width, height.toDouble() / gameImage!!.height) val xOffset = ((width - gameImage!!.width * scale) / 2) val yOffset = ((height - gameImage!!.height * scale) / 2) val adjustedX = ((e.x - xOffset) / scale).toInt().coerceIn(0, gameImage!!.width - 1) val adjustedY = ((e.y - yOffset) / scale).toInt().coerceIn(0, gameImage!!.height - 1) canvas.dispatchEvent(MouseEvent(this, e.id, e.`when`, e.modifiersEx, adjustedX, adjustedY, e.clickCount, e.isPopupTrigger, e.button)) } private fun relayKeyEvent(e: KeyEvent, action: (KeyListener) -> Unit) { for (listener in canvas.keyListeners) action(listener) } fun updateGameImage() { if (IsHD()) renderGlRaster() else renderSoftwareRaster() repaint() } private fun renderGlRaster() { val width = gameImage!!.width val height = gameImage!!.height bufferImage.setRGB(0, 0, width, height, GlRenderer.pixelData, 0, width) // Check if dimensions have changed if (width != lastImageWidth || height != lastImageHeight) { // Initialize or update transform and operation transform = AffineTransform.getScaleInstance(1.0, -1.0).apply { translate(0.0, -height.toDouble()) } op = AffineTransformOp(transform, AffineTransformOp.TYPE_NEAREST_NEIGHBOR) flippedImage = BufferedImage(width, height, bufferImage.type) lastImageWidth = width lastImageHeight = height } // Apply the transform operation op!!.filter(bufferImage, flippedImage) // Draw the flipped image onto gameImage gameImage?.createGraphics()?.apply { drawImage(flippedImage, 0, 0, null) dispose() } } private fun renderSoftwareRaster() { gameImage?.createGraphics()?.apply { SoftwareRaster.frameBuffer.draw(this) dispose() } } } private fun createAltCanvas(): AltCanvas { return AltCanvas().apply { preferredSize = Dimension(FIXED_WIDTH, FIXED_HEIGHT) } } override fun Init() { // Disable Font AA System.setProperty("sun.java2d.opengl", "false") System.setProperty("awt.useSystemAAFontSettings", "off") System.setProperty("swing.aatext", "false") val frame: Frame? = GameShell.frame if (frame != null) { frame.layout = BorderLayout() // Create the AltCanvas and add it to the main frame altCanvas = createAltCanvas() altCanvas?.let { frame.add(it) } // Use BorderLayout for better layout control frame.setComponentZOrder(altCanvas, 0) frame.setComponentZOrder(canvas, 1) } } private fun UpdateDisplaySettings() { val mode = GetWindowMode() val currentScrollPaneWidth = if (mainContentPanel.isVisible) NAVBAR_WIDTH + MAIN_CONTENT_WIDTH else NAVBAR_WIDTH lastUIOffset = uiOffset when (mode) { WindowMode.FIXED -> { if (frame.width < FIXED_WIDTH + currentScrollPaneWidth + uiOffset) { frame.setSize(FIXED_WIDTH + currentScrollPaneWidth + uiOffset, frame.height) } canvas.setLocation(0,0) val difference = frame.width - (uiOffset + currentScrollPaneWidth) altCanvas?.size = Dimension(difference, frame.height - 30) } WindowMode.RESIZABLE -> { GameShell.canvasWidth = frame.width - (currentScrollPaneWidth + uiOffset) } } rightPanelWrapper?.preferredSize = Dimension(currentScrollPaneWidth, frame.height) rightPanelWrapper?.let { it.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) if(lastUIOffset != uiOffset){ UpdateDisplaySettings() reloadInterfaces = true } } 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)) } } } } 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.") } } } override fun OnPluginsReloaded(): Boolean { if (!initialized) return true UpdateDisplaySettings() frame.remove(rightPanelWrapper) frame.layout = BorderLayout() frame.add(rightPanelWrapper, BorderLayout.EAST) frame.revalidate() pluginsReloaded = true reloadInterfaces = true return true } override fun OnXPUpdate(skillId: Int, xp: Int) { if (!initialXP.containsKey(skillId)) { initialXP[skillId] = xp return } var xpWidget = xpWidgets[skillId] if (xpWidget != null) { updateWidget(xpWidget, xp) } else { val previousXp = initialXP[skillId] ?: xp if (xp == initialXP[skillId]) return xpWidget = createXPWidget(skillId, previousXp) xpWidgets[skillId] = xpWidget xpTrackerView?.add(wrappedWidget(xpWidget.container)) xpTrackerView?.add(Box.createVerticalStrut(5)) if(focusedView == XPTrackerView.VIEW_NAME) { xpTrackerView?.revalidate() xpTrackerView?.repaint() } updateWidget(xpWidget, xp) } } 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) { reflectiveEditorView?.let { addPlugins(it) } pluginsReloaded = false } if (reloadInterfaces){ InterfaceList.method3712(true) // Gets the resize working correctly reloadInterfaces = false } accumulatedTime += timeDelta if (accumulatedTime >= tickInterval) { 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", "Error", JOptionPane.INFORMATION_MESSAGE ) return; } if(GetWindowMode() == WindowMode.RESIZABLE){ frame.setComponentZOrder(altCanvas, 1) frame.setComponentZOrder(canvas, 0) } else { frame.setComponentZOrder(altCanvas, 0) frame.setComponentZOrder(canvas, 1) } altCanvas?.updateGameImage() // Update the game image as needed } 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) { loadFont() val themeIndex = (GetData("kondoTheme") as? String) ?: "RUNELITE" theme = ThemeType.valueOf(themeIndex) applyTheme(getTheme(theme)) appliedTheme = theme 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(GameShell.frame) GameShell.frame.background = Color.BLACK } catch (e : Exception) { e.printStackTrace() } // Restore saved values 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 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)) var 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() // Removes the border completely horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_NEVER verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_NEVER } frame.layout = BorderLayout() rightPanelWrapper?.let { frame.add(it, BorderLayout.EAST) frame.setComponentZOrder(it, 2) } if(!launchMinimized){ setActiveView(XPTrackerView.VIEW_NAME) } else { setActiveView(HIDDEN_VIEW) } initialized = true pluginsReloaded = true } } 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 setActiveView(viewName: String) { // Handle the visibility of the main content panel if (viewName == HIDDEN_VIEW) { mainContentPanel.isVisible = false } else { if (!mainContentPanel.isVisible) { mainContentPanel.isVisible = true } cardLayout.show(mainContentPanel, viewName) } reloadInterfaces = true UpdateDisplaySettings() // Revalidate and repaint necessary panels mainContentPanel.revalidate() rightPanelWrapper?.revalidate() frame?.revalidate() mainContentPanel.repaint() rightPanelWrapper?.repaint() frame?.repaint() focusedView = viewName } 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 // Ensure background is painted 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 } fun loadFont(): Font? { val fontStream = plugin::class.java.getResourceAsStream("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 = "" } 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 } }