rt4-client/plugin-playground/src/main/kotlin/KondoKit/components/SettingsPanel.kt
2025-09-19 21:58:16 -07:00

419 lines
No EOL
19 KiB
Kotlin

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<out Enum<*>>).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<Any, Any>()
// Create a table model for the HashMap
val tableModel = object : DefaultTableModel() {
init {
val columnVector = java.util.Vector<String>()
columnVector.add("Key")
columnVector.add("Value")
columnIdentifiers = columnVector
hashMap.forEach { (key, value) ->
val vector = java.util.Vector<Any>()
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<Any>()
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<String, Any>()
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("<html><div style='color: $textColor; background-color: $bgColor; padding: 3px; word-break: break-all;'>$text</div></html>").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 = "<html><div style='color: $textColor; background-color: $bgColor; padding: 3px; word-break: break-all;'>$text</div></html>"
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
}
}
}