More helpers/DRY

This commit is contained in:
downthecrop 2025-10-24 18:43:16 -07:00
parent 573e579643
commit 47e545cbc7
17 changed files with 264 additions and 289 deletions

View file

@ -3,6 +3,8 @@ package KondoKit.components
import KondoKit.Helpers
import KondoKit.Helpers.FieldNotifier
import KondoKit.ViewConstants
import KondoKit.components.UiStyler
import KondoKit.setFixedSize
import KondoKit.plugin.Companion.WIDGET_COLOR
import KondoKit.plugin.Companion.primaryColor
import KondoKit.plugin.Companion.secondaryColor
@ -10,6 +12,7 @@ import plugin.Plugin
import java.awt.*
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.lang.reflect.Field
import java.util.*
import java.util.Timer
import javax.swing.*
@ -36,18 +39,7 @@ class SettingsPanel(private val plugin: Plugin) : JPanel() {
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 description = field.exposedDescription()
val fieldPanel = JPanel().apply {
layout = GridBagLayout()
@ -104,7 +96,7 @@ class SettingsPanel(private val plugin: Plugin) : JPanel() {
}
// Check if the field is a HashMap
field.type == HashMap::class.java || field.type.simpleName == "HashMap" -> {
field.isHashMapType() -> {
inputComponent = createHashMapEditor(field, plugin, fieldNotifier)
isHashMapEditor = true
}
@ -141,42 +133,45 @@ class SettingsPanel(private val plugin: Plugin) : JPanel() {
fieldPanel.add(inputComponent, gbc)
// Add apply button for non-HashMap editors
val applyButton = JButton("\u2714").apply {
maximumSize = Dimension(Int.MAX_VALUE, 8)
val applyButton = UiStyler.button(
text = "\u2714",
onClick = {
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
)
}
}
).also {
it.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") {
if (!field.isHashMapType()) {
var previousValue = field.get(plugin)?.toString()
val timer = Timer()
timer.schedule(object : TimerTask() {
@ -205,7 +200,7 @@ class SettingsPanel(private val plugin: Plugin) : JPanel() {
}
}
private fun createHashMapEditor(field: java.lang.reflect.Field, plugin: Plugin, fieldNotifier: FieldNotifier): JComponent {
private fun createHashMapEditor(field: 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)
@ -213,19 +208,7 @@ class SettingsPanel(private val plugin: Plugin) : JPanel() {
editorPanel.border = BorderFactory.createEmptyBorder(5, 0, 5, 0)
editorPanel.maximumSize = Dimension(Int.MAX_VALUE, 250)
// 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 description = field.exposedDescription()
// Create title label
val titleLabel = JLabel("${field.name} (Key-Value Pairs)").apply {
@ -288,9 +271,7 @@ class SettingsPanel(private val plugin: Plugin) : JPanel() {
// 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
setFixedSize(Dimension(Int.MAX_VALUE, 150))
background = WIDGET_COLOR
verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED
horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_NEVER
@ -304,9 +285,9 @@ class SettingsPanel(private val plugin: Plugin) : JPanel() {
}
// Add button
val addButton = JButton("+").apply {
maximumSize = Dimension(40, 30)
addActionListener {
val addButton = UiStyler.button(
text = "+",
onClick = {
val vector = java.util.Vector<Any>()
vector.add("")
vector.add("")
@ -317,12 +298,14 @@ class SettingsPanel(private val plugin: Plugin) : JPanel() {
table.editCellAt(newRow, 0)
table.editorComponent?.requestFocus()
}
).apply {
setFixedSize(40, 30)
}
// Remove button
val removeButton = JButton("-").apply {
maximumSize = Dimension(40, 30)
addActionListener {
val removeButton = UiStyler.button(
text = "-",
onClick = {
val selectedRow = table.selectedRow
if (selectedRow >= 0) {
tableModel.removeRow(selectedRow)
@ -334,51 +317,53 @@ class SettingsPanel(private val plugin: Plugin) : JPanel() {
)
}
}
).apply {
setFixedSize(40, 30)
}
// Apply button
val applyButton = JButton("Apply Changes").apply {
maximumSize = Dimension(120, 30)
addActionListener {
val applyButton = UiStyler.button(
text = "Apply Changes",
onClick = {
try {
// Commit any active cell editing before reading values
if (table.isEditing) {
table.cellEditor.stopCellEditing()
}
// Get the current HashMap from the field and modify it in place
val currentHashMap = field.get(plugin) as? HashMap<Any, Any> ?: HashMap<Any, Any>()
// Clear the current HashMap
currentHashMap.clear()
// Add the new entries from the table to the existing 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()) {
// Skip empty values
if (value.isBlank()) {
Helpers.showToast(
this@SettingsPanel,
"Skipping entry with empty value for key '$key'",
JOptionPane.WARNING_MESSAGE
)
continue
}
// Try to convert the value to the appropriate type based on the field's generic type
val convertedValue = try {
// For HashMap<String, Int> - treat all HashMap<String, Int> fields the same way regardless of plugin
val fieldTypeName = field.genericType.toString()
if (fieldTypeName.contains("java.util.HashMap<java.lang.String, java.lang.Integer>") ||
fieldTypeName.contains("HashMap<String, Int>") ||
fieldTypeName.contains("HashMap<String, Integer>")) {
if (key.isBlank()) {
continue
}
if (value.isBlank()) {
Helpers.showToast(
this@SettingsPanel,
"Skipping entry with empty value for key '$key'",
JOptionPane.WARNING_MESSAGE
)
continue
}
val convertedValue = try {
val fieldTypeName = field.genericType.toString()
when {
fieldTypeName.contains("java.util.HashMap<java.lang.String, java.lang.Integer>") ||
fieldTypeName.contains("HashMap<String, Int>") ||
fieldTypeName.contains("HashMap<String, Integer>") -> {
try {
val intValue = value.toInt()
intValue
} catch (e: NumberFormatException) {
value.toInt()
} catch (numberFormat: NumberFormatException) {
Helpers.showToast(
this@SettingsPanel,
"Invalid number format for key '$key': '$value'. Using 0 as default.",
@ -386,38 +371,20 @@ class SettingsPanel(private val plugin: Plugin) : JPanel() {
)
0
}
}
// For other numeric types
else if (fieldTypeName.contains("java.lang.Integer") ||
fieldTypeName.contains("int")) {
value.toInt()
}
else if (fieldTypeName.contains("java.lang.Double") ||
fieldTypeName.contains("double")) {
value.toDouble()
}
else if (fieldTypeName.contains("java.lang.Float") ||
fieldTypeName.contains("float")) {
value.toFloat()
}
else if (fieldTypeName.contains("java.lang.Boolean") ||
fieldTypeName.contains("boolean")) {
value.toBoolean()
}
// Default to string for other types
else {
value
}
} catch (e: Exception) {
// If conversion fails, keep as string
value
fieldTypeName.contains("java.lang.Integer") || fieldTypeName.contains("int") -> value.toInt()
fieldTypeName.contains("java.lang.Double") || fieldTypeName.contains("double") -> value.toDouble()
fieldTypeName.contains("java.lang.Float") || fieldTypeName.contains("float") -> value.toFloat()
fieldTypeName.contains("java.lang.Boolean") || fieldTypeName.contains("boolean") -> value.toBoolean()
else -> value
}
currentHashMap[key] = convertedValue
} catch (e: Exception) {
value
}
currentHashMap[key] = convertedValue
}
// Update the field to trigger notifications (even though the reference is the same)
// This ensures OnKondoValueUpdated() gets called if it exists
fieldNotifier.setFieldValue(field, currentHashMap)
Helpers.showToast(
this@SettingsPanel,
@ -431,6 +398,8 @@ class SettingsPanel(private val plugin: Plugin) : JPanel() {
)
}
}
).apply {
setFixedSize(120, 30)
}
buttonsPanel.add(addButton)
@ -447,7 +416,24 @@ class SettingsPanel(private val plugin: Plugin) : JPanel() {
return editorPanel
}
private fun Field.exposedDescription(): String {
val exposedAnnotation = annotations.firstOrNull {
it.annotationClass.simpleName == "Exposed"
} ?: return ""
return try {
val descriptionMethod = exposedAnnotation.annotationClass.java.getMethod("description")
descriptionMethod.invoke(exposedAnnotation) as? String ?: ""
} catch (_: Exception) {
""
}
}
private fun Field.isHashMapType(): Boolean {
return type == HashMap::class.java || type.simpleName == "HashMap"
}
companion object {
var customToolTipWindow: JWindow? = null