Table editor

This commit is contained in:
downthecrop 2025-09-19 21:58:16 -07:00
parent 50e03baea7
commit 5e4ee2a5da
4 changed files with 307 additions and 85 deletions

View file

@ -1,6 +1,8 @@
package FooTextPlugin package FooTextPlugin
import KondoKit.Exposed
import plugin.Plugin import plugin.Plugin
import plugin.api.API
import rt4.Component import rt4.Component
import rt4.JagString import rt4.JagString
import rt4.Player import rt4.Player
@ -32,8 +34,15 @@ class plugin : Plugin() {
3 to "<img=6>", // UIM 3 to "<img=6>", // UIM
) )
private var ACCOUNT_TYPE = 0 // Local storage for username matches to avoid API calls
@Exposed(description = "Username to account type mappings (0=Normal, 1=IM, 2=HCIM, 3=UIM)")
private var usernameMatches = HashMap<String, Int>()
// Not used anymore - keeping for reference
// @Exposed(description = "Manual username entries (format: username:accountType, e.g., 'example:1')")
// private var manualEntries = listOf<String>()
private var ACCOUNT_TYPE = 0
private var component: Component? = null private var component: Component? = null
@ -234,6 +243,19 @@ class plugin : Plugin() {
'\u00FF' to "-X" '\u00FF' to "-X"
) )
override fun Init() {
// Load username matches from local storage
usernameMatches = API.GetData("foo-text-username-matches") as? HashMap<String, Int> ?: HashMap()
}
fun OnKondoValueUpdated() {
StoreData()
}
private fun StoreData() {
API.StoreData("foo-text-username-matches", usernameMatches)
}
override fun Draw(timeDelta: Long) { override fun Draw(timeDelta: Long) {
if (component != null && component?.id == TARGET_COMPONENT_ID) { if (component != null && component?.id == TARGET_COMPONENT_ID) {
val username = Player.usernameInput.toString().toLowerCase() val username = Player.usernameInput.toString().toLowerCase()
@ -267,19 +289,31 @@ class plugin : Plugin() {
val reader = BufferedReader(InputStreamReader(connection.inputStream)) val reader = BufferedReader(InputStreamReader(connection.inputStream))
val response = reader.use { it.readText() } val response = reader.use { it.readText() }
reader.close() reader.close()
updatePlayerData(response) updatePlayerData(response, cleanUsername)
} }
} catch (_: Exception) { } } catch (_: Exception) { }
}.start() }.start()
} }
private fun updatePlayerData(jsonResponse: String) { private fun updatePlayerData(jsonResponse: String, username: String) {
val hiscoresResponse = gson.fromJson(jsonResponse, HiscoresResponse::class.java) val hiscoresResponse = gson.fromJson(jsonResponse, HiscoresResponse::class.java)
ACCOUNT_TYPE = hiscoresResponse.info.iron_mode.toInt() ACCOUNT_TYPE = hiscoresResponse.info.iron_mode.toInt()
println("Resolved you are account type: $ACCOUNT_TYPE") println("Resolved you are account type: $ACCOUNT_TYPE")
// Store the result in our local cache
usernameMatches[username] = ACCOUNT_TYPE
StoreData()
} }
override fun OnLogin() { override fun OnLogin() {
// Check if we already have the account type for this user
val cleanUsername = Player.usernameInput.toString().toLowerCase().replace(" ", "_")
if (usernameMatches.containsKey(cleanUsername)) {
ACCOUNT_TYPE = usernameMatches[cleanUsername]!!
println("Using cached account type: $ACCOUNT_TYPE for $cleanUsername")
return
}
// The server doesn't tell us what account type we are. // The server doesn't tell us what account type we are.
// Requesting from API is the easier way to tell. // Requesting from API is the easier way to tell.
fetchAccountTypeFromAPI() fetchAccountTypeFromAPI()

View file

@ -1,3 +1,3 @@
AUTHOR=YourName AUTHOR=YourName
DESCRIPTION=Replaces the username part of Component Index: 51 ID: 8978483 with "foo" DESCRIPTION=Displays account type icons next to usernames in chat. Uses local storage to cache results and avoid API calls. Supports manual entries for players not on live server.
VERSION=1.0 VERSION=1.0

View file

@ -12,6 +12,7 @@ import java.awt.event.MouseEvent
import java.util.* import java.util.*
import java.util.Timer import java.util.Timer
import javax.swing.* import javax.swing.*
import javax.swing.table.DefaultTableModel
class SettingsPanel(private val plugin: Plugin) : JPanel() { class SettingsPanel(private val plugin: Plugin) : JPanel() {
@ -70,103 +71,131 @@ class SettingsPanel(private val plugin: Plugin) : JPanel() {
fieldPanel.add(label, gbc) fieldPanel.add(label, gbc)
// Create appropriate input component based on field type // Create appropriate input component based on field type
val inputComponent: JComponent = when { val inputComponent: JComponent
field.type == Boolean::class.javaPrimitiveType || field.type == java.lang.Boolean::class.java -> val isHashMapEditor: Boolean
JCheckBox().apply { when {
field.type == Boolean::class.javaPrimitiveType || field.type == java.lang.Boolean::class.java -> {
inputComponent = JCheckBox().apply {
isSelected = field.get(plugin) as Boolean isSelected = field.get(plugin) as Boolean
} }
isHashMapEditor = false
}
field.type.isEnum -> field.type.isEnum -> {
JComboBox((field.type.enumConstants as Array<Enum<*>>)).apply { val enumConstants = field.type.enumConstants
inputComponent = JComboBox(enumConstants as Array<out Enum<*>>).apply {
selectedItem = field.get(plugin) selectedItem = field.get(plugin)
} }
isHashMapEditor = false
}
field.type == Int::class.javaPrimitiveType || field.type == Integer::class.java -> field.type == Int::class.javaPrimitiveType || field.type == Integer::class.java -> {
JSpinner(SpinnerNumberModel(field.get(plugin) as Int, Int.MIN_VALUE, Int.MAX_VALUE, 1)) inputComponent = JSpinner(SpinnerNumberModel(field.get(plugin) as Int, Int.MIN_VALUE, Int.MAX_VALUE, 1))
isHashMapEditor = false
}
field.type == Float::class.javaPrimitiveType || field.type == Float::class.javaPrimitiveType ||
field.type == Double::class.javaPrimitiveType || field.type == Double::class.javaPrimitiveType ||
field.type == java.lang.Float::class.java || field.type == java.lang.Float::class.java ||
field.type == java.lang.Double::class.java -> field.type == java.lang.Double::class.java -> {
JSpinner(SpinnerNumberModel((field.get(plugin) as Number).toDouble(), -Double.MAX_VALUE, Double.MAX_VALUE, 0.1)) inputComponent = JSpinner(SpinnerNumberModel((field.get(plugin) as Number).toDouble(), -Double.MAX_VALUE, Double.MAX_VALUE, 0.1))
isHashMapEditor = false
}
else -> // Check if the field is a HashMap
JTextField(field.get(plugin)?.toString() ?: "") field.type == HashMap::class.java || field.type.simpleName == "HashMap" -> {
} inputComponent = createHashMapEditor(field, plugin, fieldNotifier)
isHashMapEditor = true
}
// Add mouse listener to the label only if a description is available else -> {
if (description.isNotBlank()) { inputComponent = JTextField(field.get(plugin)?.toString() ?: "")
label.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) isHashMapEditor = false
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)
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) // Handle HashMap editors differently - they don't use the fieldPanel layout
add(fieldPanel) 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)
}
// Track field changes in real-time and update UI override fun mouseExited(e: MouseEvent) {
var previousValue = field.get(plugin)?.toString() customToolTipWindow?.isVisible = false
val timer = Timer() }
timer.schedule(object : TimerTask() { })
override fun run() { }
val currentValue = field.get(plugin)?.toString()
if (currentValue != previousValue) { gbc.gridx = 1
previousValue = currentValue gbc.gridy = 0
SwingUtilities.invokeLater { gbc.weightx = 1.0
// Update the inputComponent based on the new value gbc.fill = GridBagConstraints.HORIZONTAL
when (inputComponent) { fieldPanel.add(inputComponent, gbc)
is JCheckBox -> inputComponent.isSelected = field.get(plugin) as Boolean
is JComboBox<*> -> inputComponent.selectedItem = field.get(plugin) // Add apply button for non-HashMap editors
is JSpinner -> inputComponent.value = field.get(plugin) val applyButton = JButton("\u2714").apply {
is JTextField -> inputComponent.text = field.get(plugin)?.toString() ?: "" 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)
}, 0, 1000) // Poll every 1000 milliseconds (1 second) }
} }
if (exposedFields.isNotEmpty()) { if (exposedFields.isNotEmpty()) {
@ -175,6 +204,165 @@ class SettingsPanel(private val plugin: Plugin) : JPanel() {
} }
} }
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 { companion object {
var customToolTipWindow: JWindow? = null var customToolTipWindow: JWindow? = null

View file

@ -1,3 +1,3 @@
AUTHOR='downthecrop' AUTHOR='downthecrop'
DESCRIPTION='A plugin that adds a right-side panel with custom widgets and navigation.' DESCRIPTION='A plugin that adds a right-side panel with custom widgets and navigation. Now with enhanced HashMap editor for easier configuration management.'
VERSION=2.0 VERSION=2.1