package KondoKit.views import KondoKit.Helpers import KondoKit.components.* import KondoKit.ViewConstants import KondoKit.views.ViewLayoutHelpers.createSearchFieldSection import KondoKit.setFixedSize import KondoKit.attachPopupMenu import KondoKit.ReflectiveEditorPlugin import KondoKit.pluginmanager.GitLabPlugin import KondoKit.pluginmanager.GitLabPluginFetcher import KondoKit.pluginmanager.PluginDownloadManager import KondoKit.pluginmanager.PluginProperties import KondoKit.pluginmanager.PluginInfoWithStatus import KondoKit.Helpers.showToast import KondoKit.plugin.Companion.TOOLTIP_BACKGROUND import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR import KondoKit.plugin.Companion.WIDGET_COLOR import KondoKit.plugin.Companion.WRENCH_ICON import KondoKit.plugin.Companion.secondaryColor import plugin.Plugin import plugin.PluginInfo import plugin.PluginRepository import rt4.GlobalJsonConfig import java.awt.* import java.io.File import java.util.* import javax.swing.* 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 const val VIEW_NAME = "REFLECTIVE_EDITOR_VIEW" const val PLUGIN_LIST_VIEW = "PLUGIN_LIST" const val PLUGIN_DETAIL_VIEW = "PLUGIN_DETAIL" private val reflectiveEditorPlugin = ReflectiveEditorPlugin() private var searchField: SearchField? = null // Helper function to check if a plugin is the KondoKit plugin private fun isKondoKit(pluginName: String): Boolean { return pluginName == "KondoKit" } // 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 = "" private var searchFieldWrapper: JPanel? = null private var pluginListContentPanel: JPanel? = null private var pluginListScrollablePanel: ScrollablePanel? = null override val name: String = VIEW_NAME override val iconSpriteId: Int = WRENCH_ICON override val panel: JPanel get() = reflectiveEditorView ?: JPanel() override fun createView() { // Initialize the plugin reflectiveEditorPlugin.Init() createReflectiveEditorView() } override fun registerFunctions() { } fun createReflectiveEditorView() { cardLayout = CardLayout() mainPanel = JPanel(cardLayout) mainPanel.background = VIEW_BACKGROUND_COLOR mainPanel.border = BorderFactory.createEmptyBorder(0, 0, 0, 0) mainPanel.isDoubleBuffered = true val pluginListView = createPluginListView() pluginListView.name = PLUGIN_LIST_VIEW mainPanel.add(pluginListView, PLUGIN_LIST_VIEW) val pluginDetailView = BaseView(PLUGIN_DETAIL_VIEW).apply { layout = BorderLayout() background = VIEW_BACKGROUND_COLOR } mainPanel.add(pluginDetailView, PLUGIN_DETAIL_VIEW) reflectiveEditorView = mainPanel cardLayout.show(mainPanel, PLUGIN_LIST_VIEW) // Plugin initialization is handled by the plugin class, gitLabPlugins will be fetched automatically } private fun createPluginListView(): JPanel { val panel = BaseView(PLUGIN_LIST_VIEW, addDefaultSpacing = false).apply { background = VIEW_BACKGROUND_COLOR } val searchSection = createSearchFieldSection( parent = panel, placeholderText = "Search plugins...", viewName = VIEW_NAME, initialText = pluginSearchText ) { searchText -> pluginSearchText = searchText SwingUtilities.invokeLater { addPlugins(reflectiveEditorView!!) } } this.searchField = searchSection.searchField searchFieldWrapper = searchSection.wrapper panel.add(Box.createVerticalStrut(5)) panel.add(searchSection.wrapper) panel.add(Box.createVerticalStrut(10)) pluginListContentPanel = JPanel().apply { layout = BoxLayout(this, BoxLayout.Y_AXIS) background = VIEW_BACKGROUND_COLOR alignmentX = Component.CENTER_ALIGNMENT } panel.add(pluginListContentPanel) val scrollablePanel = ScrollablePanel(panel) pluginListScrollablePanel = scrollablePanel val container = JPanel(BorderLayout()) container.background = VIEW_BACKGROUND_COLOR container.add(scrollablePanel, BorderLayout.CENTER) populatePluginListContent() SwingUtilities.invokeLater { resetScrollablePanel(scrollablePanel) } return container } private fun populatePluginListContent() { val contentPanel = pluginListContentPanel ?: return val treeLock = contentPanel.treeLock synchronized(treeLock) { contentPanel.removeAll() 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) } else { pluginsWithoutExposed.add(pluginInfo as PluginInfo to plugin) } } } pluginsWithExposed.sortWith(compareBy( { if (isKondoKit(it.second.javaClass.`package`.name)) 0 else 1 }, { it.second.javaClass.`package`.name } )) pluginsWithoutExposed.sortWith(compareBy( { if (isKondoKit(it.second.javaClass.`package`.name)) 0 else 1 }, { it.second.javaClass.`package`.name } )) for ((pluginInfo, plugin) in pluginsWithExposed) { val pluginPanel = createPluginItemPanel(pluginInfo, plugin) contentPanel.add(pluginPanel) contentPanel.add(Box.createVerticalStrut(5)) } 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) contentPanel.add(separator) contentPanel.add(Box.createVerticalStrut(5)) } for ((pluginInfo, plugin) in pluginsWithoutExposed) { val pluginPanel = createPluginItemPanel(pluginInfo, plugin) contentPanel.add(pluginPanel) contentPanel.add(Box.createVerticalStrut(5)) } } catch (e: Exception) { e.printStackTrace() } val disabledDir = File(reflectiveEditorPlugin.getPluginsDirectory(), "disabled") if (disabledDir.exists() && disabledDir.isDirectory) { val disabledPlugins = disabledDir.listFiles { file -> file.isDirectory } ?: arrayOf() for (pluginDir in disabledPlugins.sortedBy { it.name }) { if (isKondoKit(pluginDir.name)) { continue } if (pluginSearchText.isBlank() || pluginDir.name.contains(pluginSearchText, ignoreCase = true)) { val pluginPanel = createDisabledPluginItemPanel(pluginDir.name) contentPanel.add(pluginPanel) contentPanel.add(Box.createVerticalStrut(5)) } } } if (pluginSearchText.isNotBlank()) { val matchingPluginStatuses = reflectiveEditorPlugin.getPluginStatuses().filter { pluginStatus -> !pluginStatus.isInstalled && !isKondoKit(pluginStatus.name) && (pluginStatus.name.contains(pluginSearchText, ignoreCase = true) || (pluginStatus.description?.contains(pluginSearchText, ignoreCase = true) ?: false)) } if (matchingPluginStatuses.isNotEmpty()) { val separator = JPanel().apply { background = VIEW_BACKGROUND_COLOR preferredSize = Dimension(Int.MAX_VALUE, 1) maximumSize = preferredSize } contentPanel.add(Box.createVerticalStrut(10)) contentPanel.add(separator) contentPanel.add(Box.createVerticalStrut(5)) val headerPanel = JPanel(FlowLayout(FlowLayout.LEFT)).apply { background = VIEW_BACKGROUND_COLOR add(JLabel("Available Plugins").apply { foreground = secondaryColor font = ViewConstants.FONT_RUNESCAPE_SMALL_BOLD_16 }) } contentPanel.add(headerPanel) contentPanel.add(Box.createVerticalStrut(5)) matchingPluginStatuses.forEach { pluginStatus -> val pluginPanel = createPluginStatusItemPanel(pluginStatus) contentPanel.add(pluginPanel) contentPanel.add(Box.createVerticalStrut(5)) } } } } contentPanel.revalidate() contentPanel.repaint() } private fun createPluginItemPanel(pluginInfo: PluginInfo, plugin: Plugin): JPanel { val panel = createPluginWidgetPanel().apply { layout = BorderLayout() } // Plugin name and version (using package name as in original implementation) val packageName = plugin.javaClass.`package`.name val nameLabel = JLabel(packageName) nameLabel.foreground = secondaryColor nameLabel.font = ViewConstants.FONT_RUNESCAPE_SMALL_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 = exposedFields.takeIf { it.isNotEmpty() }?.let { UiStyler.button(text = "Edit") { showPluginDetails(pluginInfo, plugin) } } // Plugin toggle switch (iOS style) - skip for KondoKit since it should always be on val toggleSwitch = if (!isKondoKit(packageName)) { createToggleSwitch(plugin, pluginInfo) } else { // Create a placeholder component that takes the same space but is invisible val placeholder = JPanel() placeholder.background = WIDGET_COLOR placeholder.setFixedSize(ViewConstants.TOGGLE_PLACEHOLDER_SIZE) placeholder.isVisible = false placeholder } val infoPanel = JPanel(BorderLayout()) infoPanel.background = WIDGET_COLOR infoPanel.add(nameLabel, BorderLayout.WEST) val controlsPanel = JPanel(FlowLayout(FlowLayout.RIGHT, 5, -3)) controlsPanel.background = WIDGET_COLOR editButton?.let { controlsPanel.add(it) } if (!isKondoKit(packageName)) { controlsPanel.add(toggleSwitch) } panel.add(infoPanel, BorderLayout.CENTER) panel.add(controlsPanel, BorderLayout.EAST) if (!isKondoKit(packageName)) { val popupMenu = createPluginContextMenu(plugin, pluginInfo, packageName, false) panel.attachPopupMenu(popupMenu) } return panel } private fun createDisabledPluginItemPanel(pluginName: String): JPanel { val panel = createPluginWidgetPanel().apply { layout = BorderLayout() } // Plugin name val nameLabel = JLabel(pluginName) nameLabel.foreground = secondaryColor nameLabel.font = ViewConstants.FONT_RUNESCAPE_SMALL_PLAIN_16 // Plugin toggle switch (iOS style) - initially off for disabled plugins val toggleSwitch = ToggleSwitch() toggleSwitch.setActivated(false) toggleSwitch.onToggleListener = { activated -> if (activated) { reflectiveEditorPlugin.enablePlugin(pluginName, mainPanel ?: JPanel()) } // If trying to disable an already disabled plugin, reset the toggle else { toggleSwitch.setActivated(false) } } // Layout val infoPanel = JPanel(BorderLayout()) infoPanel.background = WIDGET_COLOR infoPanel.add(nameLabel, BorderLayout.WEST) val controlsPanel = JPanel(FlowLayout(FlowLayout.RIGHT, 0, 0)) controlsPanel.background = WIDGET_COLOR controlsPanel.add(toggleSwitch) panel.add(infoPanel, BorderLayout.CENTER) panel.add(controlsPanel, BorderLayout.EAST) // Add right-click context menu (except for KondoKit itself) if (!isKondoKit(pluginName)) { val popupMenu = createPluginContextMenu(null, null, pluginName, true) panel.attachPopupMenu(popupMenu) } return panel } private fun createPluginStatusItemPanel(pluginStatus: PluginInfoWithStatus): JPanel { val panel = createPluginWidgetPanel().apply { layout = BorderLayout() } // Plugin name val nameLabel = JLabel(pluginStatus.name) nameLabel.foreground = secondaryColor nameLabel.font = ViewConstants.FONT_RUNESCAPE_SMALL_PLAIN_16 // Action button based on plugin status val actionButton = UiStyler.button() // Progress bar for downloads val progressBar = JProgressBar(0, 100) progressBar.isStringPainted = true progressBar.isVisible = false progressBar.string = "Downloading..." if (pluginStatus.isDownloading) { actionButton.isVisible = false progressBar.isVisible = true progressBar.value = pluginStatus.downloadProgress } else if (pluginStatus.isInstalled) { if (pluginStatus.needsUpdate) { actionButton.text = "Update" actionButton.addActionListener { // Start downloading the plugin to update it startPluginDownload(pluginStatus) } } else { actionButton.text = "Installed" actionButton.isEnabled = false } } else { actionButton.text = "Download" actionButton.addActionListener { // Start downloading the plugin startPluginDownload(pluginStatus) } } // Plugin toggle switch (iOS style) val toggleSwitch = ToggleSwitch() // Skip the toggle switch for KondoKit since it should always be enabled if (isKondoKit(pluginStatus.name)) { // Hide the toggle switch for KondoKit toggleSwitch.isVisible = false } else { toggleSwitch.setActivated(pluginStatus.isInstalled && !pluginStatus.needsUpdate) toggleSwitch.isEnabled = pluginStatus.isInstalled && !pluginStatus.needsUpdate if (pluginStatus.isInstalled && !pluginStatus.needsUpdate) { toggleSwitch.onToggleListener = { activated -> // Find the corresponding loaded plugin to use toggle functionality val loadedPluginsField = PluginRepository::class.java.getDeclaredField("loadedPlugins") loadedPluginsField.isAccessible = true val loadedPlugins = loadedPluginsField.get(null) as HashMap<*, *> var foundPlugin: Plugin? = null var foundPluginInfo: PluginInfo? = null for ((pluginInfo, plugin) in loadedPlugins) { if (getPluginDirName(plugin as Plugin) == pluginStatus.name) { foundPlugin = plugin foundPluginInfo = pluginInfo as PluginInfo break } } if (foundPlugin != null && foundPluginInfo != null) { reflectiveEditorPlugin.togglePlugin(foundPlugin, foundPluginInfo, toggleSwitch, activated, mainPanel ?: JPanel()) } else { // Fallback for plugins that don't have loaded instances available val pluginDirName = pluginStatus.name val sourceDir = if (activated) { // Moving from disabled to enabled File(File(reflectiveEditorPlugin.getPluginsDirectory(), "disabled"), pluginDirName) } else { // Moving from enabled to disabled File(reflectiveEditorPlugin.getPluginsDirectory(), pluginDirName) } val destDir = if (activated) { // Moving to main plugins directory File(reflectiveEditorPlugin.getPluginsDirectory(), pluginDirName) } else { // Moving to disabled directory val disabledDir = File(reflectiveEditorPlugin.getPluginsDirectory(), "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) } else { // Move the directory if (sourceDir.renameTo(destDir)) { showToast(mainPanel, if (activated) "Plugin enabled" else "Plugin disabled") // Schedule plugin reload to avoid crashes reflectiveEditorPlugin.setReloadPlugins(true) // 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) } } } } } else { // Hide the toggle switch for non-installed plugins toggleSwitch.isVisible = false } } val infoPanel = JPanel(BorderLayout()) infoPanel.background = WIDGET_COLOR infoPanel.add(nameLabel, BorderLayout.WEST) val controlsPanel = JPanel(FlowLayout(FlowLayout.RIGHT, 0, 0)) controlsPanel.background = WIDGET_COLOR controlsPanel.add(actionButton) controlsPanel.add(progressBar) if (toggleSwitch.isVisible) { controlsPanel.add(toggleSwitch) } if (!pluginStatus.isInstalled) { val tooltipText = buildInstallableTooltip(pluginStatus) panel.toolTipText = tooltipText actionButton.toolTipText = tooltipText progressBar.toolTipText = tooltipText } else { panel.toolTipText = null actionButton.toolTipText = null progressBar.toolTipText = null } panel.add(infoPanel, BorderLayout.CENTER) panel.add(controlsPanel, BorderLayout.EAST) if (!isKondoKit(pluginStatus.name) && pluginStatus.isInstalled) { val popupMenu = createPluginContextMenu(null, null, pluginStatus.name, !pluginStatus.needsUpdate) panel.attachPopupMenu(popupMenu) } return panel } private fun createPluginWidgetPanel(): WidgetPanel { return WidgetPanel( widgetWidth = ViewConstants.PLUGIN_LIST_ITEM_SIZE.width, widgetHeight = ViewConstants.PLUGIN_LIST_ITEM_SIZE.height, addDefaultPadding = false, paddingTop = 10, paddingLeft = 10, paddingBottom = 10, paddingRight = 10 ) } private fun buildInstallableTooltip(pluginStatus: PluginInfoWithStatus): String { val lines = mutableListOf() pluginStatus.remoteVersion?.takeIf { it.isNotBlank() }?.let { lines += "Version: ${escapeHtml(it)}" } pluginStatus.author?.takeIf { it.isNotBlank() }?.let { lines += "Author: ${escapeHtml(it)}" } pluginStatus.description?.takeIf { it.isNotBlank() }?.let { lines += escapeHtml(it).replace("\n", "
") } if (lines.isEmpty()) { lines += "Click Download to install this plugin." } return "${lines.joinToString("
")}" } private fun escapeHtml(value: String): String { return value .replace("&", "&") .replace("<", "<") .replace(">", ">") } private fun createToggleSwitch(plugin: Plugin, pluginInfo: PluginInfo): ToggleSwitch { val toggleSwitch = ToggleSwitch() // Set initial state toggleSwitch.setActivated(reflectiveEditorPlugin.isPluginEnabled(plugin, pluginInfo)) // Add toggle listener toggleSwitch.onToggleListener = { activated -> reflectiveEditorPlugin.togglePlugin(plugin, pluginInfo, toggleSwitch, activated, mainPanel ?: JPanel()) } return toggleSwitch } 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 showPluginDetails(pluginInfo: PluginInfo, plugin: Plugin) { currentPluginInfo = pluginInfo currentPlugin = plugin searchFieldWrapper?.isVisible = false // 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 val packageName = plugin.javaClass.`package`.name val headerPanel = ViewHeader("$packageName 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) } } searchFieldWrapper?.isVisible = true cardLayout.show(mainPanel, PLUGIN_LIST_VIEW) } 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) val infoPanel = JPanel(BorderLayout()) infoPanel.background = WIDGET_COLOR infoPanel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10) val infoText = """ Version: ${(pluginInfo.version)}
Author: ${(pluginInfo.author ?: "").trim('\'')}
Description: ${(pluginInfo.description ?: "").trim('\'')} """.trimIndent() val infoFont = ViewConstants.FONT_RUNESCAPE_SMALL_14 val infoLabel = LabelComponent(infoText, isHtml = true).apply { font = infoFont } infoPanel.add(infoLabel, BorderLayout.CENTER) val fm = infoLabel.getFontMetrics(infoFont) val avgCharWidth = fm.stringWidth("x") val availableWidth = 200 val charsPerLine = availableWidth / avgCharWidth val textContent = infoText.replace("<[^>]*>".toRegex(), "") val lines = textContent.split('\n').filter { it.isNotBlank() } var totalLines = 0 for (line in lines) { val lineLength = line.length totalLines += kotlin.math.ceil(lineLength.toDouble() / charsPerLine).toInt() } val lineHeight = fm.height val estimatedHeight = (totalLines * lineHeight) + 20 val finalHeight = kotlin.math.max(60, kotlin.math.min(estimatedHeight, 150)) infoPanel.maximumSize = Dimension(Int.MAX_VALUE, finalHeight) infoPanel.preferredSize = Dimension(ViewConstants.DEFAULT_WIDGET_SIZE.width, finalHeight) 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 } // Check if we need to reload plugins if (reflectiveEditorPlugin.shouldReloadPlugins()) { PluginRepository.reloadPlugins() reflectiveEditorPlugin.setReloadPlugins(false) } // Update plugin statuses to reflect current loaded plugins reflectiveEditorPlugin.updatePluginStatuses() val contentPanel = pluginListContentPanel if (contentPanel == null) { // Fallback path: rebuild the list view if it was not initialized yet val existingListView = mainPanel.components.find { it.name == PLUGIN_LIST_VIEW } val pluginListView = createPluginListView() pluginListView.name = PLUGIN_LIST_VIEW if (existingListView != null) { mainPanel.remove(existingListView) } mainPanel.add(pluginListView, PLUGIN_LIST_VIEW) } else { contentPanel.isVisible = false try { populatePluginListContent() } finally { contentPanel.isVisible = true } } searchFieldWrapper?.isVisible = true cardLayout.show(mainPanel, PLUGIN_LIST_VIEW) pluginListScrollablePanel?.let { scrollPanel -> SwingUtilities.invokeLater { resetScrollablePanel(scrollPanel) } } mainPanel.revalidate() mainPanel.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) } } } // Method to start downloading a plugin private fun startPluginDownload(pluginStatus: PluginInfoWithStatus) { reflectiveEditorPlugin.startPluginDownload(pluginStatus, mainPanel ?: JPanel()) } // Helper method to create context menu for plugins private fun createPluginContextMenu(plugin: Plugin?, pluginInfo: PluginInfo?, pluginName: String, isDisabled: Boolean): JPopupMenu { val popupMenu = PopupMenuComponent() // Add "Delete" menu item popupMenu.addMenuItem("Delete Plugin") { deletePlugin(pluginName, isDisabled) } return popupMenu } // Helper method to delete a plugin private fun deletePlugin(pluginName: String, isDisabled: Boolean) { reflectiveEditorPlugin.deletePlugin(pluginName, isDisabled, mainPanel ?: JPanel()) } // Helper method to recursively delete a directory private fun deleteRecursively(file: File): Boolean { if (file.isDirectory) { file.listFiles()?.forEach { child -> if (!deleteRecursively(child)) { return false } } } return file.delete() } // 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() } } }