package KondoKit.components import KondoKit.Helpers import KondoKit.Helpers.FieldNotifier import KondoKit.plugin.Companion.WIDGET_COLOR import KondoKit.plugin.Companion.primaryColor import KondoKit.plugin.Companion.secondaryColor import plugin.Plugin import java.awt.* import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import java.util.* import java.util.Timer import javax.swing.* import javax.swing.table.DefaultTableModel class SettingsPanel(private val plugin: Plugin) : JPanel() { init { layout = BoxLayout(this, BoxLayout.Y_AXIS) background = WIDGET_COLOR border = BorderFactory.createEmptyBorder(10, 10, 10, 10) } fun addSettingsFromPlugin() { val fieldNotifier = FieldNotifier(plugin) val exposedFields = plugin.javaClass.declaredFields.filter { field -> field.annotations.any { annotation -> annotation.annotationClass.simpleName == "Exposed" } } if (exposedFields.isNotEmpty()) { for (field in exposedFields) { field.isAccessible = true // Get the "Exposed" annotation specifically and retrieve its description, if available val exposedAnnotation = field.annotations.firstOrNull { annotation -> annotation.annotationClass.simpleName == "Exposed" } val description = exposedAnnotation?.let { annotation -> try { val descriptionField = annotation.annotationClass.java.getMethod("description") descriptionField.invoke(annotation) as String } catch (e: NoSuchMethodException) { "" // No description method, return empty string } } ?: "" val fieldPanel = JPanel().apply { layout = GridBagLayout() background = WIDGET_COLOR foreground = secondaryColor border = BorderFactory.createEmptyBorder(5, 0, 5, 0) maximumSize = Dimension(Int.MAX_VALUE, 50) } val gbc = GridBagConstraints().apply { insets = Insets(0, 5, 0, 5) } val label = JLabel(field.name.capitalize()).apply { foreground = secondaryColor font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16) } gbc.gridx = 0 gbc.gridy = 0 gbc.weightx = 0.0 gbc.anchor = GridBagConstraints.WEST fieldPanel.add(label, gbc) // Create appropriate input component based on field type val inputComponent: JComponent val isHashMapEditor: Boolean when { field.type == Boolean::class.javaPrimitiveType || field.type == java.lang.Boolean::class.java -> { inputComponent = JCheckBox().apply { isSelected = field.get(plugin) as Boolean } isHashMapEditor = false } field.type.isEnum -> { val enumConstants = field.type.enumConstants inputComponent = JComboBox(enumConstants as Array>).apply { selectedItem = field.get(plugin) } isHashMapEditor = false } field.type == Int::class.javaPrimitiveType || field.type == Integer::class.java -> { inputComponent = JSpinner(SpinnerNumberModel(field.get(plugin) as Int, Int.MIN_VALUE, Int.MAX_VALUE, 1)) isHashMapEditor = false } field.type == Float::class.javaPrimitiveType || field.type == Double::class.javaPrimitiveType || field.type == java.lang.Float::class.java || field.type == java.lang.Double::class.java -> { inputComponent = JSpinner(SpinnerNumberModel((field.get(plugin) as Number).toDouble(), -Double.MAX_VALUE, Double.MAX_VALUE, 0.1)) isHashMapEditor = false } // Check if the field is a HashMap field.type == HashMap::class.java || field.type.simpleName == "HashMap" -> { inputComponent = createHashMapEditor(field, plugin, fieldNotifier) isHashMapEditor = true } else -> { inputComponent = JTextField(field.get(plugin)?.toString() ?: "") isHashMapEditor = false } } // Handle HashMap editors differently - they don't use the fieldPanel layout if (isHashMapEditor) { // Add the HashMap editor directly to the settings panel add(inputComponent) } else { // Add mouse listener to the label only if a description is available if (description.isNotBlank()) { label.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) label.addMouseListener(object : MouseAdapter() { override fun mouseEntered(e: MouseEvent) { showCustomToolTip(description, label) } override fun mouseExited(e: MouseEvent) { customToolTipWindow?.isVisible = false } }) } gbc.gridx = 1 gbc.gridy = 0 gbc.weightx = 1.0 gbc.fill = GridBagConstraints.HORIZONTAL fieldPanel.add(inputComponent, gbc) // Add apply button for non-HashMap editors val applyButton = JButton("\u2714").apply { maximumSize = Dimension(Int.MAX_VALUE, 8) } gbc.gridx = 2 gbc.gridy = 0 gbc.weightx = 0.0 gbc.fill = GridBagConstraints.NONE applyButton.addActionListener { try { val newValue = when (inputComponent) { is JCheckBox -> inputComponent.isSelected is JComboBox<*> -> inputComponent.selectedItem is JSpinner -> inputComponent.value is JTextField -> Helpers.convertValue(field.type, field.genericType, inputComponent.text) else -> throw IllegalArgumentException("Unsupported input component type") } fieldNotifier.setFieldValue(field, newValue) Helpers.showToast( this@SettingsPanel, "${field.name} updated successfully!" ) } catch (e: Exception) { Helpers.showToast( this@SettingsPanel, "Failed to update ${field.name}: ${e.message}", JOptionPane.ERROR_MESSAGE ) } } fieldPanel.add(applyButton, gbc) add(fieldPanel) } // Track field changes in real-time and update UI (skip HashMap fields) if (field.type != HashMap::class.java && field.type.simpleName != "HashMap") { var previousValue = field.get(plugin)?.toString() val timer = Timer() timer.schedule(object : TimerTask() { override fun run() { val currentValue = field.get(plugin)?.toString() if (currentValue != previousValue) { previousValue = currentValue SwingUtilities.invokeLater { // Update the inputComponent based on the new value when (inputComponent) { is JCheckBox -> inputComponent.isSelected = field.get(plugin) as Boolean is JComboBox<*> -> inputComponent.selectedItem = field.get(plugin) is JSpinner -> inputComponent.value = field.get(plugin) is JTextField -> inputComponent.text = field.get(plugin)?.toString() ?: "" } } } } }, 0, 1000) // Poll every 1000 milliseconds (1 second) } } if (exposedFields.isNotEmpty()) { add(Box.createVerticalStrut(5)) } } } private fun createHashMapEditor(field: java.lang.reflect.Field, plugin: Plugin, fieldNotifier: FieldNotifier): JComponent { // Create a panel to hold the table and buttons val editorPanel = JPanel() editorPanel.layout = BoxLayout(editorPanel, BoxLayout.Y_AXIS) editorPanel.background = WIDGET_COLOR editorPanel.border = BorderFactory.createEmptyBorder(5, 0, 5, 0) editorPanel.maximumSize = Dimension(Int.MAX_VALUE, 250) // Create title label val titleLabel = JLabel("${field.name} (Key-Value Pairs)").apply { foreground = secondaryColor font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16) alignmentX = Component.LEFT_ALIGNMENT } // Get the current HashMap value val hashMap = field.get(plugin) as? HashMap<*, *> ?: HashMap() // Create a table model for the HashMap val tableModel = object : DefaultTableModel() { init { val columnVector = java.util.Vector() columnVector.add("Key") columnVector.add("Value") columnIdentifiers = columnVector hashMap.forEach { (key, value) -> val vector = java.util.Vector() vector.add(key.toString()) vector.add(value.toString()) addRow(vector) } } override fun isCellEditable(row: Int, column: Int): Boolean { return true } override fun getColumnClass(columnIndex: Int): Class<*> { return String::class.java } } // Create the table val table = JTable(tableModel).apply { background = WIDGET_COLOR foreground = secondaryColor gridColor = primaryColor tableHeader.background = WIDGET_COLOR tableHeader.foreground = secondaryColor preferredScrollableViewportSize = Dimension(Int.MAX_VALUE, 150) setFillsViewportHeight(true) } // Create a scroll pane for the table with fixed size val scrollPane = JScrollPane(table).apply { preferredSize = Dimension(Int.MAX_VALUE, 150) maximumSize = preferredSize minimumSize = preferredSize background = WIDGET_COLOR verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_NEVER alignmentX = Component.LEFT_ALIGNMENT } // Create buttons panel val buttonsPanel = JPanel(FlowLayout(FlowLayout.LEFT, 5, 5)).apply { background = WIDGET_COLOR alignmentX = Component.LEFT_ALIGNMENT } // Add button val addButton = JButton("+").apply { maximumSize = Dimension(40, 30) addActionListener { val vector = java.util.Vector() vector.add("") vector.add("") tableModel.addRow(vector) // Select the new row for editing val newRow = tableModel.rowCount - 1 table.setRowSelectionInterval(newRow, newRow) table.editCellAt(newRow, 0) table.editorComponent?.requestFocus() } } // Remove button val removeButton = JButton("-").apply { maximumSize = Dimension(40, 30) addActionListener { val selectedRow = table.selectedRow if (selectedRow >= 0) { tableModel.removeRow(selectedRow) } else { Helpers.showToast( this@SettingsPanel, "Please select a row to remove", JOptionPane.WARNING_MESSAGE ) } } } // Apply button val applyButton = JButton("Apply Changes").apply { maximumSize = Dimension(220, 30) addActionListener { try { // Create a new HashMap from the table data // We need to determine the key and value types from the field's generic type val newHashMap = HashMap() for (i in 0 until tableModel.rowCount) { val key = tableModel.getValueAt(i, 0).toString() val value = tableModel.getValueAt(i, 1).toString() // Only add non-empty keys if (key.isNotBlank()) { // Try to convert the value to the appropriate type val convertedValue = try { // For now, we'll keep everything as strings // In the future, we could parse based on the generic type information value } catch (e: Exception) { value // fallback to string } newHashMap[key] = convertedValue } } // Update the field with the new HashMap fieldNotifier.setFieldValue(field, newHashMap) Helpers.showToast( this@SettingsPanel, "${field.name} updated successfully!" ) } catch (e: Exception) { Helpers.showToast( this@SettingsPanel, "Failed to update ${field.name}: ${e.message}", JOptionPane.ERROR_MESSAGE ) } } } buttonsPanel.add(addButton) buttonsPanel.add(removeButton) buttonsPanel.add(Box.createHorizontalStrut(20)) buttonsPanel.add(applyButton) // Add components to the editor panel in vertical stack editorPanel.add(titleLabel) editorPanel.add(Box.createVerticalStrut(5)) editorPanel.add(scrollPane) editorPanel.add(Box.createVerticalStrut(5)) editorPanel.add(buttonsPanel) return editorPanel } companion object { 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 = Math.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(KondoKit.plugin.Companion.TOOLTIP_BACKGROUND) val textColor = Helpers.colorToHex(KondoKit.plugin.Companion.secondaryColor) contentPane = JLabel("
$text
").apply { border = BorderFactory.createLineBorder(Color.BLACK) isOpaque = true background = KondoKit.plugin.Companion.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(KondoKit.plugin.Companion.TOOLTIP_BACKGROUND) val textColor = Helpers.colorToHex(KondoKit.plugin.Companion.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 } } }