From ac2478d10fb7d3f916a3f28a75b9af49f0d3e706 Mon Sep 17 00:00:00 2001 From: downthecrop Date: Wed, 13 Aug 2025 02:19:12 -0700 Subject: [PATCH] Little vibe session with Qwen3-coder for a more 'runelite' settings manager --- .../kotlin/KondoKit/ReflectiveEditorView.kt | 401 ++++++++++++++---- 1 file changed, 313 insertions(+), 88 deletions(-) diff --git a/plugin-playground/src/main/kotlin/KondoKit/ReflectiveEditorView.kt b/plugin-playground/src/main/kotlin/KondoKit/ReflectiveEditorView.kt index 5b6cfd2..2e5409c 100644 --- a/plugin-playground/src/main/kotlin/KondoKit/ReflectiveEditorView.kt +++ b/plugin-playground/src/main/kotlin/KondoKit/ReflectiveEditorView.kt @@ -2,6 +2,7 @@ package KondoKit import KondoKit.Helpers.convertValue import KondoKit.Helpers.showToast +import KondoKit.ScrollablePanel import KondoKit.plugin.Companion.TITLE_BAR_COLOR import KondoKit.plugin.Companion.TOOLTIP_BACKGROUND import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR @@ -18,6 +19,7 @@ import java.awt.event.MouseEvent import java.util.* import java.util.Timer import javax.swing.* +import javax.swing.border.AbstractBorder import kotlin.math.ceil /* @@ -32,85 +34,275 @@ object ReflectiveEditorView { 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" + + // 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 + fun createReflectiveEditorView() { - val reflectiveEditorPanel = JPanel(BorderLayout()) - reflectiveEditorPanel.background = VIEW_BACKGROUND_COLOR - reflectiveEditorPanel.add(Box.createVerticalStrut(5)) - reflectiveEditorPanel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10) - reflectiveEditorView = reflectiveEditorPanel - addPlugins(reflectiveEditorView!!) + // 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) + + // 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 = JPanel(BorderLayout()) + pluginDetailView.name = PLUGIN_DETAIL_VIEW + pluginDetailView.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) } - fun addPlugins(reflectiveEditorView: JPanel) { - reflectiveEditorView.removeAll() // clear previous - loadedPlugins.clear() + private fun createPluginListView(): JPanel { + val panel = JPanel() + panel.layout = BoxLayout(panel, BoxLayout.Y_AXIS) + panel.background = VIEW_BACKGROUND_COLOR + try { val loadedPluginsField = PluginRepository::class.java.getDeclaredField("loadedPlugins") loadedPluginsField.isAccessible = true val loadedPlugins = loadedPluginsField.get(null) as HashMap<*, *> for ((pluginInfo, plugin) in loadedPlugins) { - addPluginToEditor(reflectiveEditorView, pluginInfo as PluginInfo, plugin as Plugin) + val pluginPanel = createPluginItemPanel(pluginInfo as PluginInfo, plugin as Plugin) + panel.add(pluginPanel) + panel.add(Box.createVerticalStrut(5)) } } catch (e: Exception) { e.printStackTrace() } - - // Add a centered box for plugins that have no exposed fields - if (loadedPlugins.isNotEmpty()) { - val noExposedPanel = JPanel(BorderLayout()) - noExposedPanel.background = VIEW_BACKGROUND_COLOR - noExposedPanel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10) - - val label = JLabel("Loaded Plugins without Exposed Fields", SwingConstants.CENTER) - label.font = Font("RuneScape Small", Font.PLAIN, 16) - label.foreground = primaryColor - noExposedPanel.add(label, BorderLayout.NORTH) - - val pluginsList = JList(loadedPlugins.toTypedArray()) - pluginsList.background = WIDGET_COLOR - pluginsList.foreground = secondaryColor - pluginsList.font = Font("RuneScape Small", Font.PLAIN, 16) - - // Wrap the JList in a JScrollPane with a fixed height - val maxScrollPaneHeight = 200 - val scrollPane = JScrollPane(pluginsList).apply { - verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED - horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_NEVER - } - - // Create a wrapper panel with BoxLayout to constrain the scroll pane - val scrollPaneWrapper = JPanel().apply { - layout = BoxLayout(this, BoxLayout.Y_AXIS) - add(scrollPane) - } - - noExposedPanel.add(scrollPaneWrapper, BorderLayout.CENTER) - - // Center the panel within the reflectiveEditorView - val centeredPanel = JPanel().apply { - preferredSize = Dimension(240, maxScrollPaneHeight) - maximumSize = preferredSize - minimumSize = preferredSize - } - centeredPanel.layout = BoxLayout(centeredPanel, BoxLayout.Y_AXIS) - centeredPanel.add(Box.createVerticalGlue()) - centeredPanel.add(noExposedPanel) - centeredPanel.add(Box.createVerticalGlue()) - - reflectiveEditorView.add(Box.createVerticalStrut(10)) - reflectiveEditorView.add(centeredPanel) + + // 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) } - - - reflectiveEditorView.revalidate() - if(focusedView == VIEW_NAME) - reflectiveEditorView.repaint() + + val container = JPanel(BorderLayout()) + container.background = VIEW_BACKGROUND_COLOR + container.border = BorderFactory.createEmptyBorder(10, 5, 10, 5) + container.add(scrollablePanel, BorderLayout.CENTER) + + return container } - - private fun addPluginToEditor(reflectiveEditorView: JPanel, pluginInfo : PluginInfo, plugin: Plugin) { - reflectiveEditorView.layout = BoxLayout(reflectiveEditorView, BoxLayout.Y_AXIS) - + + private fun createPluginItemPanel(pluginInfo: PluginInfo, plugin: Plugin): JPanel { + val panel = JPanel(BorderLayout()) + panel.background = WIDGET_COLOR + panel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10) + panel.maximumSize = Dimension(Int.MAX_VALUE, 60) + // Remove fixed preferredSize to allow flexible width + // panel.preferredSize = Dimension(220, 60) + + // Plugin name and version (using package name as in original implementation) + val packageName = plugin.javaClass.`package`.name + val nameLabel = JLabel("$packageName v${pluginInfo.version}") + nameLabel.foreground = secondaryColor + nameLabel.font = Font("RuneScape Small", Font.BOLD, 16) + + // Plugin toggle switch (iOS style) + val toggleSwitch = createToggleSwitch(plugin, pluginInfo) + + // Edit button + val editButton = JButton("Edit") + editButton.background = TITLE_BAR_COLOR + editButton.foreground = secondaryColor + editButton.font = Font("RuneScape Small", Font.PLAIN, 14) + editButton.addActionListener { + showPluginDetails(pluginInfo, plugin) + } + + // Layout + val infoPanel = JPanel(BorderLayout()) + infoPanel.background = WIDGET_COLOR + infoPanel.add(nameLabel, BorderLayout.WEST) + + val controlsPanel = JPanel(FlowLayout(FlowLayout.RIGHT, 5, 0)) + controlsPanel.background = WIDGET_COLOR + controlsPanel.add(toggleSwitch) + controlsPanel.add(editButton) + + panel.add(infoPanel, BorderLayout.CENTER) + panel.add(controlsPanel, BorderLayout.EAST) + + return panel + } + + private fun createToggleSwitch(plugin: Plugin, pluginInfo: PluginInfo): JPanel { + val switchPanel = JPanel(null) // null layout for precise positioning + switchPanel.background = WIDGET_COLOR + switchPanel.preferredSize = Dimension(50, 25) + switchPanel.maximumSize = Dimension(50, 25) + switchPanel.minimumSize = Dimension(50, 25) + + // Track (background) + val track = JPanel() + track.background = if (isPluginEnabled(pluginInfo)) Color(0, 122, 255) else Color(142, 142, 147) + track.setBounds(0, 0, 50, 25) + track.border = RoundedBorder(12) + + // Thumb (slider) + val thumb = JPanel() + thumb.background = Color.WHITE + thumb.setBounds(if (isPluginEnabled(pluginInfo)) 27 else 3, 3, 20, 20) + thumb.border = RoundedBorder(10) + + switchPanel.add(track) + switchPanel.add(thumb) + + // Add click handler to toggle + switchPanel.addMouseListener(object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + togglePlugin(pluginInfo, plugin, track, thumb) + } + }) + + return switchPanel + } + + private fun isPluginEnabled(pluginInfo: PluginInfo): Boolean { + // For now, we'll assume all loaded plugins are enabled + // In a more complete implementation, you might track enabled/disabled state + return true + } + + private fun togglePlugin(pluginInfo: PluginInfo, plugin: Plugin, track: JPanel, thumb: JPanel) { + // Simple toggle animation + val isEnabled = track.background == Color(0, 122, 255) + + if (isEnabled) { + // Toggle off + track.background = Color(142, 142, 147) + thumb.setLocation(3, 3) + } else { + // Toggle on + track.background = Color(0, 122, 255) + thumb.setLocation(27, 3) + } + + track.repaint() + thumb.repaint() + + // In a full implementation, you would disable/enable the plugin here + showToast(mainPanel, if (isEnabled) "Plugin disabled" else "Plugin enabled") + } + + 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 = JPanel(BorderLayout()) + detailView.name = PLUGIN_DETAIL_VIEW + detailView.background = VIEW_BACKGROUND_COLOR + + // Header with back button + val headerPanel = JPanel(BorderLayout()) + headerPanel.background = TITLE_BAR_COLOR + headerPanel.preferredSize = Dimension(Int.MAX_VALUE, 40) + headerPanel.border = BorderFactory.createEmptyBorder(5, 10, 5, 10) + + val backButton = JButton("◀ Back") + backButton.background = TITLE_BAR_COLOR + backButton.foreground = secondaryColor + backButton.font = Font("RuneScape Small", Font.PLAIN, 14) + backButton.addActionListener { + // 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 + val titleLabel = JLabel("$packageName Settings") + titleLabel.foreground = secondaryColor + titleLabel.font = Font("RuneScape Small", Font.BOLD, 16) + titleLabel.horizontalAlignment = SwingConstants.CENTER + + headerPanel.add(backButton, BorderLayout.WEST) + headerPanel.add(titleLabel, BorderLayout.CENTER) + + // 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 = 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 = JLabel("$infoText") + infoLabel.foreground = secondaryColor + infoLabel.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 fieldNotifier = Helpers.FieldNotifier(plugin) val exposedFields = plugin.javaClass.declaredFields.filter { field -> field.annotations.any { annotation -> @@ -119,22 +311,6 @@ object ReflectiveEditorView { } if (exposedFields.isNotEmpty()) { - - val packageName = plugin.javaClass.`package`.name - val version = pluginInfo.version - val labelPanel = JPanel(BorderLayout()) - labelPanel.maximumSize = Dimension(Int.MAX_VALUE, 30) - labelPanel.background = VIEW_BACKGROUND_COLOR - labelPanel.border = BorderFactory.createEmptyBorder(5, 0, 0, 0) - - val label = JLabel("$packageName v$version", SwingConstants.CENTER) - label.foreground = primaryColor - label.font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16) - labelPanel.add(label, BorderLayout.CENTER) - label.isOpaque = true - label.background = TITLE_BAR_COLOR - reflectiveEditorView.add(labelPanel) - for (field in exposedFields) { field.isAccessible = true @@ -157,7 +333,7 @@ object ReflectiveEditorView { fieldPanel.background = WIDGET_COLOR fieldPanel.foreground = secondaryColor fieldPanel.border = BorderFactory.createEmptyBorder(5, 0, 5, 0) - fieldPanel.maximumSize = Dimension(Int.MAX_VALUE, 40) + fieldPanel.maximumSize = Dimension(Int.MAX_VALUE, 50) val gbc = GridBagConstraints() gbc.insets = Insets(0, 5, 0, 5) @@ -224,12 +400,12 @@ object ReflectiveEditorView { } fieldNotifier.setFieldValue(field, newValue) showToast( - reflectiveEditorView, + mainPanel, "${field.name} updated successfully!" ) } catch (e: Exception) { showToast( - reflectiveEditorView, + mainPanel, "Failed to update ${field.name}: ${e.message}", JOptionPane.ERROR_MESSAGE ) @@ -237,7 +413,7 @@ object ReflectiveEditorView { } fieldPanel.add(applyButton, gbc) - reflectiveEditorView.add(fieldPanel) + contentPanel.add(fieldPanel) // Track field changes in real-time and update UI var previousValue = field.get(plugin)?.toString() @@ -262,12 +438,16 @@ object ReflectiveEditorView { } if (exposedFields.isNotEmpty()) { - reflectiveEditorView.add(Box.createVerticalStrut(5)) + contentPanel.add(Box.createVerticalStrut(5)) } } - else { - loadedPlugins.add(plugin.javaClass.`package`.name) - } + } + + fun addPlugins(reflectiveEditorView: JPanel) { + // This function is now deprecated with the new view system + // but kept for compatibility + // In the new system, we don't need to do anything here + // as the views are managed by our internal card layout } var customToolTipWindow: JWindow? = null @@ -321,4 +501,49 @@ object ReflectiveEditorView { 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) + } + } } \ No newline at end of file