package KondoKit.views import KondoKit.Helpers import KondoKit.components.* import KondoKit.components.ReflectiveEditorComponents.CustomSearchField import KondoKit.components.ReflectiveEditorComponents.GitLabPlugin import KondoKit.components.ReflectiveEditorComponents.GitLabPluginFetcher import KondoKit.Helpers.showToast import KondoKit.plugin import KondoKit.plugin.Companion.TOOLTIP_BACKGROUND import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR import KondoKit.plugin.Companion.secondaryColor import plugin.Plugin import plugin.PluginInfo import plugin.PluginRepository import rt4.GlobalJsonConfig import java.awt.* import java.awt.image.BufferedImage import java.io.File import java.util.* import javax.imageio.ImageIO import javax.swing.* import javax.swing.border.AbstractBorder import kotlin.math.ceil /* This is used for the runtime editing of plugin variables. To expose fields add the @Exposed annotation. When they are applied this will trigger an invoke of OnKondoValueUpdated() if it is implemented. Check GroundItems plugin for an example. */ object ReflectiveEditorView : View { var reflectiveEditorView: JPanel? = null private val loadedPlugins: MutableList = mutableListOf() const val VIEW_NAME = "REFLECTIVE_EDITOR_VIEW" const val PLUGIN_LIST_VIEW = "PLUGIN_LIST" const val PLUGIN_DETAIL_VIEW = "PLUGIN_DETAIL" val pluginsDirectory: File = File(GlobalJsonConfig.instance.pluginsFolder) // GitLab API configuration private const val GITLAB_ACCESS_TOKEN = "glpat-dE2Cs2e4b32-H7c9oGuS" private const val GITLAB_PROJECT_ID = "38297322" private const val GITLAB_BRANCH = "master" // Store fetched GitLab plugins private var gitLabPlugins: List = listOf() // Store the cog icon to be reused private var cogIcon: Icon? = null private fun loadCogIcon() { try { val imageStream = this::class.java.getResourceAsStream("res/cog.png") if (imageStream != null) { val image: BufferedImage = ImageIO.read(imageStream) val scaledImage = image.getScaledInstance(12, 12, Image.SCALE_SMOOTH) cogIcon = ImageIcon(scaledImage) } } catch (e: Exception) { e.printStackTrace() } } // Card layout for switching between views within the reflective editor view private lateinit var cardLayout: CardLayout private lateinit var mainPanel: JPanel private var currentPluginInfo: PluginInfo? = null private var currentPlugin: Plugin? = null // Search text for filtering plugins private var pluginSearchText: String = "" override val name: String = VIEW_NAME override val iconSpriteId: Int = KondoKit.plugin.WRENCH_ICON override val panel: JPanel get() = reflectiveEditorView ?: JPanel() override fun createView() { createReflectiveEditorView() } override fun registerFunctions() { // Reflective editor functions are handled within the view itself } fun createReflectiveEditorView() { // Load the cog icon once loadCogIcon() // Create the main panel with card layout cardLayout = CardLayout() mainPanel = JPanel(cardLayout) mainPanel.background = VIEW_BACKGROUND_COLOR mainPanel.border = BorderFactory.createEmptyBorder(0, 0, 0, 0) // Help minimize flicker during dynamic swaps mainPanel.isDoubleBuffered = true // Create the plugin list view val pluginListView = createPluginListView() pluginListView.name = PLUGIN_LIST_VIEW mainPanel.add(pluginListView, PLUGIN_LIST_VIEW) // Create a placeholder for the plugin detail view val pluginDetailView = BaseView(PLUGIN_DETAIL_VIEW).apply { layout = BorderLayout() background = VIEW_BACKGROUND_COLOR } mainPanel.add(pluginDetailView, PLUGIN_DETAIL_VIEW) // Set the reflectiveEditorView to our main panel reflectiveEditorView = mainPanel // Show the plugin list view by default cardLayout.show(mainPanel, PLUGIN_LIST_VIEW) // Fetch GitLab plugins in the background GitLabPluginFetcher.fetchGitLabPlugins { plugins -> gitLabPlugins = plugins // We'll update the UI when needed } } private fun createPluginListView(): JPanel { val panel = JPanel() panel.layout = BoxLayout(panel, BoxLayout.Y_AXIS) panel.background = VIEW_BACKGROUND_COLOR // Add search field at the top val searchField = CustomSearchField(panel) { searchText -> pluginSearchText = searchText // Refresh the plugin list to apply filtering SwingUtilities.invokeLater { addPlugins(reflectiveEditorView!!) } } val searchFieldWrapper = JPanel().apply { layout = BoxLayout(this, BoxLayout.X_AXIS) background = VIEW_BACKGROUND_COLOR preferredSize = Dimension(230, 30) maximumSize = preferredSize minimumSize = preferredSize alignmentX = Component.CENTER_ALIGNMENT add(searchField) } panel.add(Box.createVerticalStrut(10)) // Spacer panel.add(searchFieldWrapper) panel.add(Box.createVerticalStrut(10)) // Spacer try { // Get loaded plugins val loadedPluginsField = PluginRepository::class.java.getDeclaredField("loadedPlugins") loadedPluginsField.isAccessible = true val loadedPlugins = loadedPluginsField.get(null) as HashMap<*, *> // Separate plugins with and without exposed attributes val pluginsWithExposed = mutableListOf>() val pluginsWithoutExposed = mutableListOf>() for ((pluginInfo, plugin) in loadedPlugins) { val exposedFields = (plugin as Plugin).javaClass.declaredFields.filter { field -> field.annotations.any { annotation -> annotation.annotationClass.simpleName == "Exposed" } } // Apply search filter val pluginName = plugin.javaClass.`package`.name if (pluginSearchText.isBlank() || pluginName.contains(pluginSearchText, ignoreCase = true)) { if (exposedFields.isNotEmpty()) { pluginsWithExposed.add(pluginInfo as PluginInfo to plugin as Plugin) } else { pluginsWithoutExposed.add(pluginInfo as PluginInfo to plugin as Plugin) } } } // Sort both lists by package name pluginsWithExposed.sortBy { it.second.javaClass.`package`.name } pluginsWithoutExposed.sortBy { it.second.javaClass.`package`.name } // Add plugins with exposed attributes first for ((pluginInfo, plugin) in pluginsWithExposed) { val pluginPanel = createPluginItemPanel(pluginInfo, plugin) panel.add(pluginPanel) panel.add(Box.createVerticalStrut(5)) } // Add a separator if we have plugins with exposed attributes if (pluginsWithExposed.isNotEmpty() && pluginsWithoutExposed.isNotEmpty()) { val separator = JPanel() separator.background = VIEW_BACKGROUND_COLOR separator.preferredSize = Dimension(Int.MAX_VALUE, 1) separator.maximumSize = Dimension(Int.MAX_VALUE, 1) panel.add(separator) panel.add(Box.createVerticalStrut(5)) } // Add plugins without exposed attributes for ((pluginInfo, plugin) in pluginsWithoutExposed) { val pluginPanel = createPluginItemPanel(pluginInfo, plugin) panel.add(pluginPanel) panel.add(Box.createVerticalStrut(5)) } } catch (e: Exception) { e.printStackTrace() } // Add disabled plugins to the list (filtered by search text) val disabledDir = File(pluginsDirectory, "disabled") if (disabledDir.exists() && disabledDir.isDirectory) { val disabledPlugins = disabledDir.listFiles { file -> file.isDirectory } ?: arrayOf() // Add disabled plugins to the list without exposed attributes (filtered by search text) for (pluginDir in disabledPlugins.sortedBy { it.name }) { // Apply search filter if (pluginSearchText.isBlank() || pluginDir.name.contains(pluginSearchText, ignoreCase = true)) { val pluginPanel = createDisabledPluginItemPanel(pluginDir.name) panel.add(pluginPanel) panel.add(Box.createVerticalStrut(5)) } } } // Add a section for available plugins from GitLab that are not installed // Only show this section when there's a search term if (pluginSearchText.isNotBlank()) { val matchingGitLabPlugins = gitLabPlugins.filter { plugin -> plugin.pluginProperties != null && (plugin.path.contains(pluginSearchText, ignoreCase = true) || plugin.pluginProperties!!.description.contains(pluginSearchText, ignoreCase = true)) } if (matchingGitLabPlugins.isNotEmpty()) { // Add a separator val separator = JPanel() separator.background = VIEW_BACKGROUND_COLOR separator.preferredSize = Dimension(Int.MAX_VALUE, 1) separator.maximumSize = Dimension(Int.MAX_VALUE, 1) panel.add(Box.createVerticalStrut(10)) panel.add(separator) panel.add(Box.createVerticalStrut(5)) // Add a header for available plugins val headerPanel = JPanel(FlowLayout(FlowLayout.LEFT)) headerPanel.background = VIEW_BACKGROUND_COLOR val headerLabel = JLabel("Available Plugins") headerLabel.foreground = plugin.Companion.secondaryColor headerLabel.font = Font("RuneScape Small", Font.BOLD, 16) headerPanel.add(headerLabel) panel.add(headerPanel) panel.add(Box.createVerticalStrut(5)) // Add matching GitLab plugins for (gitLabPlugin in matchingGitLabPlugins) { val pluginPanel = createGitLabPluginItemPanel(gitLabPlugin) panel.add(pluginPanel) panel.add(Box.createVerticalStrut(5)) } } } // Wrap the panel in a ScrollablePanel for custom scrolling val scrollablePanel = ScrollablePanel(panel) // Reset scroll position to top SwingUtilities.invokeLater { // For ScrollablePanel, we need to reset the internal offset resetScrollablePanel(scrollablePanel) } val container = JPanel(BorderLayout()) container.background = VIEW_BACKGROUND_COLOR container.add(scrollablePanel, BorderLayout.CENTER) return container } private fun createPluginItemPanel(pluginInfo: PluginInfo, plugin: Plugin): JPanel { val panel = JPanel(BorderLayout()) panel.background = KondoKit.plugin.Companion.WIDGET_COLOR panel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10) panel.maximumSize = Dimension(220, 60) // Plugin name and version (using package name as in original implementation) val packageName = plugin.javaClass.`package`.name val nameLabel = JLabel(packageName) nameLabel.foreground = KondoKit.plugin.Companion.secondaryColor nameLabel.font = Font("RuneScape Small", Font.PLAIN, 16) // Check if plugin has exposed attributes val exposedFields = plugin.javaClass.declaredFields.filter { field -> field.annotations.any { annotation -> annotation.annotationClass.simpleName == "Exposed" } } // Edit button (only show if plugin has exposed attributes) val editButton = if (exposedFields.isNotEmpty()) { val button = JButton() if (cogIcon != null) { button.icon = cogIcon } else { button.text = "Edit" } button.background = KondoKit.plugin.Companion.TITLE_BAR_COLOR button.foreground = KondoKit.plugin.Companion.secondaryColor button.font = Font("RuneScape Small", Font.PLAIN, 14) button.addActionListener { showPluginDetails(pluginInfo, plugin) } button } else { null } // Plugin toggle switch (iOS style) val toggleSwitch = createToggleSwitch(plugin, pluginInfo) // Layout val infoPanel = JPanel(BorderLayout()) infoPanel.background = KondoKit.plugin.Companion.WIDGET_COLOR infoPanel.add(nameLabel, BorderLayout.WEST) val controlsPanel = JPanel(FlowLayout(FlowLayout.RIGHT, 5, 0)) controlsPanel.background = KondoKit.plugin.Companion.WIDGET_COLOR // Add edit button first (left side of controls) editButton?.let { controlsPanel.add(it) } // Add toggle switch second (right side of controls) controlsPanel.add(toggleSwitch) panel.add(infoPanel, BorderLayout.CENTER) panel.add(controlsPanel, BorderLayout.EAST) return panel } private fun createDisabledPluginItemPanel(pluginName: String): JPanel { val panel = JPanel(BorderLayout()) panel.background = plugin.Companion.WIDGET_COLOR panel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10) panel.maximumSize = Dimension(220, 60) // Plugin name val nameLabel = JLabel(pluginName) nameLabel.foreground = plugin.Companion.secondaryColor nameLabel.font = Font("RuneScape Small", Font.PLAIN, 16) // Plugin toggle switch (iOS style) - initially off for disabled plugins val toggleSwitch = ToggleSwitch() toggleSwitch.setActivated(false) toggleSwitch.onToggleListener = { activated -> if (activated) { enablePlugin(pluginName) } // If trying to disable an already disabled plugin, reset the toggle else { toggleSwitch.setActivated(false) } } // Layout val infoPanel = JPanel(BorderLayout()) infoPanel.background = plugin.Companion.WIDGET_COLOR infoPanel.add(nameLabel, BorderLayout.WEST) val controlsPanel = JPanel(FlowLayout(FlowLayout.RIGHT, 5, 0)) controlsPanel.background = plugin.Companion.WIDGET_COLOR controlsPanel.add(toggleSwitch) panel.add(infoPanel, BorderLayout.CENTER) panel.add(controlsPanel, BorderLayout.EAST) return panel } private fun createGitLabPluginItemPanel(gitLabPlugin: GitLabPlugin): JPanel { val panel = JPanel(BorderLayout()) panel.background = plugin.Companion.WIDGET_COLOR panel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10) panel.maximumSize = Dimension(220, 60) // Plugin name val nameLabel = JLabel(gitLabPlugin.path) nameLabel.foreground = plugin.Companion.secondaryColor nameLabel.font = Font("RuneScape Small", Font.PLAIN, 16) // Download button (placeholder for now) val downloadButton = JButton("Download") downloadButton.background = plugin.Companion.TITLE_BAR_COLOR downloadButton.foreground = plugin.Companion.secondaryColor downloadButton.font = Font("RuneScape Small", Font.PLAIN, 14) downloadButton.addActionListener { // TODO: Implement download functionality showToast(mainPanel, "Download functionality not yet implemented", JOptionPane.INFORMATION_MESSAGE) } // Plugin toggle switch (iOS style) - initially off for GitLab plugins val toggleSwitch = ToggleSwitch() toggleSwitch.setActivated(false) toggleSwitch.isEnabled = false // Disable toggle for GitLab plugins until downloaded // Layout val infoPanel = JPanel(BorderLayout()) infoPanel.background = plugin.Companion.WIDGET_COLOR infoPanel.add(nameLabel, BorderLayout.WEST) val controlsPanel = JPanel(FlowLayout(FlowLayout.RIGHT, 5, 0)) controlsPanel.background = plugin.Companion.WIDGET_COLOR controlsPanel.add(downloadButton) controlsPanel.add(toggleSwitch) panel.add(infoPanel, BorderLayout.CENTER) panel.add(controlsPanel, BorderLayout.EAST) return panel } private fun enablePlugin(pluginName: String) { try { // Source and destination directories val disabledDir = File(pluginsDirectory, "disabled") val sourceDir = File(disabledDir, pluginName) val destDir = File(pluginsDirectory, pluginName) // Check if source directory exists if (!sourceDir.exists()) { showToast(mainPanel, "Plugin directory not found: ${sourceDir.absolutePath}", JOptionPane.ERROR_MESSAGE) return } // Move the directory if (sourceDir.renameTo(destDir)) { showToast(mainPanel, "Plugin enabled") // Reload plugins to apply the change PluginRepository.reloadPlugins() // Refresh the plugin list view SwingUtilities.invokeLater { addPlugins(reflectiveEditorView!!) } } else { showToast(mainPanel, "Failed to enable plugin", JOptionPane.ERROR_MESSAGE) } } catch (e: Exception) { e.printStackTrace() showToast(mainPanel, "Error enabling plugin: ${e.message}", JOptionPane.ERROR_MESSAGE) } } private fun createToggleSwitch(plugin: Plugin, pluginInfo: PluginInfo): ToggleSwitch { val toggleSwitch = ToggleSwitch() // Set initial state toggleSwitch.setActivated(isPluginEnabled(plugin, pluginInfo)) // Add toggle listener toggleSwitch.onToggleListener = { activated -> togglePlugin(plugin, pluginInfo, toggleSwitch, activated) } return toggleSwitch } private fun isPluginEnabled(plugin: Plugin, pluginInfo: PluginInfo): Boolean { // Get the plugin directory name from the plugin's class package val pluginDirName = getPluginDirName(plugin) val pluginDir = File(pluginsDirectory, pluginDirName) return pluginDir.exists() && pluginDir.isDirectory } private fun getPluginDirName(plugin: Plugin): String { // Extract the directory name from the plugin's package // The package name is typically like "GroundItems.plugin" so we take the first part val packageName = plugin.javaClass.`package`.name return packageName.substringBefore(".") } private fun togglePlugin(plugin: Plugin, pluginInfo: PluginInfo, toggleSwitch: ToggleSwitch, activated: Boolean) { try { // Get the plugin directory name from the plugin's class package val pluginDirName = getPluginDirName(plugin) // Source and destination directories val sourceDir = if (activated) { // Moving from disabled to enabled File(File(pluginsDirectory, "disabled"), pluginDirName) } else { // Moving from enabled to disabled File(pluginsDirectory, pluginDirName) } val destDir = if (activated) { // Moving to main plugins directory File(pluginsDirectory, pluginDirName) } else { // Moving to disabled directory val disabledDir = File(pluginsDirectory, "disabled") if (!disabledDir.exists()) { disabledDir.mkdirs() } File(disabledDir, pluginDirName) } // Check if source directory exists if (!sourceDir.exists()) { showToast(mainPanel, "Plugin directory not found: ${sourceDir.absolutePath}", JOptionPane.ERROR_MESSAGE) // Reset toggle switch to previous state toggleSwitch.setActivated(!activated) return } // Move the directory if (sourceDir.renameTo(destDir)) { showToast(mainPanel, if (activated) "Plugin enabled" else "Plugin disabled") // Reload plugins to apply the change PluginRepository.reloadPlugins() // Refresh the plugin list view SwingUtilities.invokeLater { addPlugins(reflectiveEditorView!!) } } else { showToast(mainPanel, "Failed to ${if (activated) "enable" else "disable"} plugin", JOptionPane.ERROR_MESSAGE) // Reset toggle switch to previous state toggleSwitch.setActivated(!activated) } } catch (e: Exception) { e.printStackTrace() showToast(mainPanel, "Error toggling plugin: ${e.message}", JOptionPane.ERROR_MESSAGE) // Reset toggle switch to previous state toggleSwitch.setActivated(!activated) } } private fun showPluginDetails(pluginInfo: PluginInfo, plugin: Plugin) { currentPluginInfo = pluginInfo currentPlugin = plugin // Remove the existing detail view if it exists val existingDetailView = mainPanel.components.find { it.name == PLUGIN_DETAIL_VIEW } if (existingDetailView != null) { mainPanel.remove(existingDetailView) } // Create a new detail view val detailView = BaseView(PLUGIN_DETAIL_VIEW) detailView.layout = BorderLayout() detailView.background = VIEW_BACKGROUND_COLOR // Header with back button //$packageName v${pluginInfo.version} should be val headerPanel = ViewHeader(" v${pluginInfo.version}", 40).apply { border = BorderFactory.createEmptyBorder(5, 10, 5, 10) } val backButton = ButtonPanel(FlowLayout.LEFT).addButton("Back") { // Reset scroll position to top when returning to the list view SwingUtilities.invokeLater { // Find the ScrollablePanel in the plugin list view and reset its scroll position val listView = mainPanel.components.find { it.name == PLUGIN_LIST_VIEW } if (listView != null && listView is Container) { findAndResetScrollablePanel(listView) } } cardLayout.show(mainPanel, PLUGIN_LIST_VIEW) } val packageName = plugin.javaClass.`package`.name headerPanel.add(backButton, BorderLayout.WEST) // Content panel for settings val contentPanel = JPanel() contentPanel.layout = BoxLayout(contentPanel, BoxLayout.Y_AXIS) contentPanel.background = VIEW_BACKGROUND_COLOR contentPanel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10) // Add plugin info val infoPanel = JPanel(BorderLayout()) infoPanel.background = KondoKit.plugin.Companion.WIDGET_COLOR infoPanel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10) infoPanel.maximumSize = Dimension(Int.MAX_VALUE, 80) val infoText = """ Version: ${pluginInfo.version} Author: ${pluginInfo.author} Description: ${pluginInfo.description} """.trimIndent() val infoLabel = LabelComponent(infoText).apply { font = Font("RuneScape Small", Font.PLAIN, 14) } infoPanel.add(infoLabel, BorderLayout.CENTER) contentPanel.add(infoPanel) contentPanel.add(Box.createVerticalStrut(10)) // Add exposed fields addPluginSettings(contentPanel, plugin) // Wrap the content panel in a ScrollablePanel for custom scrolling val scrollablePanel = ScrollablePanel(contentPanel) // Reset scroll position to top when entering the edit page SwingUtilities.invokeLater { resetScrollablePanel(scrollablePanel) } detailView.add(headerPanel, BorderLayout.NORTH) detailView.add(scrollablePanel, BorderLayout.CENTER) // Add the new detail view to the main panel mainPanel.add(detailView, PLUGIN_DETAIL_VIEW) // Revalidate and repaint the main panel mainPanel.revalidate() mainPanel.repaint() // Show the detail view cardLayout.show(mainPanel, PLUGIN_DETAIL_VIEW) } private fun addPluginSettings(contentPanel: JPanel, plugin: Plugin) { val settingsPanel = SettingsPanel(plugin) settingsPanel.addSettingsFromPlugin() contentPanel.add(settingsPanel) } fun addPlugins(reflectiveEditorView: JPanel) { // Ensure we run on the EDT; if not, reschedule and return if (!SwingUtilities.isEventDispatchThread()) { SwingUtilities.invokeLater { addPlugins(reflectiveEditorView) } return } // Batch updates to avoid intermediate repaints/flicker mainPanel.ignoreRepaint = true try { // Remove the existing plugin list view if present val existingListView = mainPanel.components.find { it.name == PLUGIN_LIST_VIEW } if (existingListView != null) { mainPanel.remove(existingListView) } // Create a new plugin list view off-EDT (already on EDT here) and add it val pluginListView = createPluginListView() pluginListView.name = PLUGIN_LIST_VIEW mainPanel.add(pluginListView, PLUGIN_LIST_VIEW) // Switch card after the new component is in place cardLayout.show(mainPanel, PLUGIN_LIST_VIEW) // Revalidate/repaint once at the end mainPanel.revalidate() mainPanel.repaint() } finally { mainPanel.ignoreRepaint = false } } var customToolTipWindow: JWindow? = null fun showCustomToolTip(text: String, component: JComponent) { val _font = Font("RuneScape Small", Font.PLAIN, 16) val maxWidth = 150 val lineHeight = 16 // Create a dummy JLabel to get FontMetrics for the font used in the tooltip val dummyLabel = JLabel() dummyLabel.font = _font val fontMetrics = dummyLabel.getFontMetrics(_font) // Calculate the approximate width of the text val textWidth = fontMetrics.stringWidth(text) // Calculate the number of lines required based on the text width and max tooltip width val numberOfLines = ceil(textWidth.toDouble() / maxWidth).toInt() // Calculate the required height of the tooltip val requiredHeight = numberOfLines * lineHeight + 6 // Adding some padding if (customToolTipWindow == null) { customToolTipWindow = JWindow().apply { val bgColor = Helpers.colorToHex(TOOLTIP_BACKGROUND) val textColor = Helpers.colorToHex(secondaryColor) contentPane = JLabel("
$text
").apply { border = BorderFactory.createLineBorder(Color.BLACK) isOpaque = true background = TOOLTIP_BACKGROUND foreground = Color.WHITE font = _font maximumSize = Dimension(maxWidth, Int.MAX_VALUE) preferredSize = Dimension(maxWidth, requiredHeight) } pack() } } else { // Update the tooltip text val label = customToolTipWindow!!.contentPane as JLabel val bgColor = Helpers.colorToHex(TOOLTIP_BACKGROUND) val textColor = Helpers.colorToHex(secondaryColor) label.text = "
$text
" label.preferredSize = Dimension(maxWidth, requiredHeight) customToolTipWindow!!.pack() } // Position the tooltip near the component val locationOnScreen = component.locationOnScreen customToolTipWindow!!.setLocation(locationOnScreen.x, locationOnScreen.y + 15) customToolTipWindow!!.isVisible = true } // Helper method to reset a ScrollablePanel to the top private fun resetScrollablePanel(scrollablePanel: ScrollablePanel) { // Access the content panel and reset its position // Since we can't directly access the private fields, we'll use reflection try { val contentField = ScrollablePanel::class.java.getDeclaredField("content") contentField.isAccessible = true val contentPanel = contentField.get(scrollablePanel) as JPanel // Reset the location to the top contentPanel.setLocation(0, 0) // Force a repaint scrollablePanel.revalidate() scrollablePanel.repaint() } catch (e: Exception) { // If reflection fails, at least try to repaint scrollablePanel.revalidate() scrollablePanel.repaint() } } // Helper method to find and reset ScrollablePanel components within a container private fun findAndResetScrollablePanel(container: Component) { if (container is ScrollablePanel) { resetScrollablePanel(container) } else if (container is Container) { for (child in container.components) { findAndResetScrollablePanel(child) } } } // Custom border for rounded components class RoundedBorder(radius: Int) : AbstractBorder() { private val radius = radius override fun paintBorder(c: Component, g: Graphics, x: Int, y: Int, width: Int, height: Int) { val g2 = g as Graphics2D g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) g2.setColor(c.background) g2.fillRoundRect(x, y, width - 1, height - 1, radius, radius) } } }