mirror of
https://gitlab.com/2009scape/rt4-client.git
synced 2025-12-09 16:45:46 -07:00
Merge branch 'KondoImprovements' into 'master'
KondoKit V2.1 + ChatboxHelms plugin See merge request 2009scape/rt4-client!30
This commit is contained in:
commit
e91ed4269a
33 changed files with 4486 additions and 1078 deletions
402
plugin-playground/src/main/kotlin/ChatboxHelmets/plugin.kt
Normal file
402
plugin-playground/src/main/kotlin/ChatboxHelmets/plugin.kt
Normal file
|
|
@ -0,0 +1,402 @@
|
||||||
|
package ChatboxHelmets
|
||||||
|
|
||||||
|
import KondoKit.Exposed
|
||||||
|
import plugin.Plugin
|
||||||
|
import plugin.api.API
|
||||||
|
import rt4.Component
|
||||||
|
import rt4.JagString
|
||||||
|
import rt4.Player
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
class plugin : Plugin() {
|
||||||
|
|
||||||
|
private val TARGET_COMPONENT_ID = 8978483
|
||||||
|
private val TARGET_COMPONENT_INDEX = 51
|
||||||
|
private val SETTINGS_KEY = "chat-helms"
|
||||||
|
private val BUBBLE_ICON = "<img=3>"
|
||||||
|
private val gson = Gson()
|
||||||
|
|
||||||
|
val TYPE_ICONS: Map<Int, String> = hashMapOf(
|
||||||
|
0 to "",
|
||||||
|
1 to "<img=4>", // IM
|
||||||
|
2 to "<img=5>", // HCIM
|
||||||
|
3 to "<img=6>", // UIM
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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>()
|
||||||
|
|
||||||
|
private var ACCOUNT_TYPE = 0
|
||||||
|
|
||||||
|
private var component: Component? = null
|
||||||
|
|
||||||
|
// See JagString.java parse(), There is special encoding
|
||||||
|
// and this correctly resolves specials.
|
||||||
|
val SPECIAL_ESCAPES: Map<Char, String> = hashMapOf(
|
||||||
|
' ' to "(P",
|
||||||
|
'!' to "(Q",
|
||||||
|
'"' to "(R",
|
||||||
|
'#' to "(S",
|
||||||
|
'$' to "(T",
|
||||||
|
'%' to "(U",
|
||||||
|
'&' to "(V",
|
||||||
|
'\'' to "(W",
|
||||||
|
'(' to "(X",
|
||||||
|
')' to "(Y",
|
||||||
|
'*' to "(Z",
|
||||||
|
'+' to ")0",
|
||||||
|
',' to ")1",
|
||||||
|
'-' to ")2",
|
||||||
|
'.' to ")3",
|
||||||
|
'/' to ")4",
|
||||||
|
':' to "(j",
|
||||||
|
';' to "(k",
|
||||||
|
'=' to ")B",
|
||||||
|
'?' to ")D",
|
||||||
|
'@' to ")E",
|
||||||
|
'[' to "*5",
|
||||||
|
'\\' to "*6",
|
||||||
|
']' to "*7",
|
||||||
|
'^' to "*8",
|
||||||
|
'_' to "*9",
|
||||||
|
'`' to ")e",
|
||||||
|
'{' to "*U",
|
||||||
|
'|' to "*V",
|
||||||
|
'}' to "*W",
|
||||||
|
'~' to "*X",
|
||||||
|
'\u0000' to "(0",
|
||||||
|
'\u0001' to "(1",
|
||||||
|
'\u0002' to "(2",
|
||||||
|
'\u0003' to "(3",
|
||||||
|
'\u0004' to "(4",
|
||||||
|
'\u0005' to "(5",
|
||||||
|
'\u0006' to "(6",
|
||||||
|
'\u0007' to "(7",
|
||||||
|
'\b' to "(8",
|
||||||
|
'\t' to "(9",
|
||||||
|
'\n' to "(:",
|
||||||
|
'\u000B' to "(;",
|
||||||
|
'\u000C' to "(<",
|
||||||
|
'\r' to "(=",
|
||||||
|
'\u000E' to "(>",
|
||||||
|
'\u000F' to "(?",
|
||||||
|
'\u0010' to "(@",
|
||||||
|
'\u0011' to "(A",
|
||||||
|
'\u0012' to "(B",
|
||||||
|
'\u0013' to "(C",
|
||||||
|
'\u0014' to "(D",
|
||||||
|
'\u0015' to "(E",
|
||||||
|
'\u0016' to "(F",
|
||||||
|
'\u0017' to "(G",
|
||||||
|
'\u0018' to "(H",
|
||||||
|
'\u0019' to "(I",
|
||||||
|
'\u001A' to "(J",
|
||||||
|
'\u001B' to "(K",
|
||||||
|
'\u001C' to "(L",
|
||||||
|
'\u001D' to "(M",
|
||||||
|
'\u001E' to "(N",
|
||||||
|
'\u001F' to "(O",
|
||||||
|
'\u007F' to "*Y",
|
||||||
|
'\u0080' to "*Z",
|
||||||
|
'\u0081' to "+0",
|
||||||
|
'\u0082' to "+1",
|
||||||
|
'\u0083' to "+2",
|
||||||
|
'\u0084' to "+3",
|
||||||
|
'\u0085' to "+4",
|
||||||
|
'\u0086' to "+5",
|
||||||
|
'\u0087' to "+6",
|
||||||
|
'\u0088' to "+7",
|
||||||
|
'\u0089' to "+8",
|
||||||
|
'\u008A' to "+9",
|
||||||
|
'\u008B' to "*e",
|
||||||
|
'\u008C' to "*f",
|
||||||
|
'\u008D' to "*g",
|
||||||
|
'\u008E' to "*h",
|
||||||
|
'\u008F' to "*i",
|
||||||
|
'\u0090' to "*j",
|
||||||
|
'\u0091' to "*k",
|
||||||
|
'\u0092' to "+A",
|
||||||
|
'\u0093' to "+B",
|
||||||
|
'\u0094' to "+C",
|
||||||
|
'\u0095' to "+D",
|
||||||
|
'\u0096' to "+E",
|
||||||
|
'\u0097' to "+F",
|
||||||
|
'\u0098' to "+G",
|
||||||
|
'\u0099' to "+H",
|
||||||
|
'\u009A' to "+I",
|
||||||
|
'\u009B' to "+J",
|
||||||
|
'\u009C' to "+K",
|
||||||
|
'\u009D' to "+L",
|
||||||
|
'\u009E' to "+M",
|
||||||
|
'\u009F' to "+N",
|
||||||
|
'\u00A0' to "+O",
|
||||||
|
'\u00A1' to "+P",
|
||||||
|
'\u00A2' to "+Q",
|
||||||
|
'\u00A3' to "+R",
|
||||||
|
'\u00A4' to "+S",
|
||||||
|
'\u00A5' to "+T",
|
||||||
|
'\u00A6' to "+U",
|
||||||
|
'\u00A7' to "+V",
|
||||||
|
'\u00A8' to "+W",
|
||||||
|
'\u00A9' to "+X",
|
||||||
|
'\u00AA' to "+Y",
|
||||||
|
'\u00AB' to "+Z",
|
||||||
|
'\u00AC' to ",0",
|
||||||
|
'\u00AD' to ",1",
|
||||||
|
'\u00AE' to ",2",
|
||||||
|
'\u00AF' to ",3",
|
||||||
|
'\u00B0' to ",4",
|
||||||
|
'\u00B1' to ",5",
|
||||||
|
'\u00B2' to ",6",
|
||||||
|
'\u00B3' to ",7",
|
||||||
|
'\u00B4' to ",8",
|
||||||
|
'\u00B5' to ",9",
|
||||||
|
'\u00B6' to "+e",
|
||||||
|
'\u00B7' to "+f",
|
||||||
|
'\u00B8' to "+g",
|
||||||
|
'\u00B9' to "+h",
|
||||||
|
'\u00BA' to "+i",
|
||||||
|
'\u00BB' to "+j",
|
||||||
|
'\u00BC' to "+k",
|
||||||
|
'\u00BD' to ",A",
|
||||||
|
'\u00BE' to ",B",
|
||||||
|
'\u00BF' to ",C",
|
||||||
|
'\u00C0' to ",D",
|
||||||
|
'\u00C1' to ",E",
|
||||||
|
'\u00C2' to ",F",
|
||||||
|
'\u00C3' to ",G",
|
||||||
|
'\u00C4' to ",H",
|
||||||
|
'\u00C5' to ",I",
|
||||||
|
'\u00C6' to ",J",
|
||||||
|
'\u00C7' to ",K",
|
||||||
|
'\u00C8' to ",L",
|
||||||
|
'\u00C9' to ",M",
|
||||||
|
'\u00CA' to ",N",
|
||||||
|
'\u00CB' to ",O",
|
||||||
|
'\u00CC' to ",P",
|
||||||
|
'\u00CD' to ",Q",
|
||||||
|
'\u00CE' to ",R",
|
||||||
|
'\u00CF' to ",S",
|
||||||
|
'\u00D0' to ",T",
|
||||||
|
'\u00D1' to ",U",
|
||||||
|
'\u00D2' to ",V",
|
||||||
|
'\u00D3' to ",W",
|
||||||
|
'\u00D4' to ",X",
|
||||||
|
'\u00D5' to ",Y",
|
||||||
|
'\u00D6' to ",Z",
|
||||||
|
'\u00D7' to "-0",
|
||||||
|
'\u00D8' to "-1",
|
||||||
|
'\u00D9' to "-2",
|
||||||
|
'\u00DA' to "-3",
|
||||||
|
'\u00DB' to "-4",
|
||||||
|
'\u00DC' to "-5",
|
||||||
|
'\u00DD' to "-6",
|
||||||
|
'\u00DE' to "-7",
|
||||||
|
'\u00DF' to "-8",
|
||||||
|
'\u00E0' to "-9",
|
||||||
|
'\u00E1' to ",e",
|
||||||
|
'\u00E2' to ",f",
|
||||||
|
'\u00E3' to ",g",
|
||||||
|
'\u00E4' to ",h",
|
||||||
|
'\u00E5' to ",i",
|
||||||
|
'\u00E6' to ",j",
|
||||||
|
'\u00E7' to ",k",
|
||||||
|
'\u00E8' to "-A",
|
||||||
|
'\u00E9' to "-B",
|
||||||
|
'\u00EA' to "-C",
|
||||||
|
'\u00EB' to "-D",
|
||||||
|
'\u00EC' to "-E",
|
||||||
|
'\u00ED' to "-F",
|
||||||
|
'\u00EE' to "-G",
|
||||||
|
'\u00EF' to "-H",
|
||||||
|
'\u00F0' to "-I",
|
||||||
|
'\u00F1' to "-J",
|
||||||
|
'\u00F2' to "-K",
|
||||||
|
'\u00F3' to "-L",
|
||||||
|
'\u00F4' to "-M",
|
||||||
|
'\u00F5' to "-N",
|
||||||
|
'\u00F6' to "-O",
|
||||||
|
'\u00F7' to "-P",
|
||||||
|
'\u00F8' to "-Q",
|
||||||
|
'\u00F9' to "-R",
|
||||||
|
'\u00FA' to "-S",
|
||||||
|
'\u00FB' to "-T",
|
||||||
|
'\u00FC' to "-U",
|
||||||
|
'\u00FD' to "-V",
|
||||||
|
'\u00FE' to "-W",
|
||||||
|
'\u00FF' to "-X"
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun Init() {
|
||||||
|
// Load username matches from local storage
|
||||||
|
val storedData = API.GetData(SETTINGS_KEY)
|
||||||
|
usernameMatches = when (storedData) {
|
||||||
|
is String -> {
|
||||||
|
try {
|
||||||
|
gson.fromJson(storedData, HashMap::class.java) as HashMap<String, Int>
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("Failed to deserialize username matches: ${e.message}")
|
||||||
|
HashMap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// No data or unexpected format
|
||||||
|
HashMap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun OnKondoValueUpdated() {
|
||||||
|
println("OnKondoValueUpdated called - current usernameMatches: $usernameMatches")
|
||||||
|
StoreData()
|
||||||
|
OnPluginsReloaded() //refresh the ui
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCleanUserName(): String {
|
||||||
|
return Player.usernameInput.toString().replace(" ", "_")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun OnPluginsReloaded(): Boolean {
|
||||||
|
grabConfig()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun StoreData() {
|
||||||
|
val jsonString = gson.toJson(usernameMatches)
|
||||||
|
println("Storing ${jsonString}")
|
||||||
|
API.StoreData(SETTINGS_KEY, jsonString)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun Draw(timeDelta: Long) {
|
||||||
|
if (component != null && component?.id == TARGET_COMPONENT_ID) {
|
||||||
|
val username = Player.usernameInput.toString().toLowerCase()
|
||||||
|
val modifiedStr = replaceUsernameInBytes(component!!.text.chars, username)
|
||||||
|
component?.text = JagString.parse(modifiedStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun ComponentDraw(componentIndex: Int, component: Component?, screenX: Int, screenY: Int) {
|
||||||
|
if (component?.id == TARGET_COMPONENT_ID && componentIndex == TARGET_COMPONENT_INDEX) {
|
||||||
|
this.component = component
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetchAccountTypeFromAPI(){
|
||||||
|
println("Fetching Iron status from API...")
|
||||||
|
val cleanUsername = getCleanUserName()
|
||||||
|
val apiUrl = "http://api.2009scape.org:3000/hiscores/playerSkills/1/${cleanUsername.toLowerCase()}"
|
||||||
|
Thread {
|
||||||
|
try {
|
||||||
|
val url = URL(apiUrl)
|
||||||
|
val connection = url.openConnection() as HttpURLConnection
|
||||||
|
connection.requestMethod = "GET"
|
||||||
|
|
||||||
|
// If a request take longer than 5 seconds timeout.
|
||||||
|
connection.connectTimeout = 5000
|
||||||
|
connection.readTimeout = 5000
|
||||||
|
|
||||||
|
val responseCode = connection.responseCode
|
||||||
|
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||||
|
val reader = BufferedReader(InputStreamReader(connection.inputStream))
|
||||||
|
val response = reader.use { it.readText() }
|
||||||
|
reader.close()
|
||||||
|
updatePlayerData(response, cleanUsername)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { }
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePlayerData(jsonResponse: String, username: String) {
|
||||||
|
val hiscoresResponse = gson.fromJson(jsonResponse, HiscoresResponse::class.java)
|
||||||
|
ACCOUNT_TYPE = hiscoresResponse.info.iron_mode.toInt()
|
||||||
|
println("Resolved you are account type: $ACCOUNT_TYPE")
|
||||||
|
|
||||||
|
// Store the result in our local cache
|
||||||
|
usernameMatches[username] = ACCOUNT_TYPE
|
||||||
|
StoreData()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun OnLogin() {
|
||||||
|
grabConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun grabConfig() {
|
||||||
|
// Check if we already have the account type for this user
|
||||||
|
val cleanUsername = getCleanUserName()
|
||||||
|
if (usernameMatches.containsKey(cleanUsername.toLowerCase())) {
|
||||||
|
ACCOUNT_TYPE = usernameMatches[cleanUsername]!!
|
||||||
|
println("Using cached account type: $ACCOUNT_TYPE for $cleanUsername")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// The game server doesn't tell us what account type we are.
|
||||||
|
// Requesting from API is the easier way to tell.
|
||||||
|
fetchAccountTypeFromAPI()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow setting from the chatbox
|
||||||
|
override fun ProcessCommand(commandStr: String?, args: Array<out String>?) {
|
||||||
|
super.ProcessCommand(commandStr, args)
|
||||||
|
when(commandStr) {
|
||||||
|
"::chathelm" -> {
|
||||||
|
if (args != null) {
|
||||||
|
if(args.isEmpty()) return
|
||||||
|
val type = args[0].toIntOrNull() ?: return
|
||||||
|
ACCOUNT_TYPE = type
|
||||||
|
usernameMatches[getCleanUserName()] = ACCOUNT_TYPE
|
||||||
|
StoreData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun replaceUsernameInBytes(bytes: ByteArray, username: String): String {
|
||||||
|
// Convert bytes to string to work with them
|
||||||
|
val text = String(bytes, StandardCharsets.ISO_8859_1)
|
||||||
|
val colonIndex = text.indexOf(": ")
|
||||||
|
if (colonIndex != -1) {
|
||||||
|
val suffix = text.substring(colonIndex)
|
||||||
|
// modify and add the rest (username is always before the first ':' )
|
||||||
|
val newText = "${TYPE_ICONS.getOrDefault(ACCOUNT_TYPE, "")}$username$BUBBLE_ICON$suffix"
|
||||||
|
return encodeToJagStr(newText)
|
||||||
|
}
|
||||||
|
return encodeToJagStr(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun encodeToJagStr(str: String): String {
|
||||||
|
val result = StringBuilder()
|
||||||
|
for (char in str) {
|
||||||
|
val escaped = SPECIAL_ESCAPES[char]
|
||||||
|
if (escaped != null) {
|
||||||
|
result.append(escaped)
|
||||||
|
} else {
|
||||||
|
result.append(char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lifted from KondoHiscores view
|
||||||
|
data class HiscoresResponse(
|
||||||
|
val info: PlayerInfo,
|
||||||
|
val skills: List<Skill>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PlayerInfo(
|
||||||
|
val exp_multiplier: String,
|
||||||
|
val iron_mode: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Skill(
|
||||||
|
val id: String,
|
||||||
|
val dynamic: String,
|
||||||
|
val experience: String,
|
||||||
|
val static: String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
AUTHOR='downthecrop'
|
||||||
|
DESCRIPTION='Displays account type icons next to usernames in chat pulled from the live server API. \
|
||||||
|
Can be set with a chat command for single player with ::chathelm 1 (0=Normal, 1=IM, 2=HCIM, 3=UIM) \
|
||||||
|
or via KondoKit settings.'
|
||||||
|
VERSION=1.0
|
||||||
BIN
plugin-playground/src/main/kotlin/KondoKit/.DS_Store
vendored
Normal file
BIN
plugin-playground/src/main/kotlin/KondoKit/.DS_Store
vendored
Normal file
Binary file not shown.
|
|
@ -3,6 +3,8 @@ package KondoKit
|
||||||
import rt4.GameShell
|
import rt4.GameShell
|
||||||
import java.awt.*
|
import java.awt.*
|
||||||
import java.awt.event.MouseListener
|
import java.awt.event.MouseListener
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
import java.lang.reflect.ParameterizedType
|
import java.lang.reflect.ParameterizedType
|
||||||
import java.lang.reflect.Type
|
import java.lang.reflect.Type
|
||||||
|
|
@ -11,6 +13,16 @@ import java.util.Timer
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
|
|
||||||
object Helpers {
|
object Helpers {
|
||||||
|
// Convenience helper for loading resources relative to the KondoKit plugin class
|
||||||
|
fun openResource(path: String) = plugin::class.java.getResourceAsStream(path)
|
||||||
|
|
||||||
|
// Read a bundled resource as text using the given charset (UTF-8 by default)
|
||||||
|
fun readResourceText(path: String, charset: Charset = StandardCharsets.UTF_8): String? {
|
||||||
|
val stream = openResource(path) ?: return null
|
||||||
|
stream.use { s ->
|
||||||
|
return s.reader(charset).use { it.readText() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun convertValue(type: Class<*>, genericType: Type?, value: String): Any {
|
fun convertValue(type: Class<*>, genericType: Type?, value: String): Any {
|
||||||
return when {
|
return when {
|
||||||
|
|
|
||||||
|
|
@ -1,324 +0,0 @@
|
||||||
package KondoKit
|
|
||||||
|
|
||||||
import KondoKit.Helpers.convertValue
|
|
||||||
import KondoKit.Helpers.showToast
|
|
||||||
import KondoKit.plugin.Companion.TITLE_BAR_COLOR
|
|
||||||
import KondoKit.plugin.Companion.TOOLTIP_BACKGROUND
|
|
||||||
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
|
|
||||||
import KondoKit.plugin.Companion.WIDGET_COLOR
|
|
||||||
import KondoKit.plugin.Companion.primaryColor
|
|
||||||
import KondoKit.plugin.Companion.secondaryColor
|
|
||||||
import KondoKit.plugin.StateManager.focusedView
|
|
||||||
import plugin.Plugin
|
|
||||||
import plugin.PluginInfo
|
|
||||||
import plugin.PluginRepository
|
|
||||||
import java.awt.*
|
|
||||||
import java.awt.event.MouseAdapter
|
|
||||||
import java.awt.event.MouseEvent
|
|
||||||
import java.util.*
|
|
||||||
import java.util.Timer
|
|
||||||
import javax.swing.*
|
|
||||||
import kotlin.math.ceil
|
|
||||||
|
|
||||||
/*
|
|
||||||
This is used for the runtime editing of plugin variables.
|
|
||||||
To expose fields add the @Exposed annotation.
|
|
||||||
When they are applied this will trigger an invoke of OnKondoValueUpdated()
|
|
||||||
if it is implemented. Check GroundItems plugin for an example.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
object ReflectiveEditorView {
|
|
||||||
var reflectiveEditorView: JPanel? = null
|
|
||||||
private val loadedPlugins: MutableList<String> = mutableListOf()
|
|
||||||
const val VIEW_NAME = "REFLECTIVE_EDITOR_VIEW"
|
|
||||||
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!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addPlugins(reflectiveEditorView: JPanel) {
|
|
||||||
reflectiveEditorView.removeAll() // clear previous
|
|
||||||
loadedPlugins.clear()
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
} 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
reflectiveEditorView.revalidate()
|
|
||||||
if(focusedView == VIEW_NAME)
|
|
||||||
reflectiveEditorView.repaint()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addPluginToEditor(reflectiveEditorView: JPanel, pluginInfo : PluginInfo, plugin: Plugin) {
|
|
||||||
reflectiveEditorView.layout = BoxLayout(reflectiveEditorView, BoxLayout.Y_AXIS)
|
|
||||||
|
|
||||||
val fieldNotifier = Helpers.FieldNotifier(plugin)
|
|
||||||
val exposedFields = plugin.javaClass.declaredFields.filter { field ->
|
|
||||||
field.annotations.any { annotation ->
|
|
||||||
annotation.annotationClass.simpleName == "Exposed"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
fieldPanel.layout = GridBagLayout()
|
|
||||||
fieldPanel.background = WIDGET_COLOR
|
|
||||||
fieldPanel.foreground = secondaryColor
|
|
||||||
fieldPanel.border = BorderFactory.createEmptyBorder(5, 0, 5, 0)
|
|
||||||
fieldPanel.maximumSize = Dimension(Int.MAX_VALUE, 40)
|
|
||||||
|
|
||||||
val gbc = GridBagConstraints()
|
|
||||||
gbc.insets = Insets(0, 5, 0, 5)
|
|
||||||
|
|
||||||
val label = JLabel(field.name.capitalize())
|
|
||||||
label.foreground = secondaryColor
|
|
||||||
gbc.gridx = 0
|
|
||||||
gbc.gridy = 0
|
|
||||||
gbc.weightx = 0.0
|
|
||||||
label.font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
|
|
||||||
gbc.anchor = GridBagConstraints.WEST
|
|
||||||
fieldPanel.add(label, gbc)
|
|
||||||
|
|
||||||
// Create appropriate input component based on field type
|
|
||||||
val inputComponent: JComponent = when {
|
|
||||||
field.type == Boolean::class.javaPrimitiveType || field.type == java.lang.Boolean::class.java -> JCheckBox().apply {
|
|
||||||
isSelected = field.get(plugin) as Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
field.type.isEnum -> JComboBox((field.type.enumConstants as Array<Enum<*>>)).apply {
|
|
||||||
selectedItem = field.get(plugin)
|
|
||||||
}
|
|
||||||
|
|
||||||
field.type == Int::class.javaPrimitiveType || field.type == Integer::class.java -> JSpinner(SpinnerNumberModel(field.get(plugin) as Int, Int.MIN_VALUE, Int.MAX_VALUE, 1))
|
|
||||||
field.type == Float::class.javaPrimitiveType || field.type == Double::class.javaPrimitiveType || field.type == java.lang.Float::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))
|
|
||||||
else -> JTextField(field.get(plugin)?.toString() ?: "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
|
|
||||||
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 -> convertValue(field.type, field.genericType, inputComponent.text)
|
|
||||||
else -> throw IllegalArgumentException("Unsupported input component type")
|
|
||||||
}
|
|
||||||
fieldNotifier.setFieldValue(field, newValue)
|
|
||||||
showToast(
|
|
||||||
reflectiveEditorView,
|
|
||||||
"${field.name} updated successfully!"
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showToast(
|
|
||||||
reflectiveEditorView,
|
|
||||||
"Failed to update ${field.name}: ${e.message}",
|
|
||||||
JOptionPane.ERROR_MESSAGE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldPanel.add(applyButton, gbc)
|
|
||||||
reflectiveEditorView.add(fieldPanel)
|
|
||||||
|
|
||||||
// Track field changes in real-time and update UI
|
|
||||||
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()) {
|
|
||||||
reflectiveEditorView.add(Box.createVerticalStrut(5))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
loadedPlugins.add(plugin.javaClass.`package`.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = 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(TOOLTIP_BACKGROUND)
|
|
||||||
val textColor = Helpers.colorToHex(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 = 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(TOOLTIP_BACKGROUND)
|
|
||||||
val textColor = Helpers.colorToHex(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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,444 +0,0 @@
|
||||||
package KondoKit
|
|
||||||
|
|
||||||
import KondoKit.Helpers.addMouseListenerToAll
|
|
||||||
import KondoKit.Helpers.formatHtmlLabelText
|
|
||||||
import KondoKit.Helpers.formatNumber
|
|
||||||
import KondoKit.Helpers.getProgressBarColor
|
|
||||||
import KondoKit.Helpers.getSpriteId
|
|
||||||
import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite
|
|
||||||
import KondoKit.plugin.Companion.IMAGE_SIZE
|
|
||||||
import KondoKit.plugin.Companion.LVL_ICON
|
|
||||||
import KondoKit.plugin.Companion.POPUP_BACKGROUND
|
|
||||||
import KondoKit.plugin.Companion.POPUP_FOREGROUND
|
|
||||||
import KondoKit.plugin.Companion.TOTAL_XP_WIDGET_SIZE
|
|
||||||
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
|
|
||||||
import KondoKit.plugin.Companion.WIDGET_COLOR
|
|
||||||
import KondoKit.plugin.Companion.WIDGET_SIZE
|
|
||||||
import KondoKit.plugin.Companion.playerXPMultiplier
|
|
||||||
import KondoKit.plugin.Companion.primaryColor
|
|
||||||
import KondoKit.plugin.Companion.secondaryColor
|
|
||||||
import KondoKit.plugin.StateManager.focusedView
|
|
||||||
import plugin.api.API
|
|
||||||
import java.awt.*
|
|
||||||
import java.awt.event.MouseAdapter
|
|
||||||
import java.awt.event.MouseEvent
|
|
||||||
import java.io.BufferedReader
|
|
||||||
import java.io.InputStreamReader
|
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
import javax.swing.*
|
|
||||||
|
|
||||||
object XPTrackerView {
|
|
||||||
private val COMBAT_SKILLS = intArrayOf(0,1,2,3,4)
|
|
||||||
val xpWidgets: MutableMap<Int, XPWidget> = HashMap()
|
|
||||||
var totalXPWidget: XPWidget? = null
|
|
||||||
val initialXP: MutableMap<Int, Int> = HashMap()
|
|
||||||
var xpTrackerView: JPanel? = null
|
|
||||||
const val VIEW_NAME = "XP_TRACKER_VIEW"
|
|
||||||
|
|
||||||
|
|
||||||
val npcHitpointsMap: Map<Int, Int> = try {
|
|
||||||
BufferedReader(InputStreamReader(plugin::class.java.getResourceAsStream("res/npc_hitpoints_map.json"), StandardCharsets.UTF_8))
|
|
||||||
.useLines { lines ->
|
|
||||||
val json = lines.joinToString("\n")
|
|
||||||
val pairs = json.trim().removeSurrounding("{", "}").split(",")
|
|
||||||
val map = mutableMapOf<Int, Int>()
|
|
||||||
|
|
||||||
for (pair in pairs) {
|
|
||||||
val keyValue = pair.split(":")
|
|
||||||
val id = keyValue[0].trim().trim('\"').toIntOrNull()
|
|
||||||
val hitpoints = keyValue[1].trim()
|
|
||||||
|
|
||||||
if (id != null && hitpoints.isNotEmpty()) {
|
|
||||||
map[id] = hitpoints.toIntOrNull() ?: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
map
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("XPTracker Error parsing NPC HP: ${e.message}")
|
|
||||||
emptyMap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateWidget(xpWidget: XPWidget, xp: Int) {
|
|
||||||
val (currentLevel, xpGainedSinceLastLevel) = XPTable.getLevelForXp(xp)
|
|
||||||
|
|
||||||
var xpGainedSinceLastUpdate = xp - xpWidget.previousXp
|
|
||||||
xpWidget.totalXpGained += xpGainedSinceLastUpdate
|
|
||||||
updateTotalXPWidget(xpGainedSinceLastUpdate)
|
|
||||||
|
|
||||||
val progress: Double
|
|
||||||
if (currentLevel >= 99) {
|
|
||||||
progress = 100.0 // Set progress to 100% if the level is 99 or above
|
|
||||||
xpWidget.xpLeftLabel.text = "" // Hide XP Left when level is 99
|
|
||||||
xpWidget.actionsRemainingLabel.text = ""
|
|
||||||
} else {
|
|
||||||
val nextLevelXp = XPTable.getXpRequiredForLevel(currentLevel + 1)
|
|
||||||
val xpLeft = nextLevelXp - xp
|
|
||||||
progress = xpGainedSinceLastLevel.toDouble() / (nextLevelXp - XPTable.getXpRequiredForLevel(currentLevel)) * 100
|
|
||||||
val xpLeftstr = formatNumber(xpLeft)
|
|
||||||
xpWidget.xpLeftLabel.text = formatHtmlLabelText("XP Left: ", primaryColor, xpLeftstr, secondaryColor)
|
|
||||||
if(COMBAT_SKILLS.contains(xpWidget.skillId)) {
|
|
||||||
if(LootTrackerView.lastConfirmedKillNpcId != -1 && npcHitpointsMap.isNotEmpty()) {
|
|
||||||
val npcHP = npcHitpointsMap[LootTrackerView.lastConfirmedKillNpcId]
|
|
||||||
val xpPerKill = when (xpWidget.skillId) {
|
|
||||||
3 -> playerXPMultiplier * (npcHP ?: 1) // Hitpoints
|
|
||||||
else -> playerXPMultiplier * (npcHP ?: 1) * 4 // Combat XP for other skills
|
|
||||||
}
|
|
||||||
val remainingKills = xpLeft / xpPerKill
|
|
||||||
xpWidget.actionsRemainingLabel.text = formatHtmlLabelText("Kills: ", primaryColor, remainingKills.toString(), secondaryColor)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if(xpGainedSinceLastUpdate == 0)
|
|
||||||
xpGainedSinceLastUpdate = 1 // Avoid possible divide by 0
|
|
||||||
val remainingActions = (xpLeft / xpGainedSinceLastUpdate).coerceAtLeast(1)
|
|
||||||
xpWidget.actionsRemainingLabel.text = formatHtmlLabelText("Actions: ", primaryColor, remainingActions.toString(), secondaryColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val formattedXp = formatNumber(xpWidget.totalXpGained)
|
|
||||||
xpWidget.xpGainedLabel.text = formatHtmlLabelText("XP Gained: ", primaryColor, formattedXp, secondaryColor)
|
|
||||||
|
|
||||||
// Update the progress bar with current level, progress, and next level
|
|
||||||
xpWidget.progressBar.updateProgress(progress, currentLevel, if (currentLevel < 99) currentLevel + 1 else 99, focusedView == VIEW_NAME)
|
|
||||||
|
|
||||||
xpWidget.previousXp = xp
|
|
||||||
if (focusedView == VIEW_NAME)
|
|
||||||
xpWidget.container.repaint()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun updateTotalXPWidget(xpGainedSinceLastUpdate: Int) {
|
|
||||||
val totalXPWidget = totalXPWidget ?: return
|
|
||||||
totalXPWidget.totalXpGained += xpGainedSinceLastUpdate
|
|
||||||
val formattedXp = formatNumber(totalXPWidget.totalXpGained)
|
|
||||||
totalXPWidget.xpGainedLabel.text = formatHtmlLabelText("Gained: ", primaryColor, formattedXp, secondaryColor)
|
|
||||||
if (focusedView == VIEW_NAME)
|
|
||||||
totalXPWidget.container.repaint()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun resetXPTracker(xpTrackerView : JPanel){
|
|
||||||
|
|
||||||
// Redo logic here
|
|
||||||
xpTrackerView.removeAll()
|
|
||||||
val popupMenu = createResetMenu()
|
|
||||||
|
|
||||||
// Create a custom MouseListener
|
|
||||||
val rightClickListener = object : MouseAdapter() {
|
|
||||||
override fun mousePressed(e: MouseEvent) {
|
|
||||||
if (e.isPopupTrigger) {
|
|
||||||
popupMenu.show(e.component, e.x, e.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mouseReleased(e: MouseEvent) {
|
|
||||||
if (e.isPopupTrigger) {
|
|
||||||
popupMenu.show(e.component, e.x, e.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the XP widget
|
|
||||||
totalXPWidget = createTotalXPWidget()
|
|
||||||
val wrapped = wrappedWidget(totalXPWidget!!.container)
|
|
||||||
addMouseListenerToAll(wrapped,rightClickListener)
|
|
||||||
wrapped.addMouseListener(rightClickListener)
|
|
||||||
xpTrackerView.add(Box.createVerticalStrut(5))
|
|
||||||
xpTrackerView.add(wrapped)
|
|
||||||
xpTrackerView.add(Box.createVerticalStrut(5))
|
|
||||||
|
|
||||||
initialXP.clear()
|
|
||||||
xpWidgets.clear()
|
|
||||||
|
|
||||||
xpTrackerView.revalidate()
|
|
||||||
if (focusedView == VIEW_NAME)
|
|
||||||
xpTrackerView.repaint()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createTotalXPWidget(): XPWidget {
|
|
||||||
val widgetPanel = Panel().apply {
|
|
||||||
layout = BorderLayout(5, 5)
|
|
||||||
background = WIDGET_COLOR
|
|
||||||
preferredSize = TOTAL_XP_WIDGET_SIZE
|
|
||||||
maximumSize = TOTAL_XP_WIDGET_SIZE
|
|
||||||
minimumSize = TOTAL_XP_WIDGET_SIZE
|
|
||||||
}
|
|
||||||
|
|
||||||
val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(LVL_ICON))
|
|
||||||
|
|
||||||
val imageContainer = Panel(FlowLayout()).apply {
|
|
||||||
preferredSize = Dimension(bufferedImageSprite.width, bufferedImageSprite.height)
|
|
||||||
maximumSize = preferredSize
|
|
||||||
minimumSize = preferredSize
|
|
||||||
size = preferredSize
|
|
||||||
}
|
|
||||||
|
|
||||||
bufferedImageSprite.let { image ->
|
|
||||||
val imageCanvas = ImageCanvas(image).apply {
|
|
||||||
preferredSize = Dimension(bufferedImageSprite.width, bufferedImageSprite.height)
|
|
||||||
maximumSize = preferredSize
|
|
||||||
minimumSize = preferredSize
|
|
||||||
size = preferredSize
|
|
||||||
}
|
|
||||||
|
|
||||||
imageContainer.add(imageCanvas)
|
|
||||||
imageContainer.size = Dimension(bufferedImageSprite.width, bufferedImageSprite.height)
|
|
||||||
|
|
||||||
imageContainer.revalidate()
|
|
||||||
if(focusedView == VIEW_NAME)
|
|
||||||
imageContainer.repaint()
|
|
||||||
}
|
|
||||||
|
|
||||||
val textPanel = Panel().apply {
|
|
||||||
layout = GridLayout(2, 1, 5, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
val font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
|
|
||||||
|
|
||||||
val xpGainedLabel = JLabel(
|
|
||||||
formatHtmlLabelText("Gained: ", primaryColor, "0", secondaryColor)
|
|
||||||
).apply {
|
|
||||||
this.horizontalAlignment = JLabel.LEFT
|
|
||||||
this.font = font
|
|
||||||
}
|
|
||||||
|
|
||||||
val xpPerHourLabel = JLabel(
|
|
||||||
formatHtmlLabelText("XP /hr: ", primaryColor, "0", secondaryColor)
|
|
||||||
).apply {
|
|
||||||
this.horizontalAlignment = JLabel.LEFT
|
|
||||||
this.font = font
|
|
||||||
}
|
|
||||||
|
|
||||||
textPanel.add(xpGainedLabel)
|
|
||||||
textPanel.add(xpPerHourLabel)
|
|
||||||
|
|
||||||
widgetPanel.add(imageContainer, BorderLayout.WEST)
|
|
||||||
widgetPanel.add(textPanel, BorderLayout.CENTER)
|
|
||||||
|
|
||||||
return XPWidget(
|
|
||||||
skillId = -1,
|
|
||||||
container = widgetPanel,
|
|
||||||
xpGainedLabel = xpGainedLabel,
|
|
||||||
xpLeftLabel = JLabel(formatHtmlLabelText("XP Left: ", primaryColor, "0", secondaryColor)).apply {
|
|
||||||
this.horizontalAlignment = JLabel.LEFT
|
|
||||||
this.font = font
|
|
||||||
},
|
|
||||||
xpPerHourLabel = xpPerHourLabel,
|
|
||||||
progressBar = ProgressBar(0.0, Color.BLACK), // Unused
|
|
||||||
totalXpGained = 0,
|
|
||||||
startTime = System.currentTimeMillis(),
|
|
||||||
previousXp = 0,
|
|
||||||
actionsRemainingLabel = JLabel(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun createXPTrackerView(){
|
|
||||||
val widgetViewPanel = JPanel().apply {
|
|
||||||
layout = BoxLayout(this, BoxLayout.Y_AXIS)
|
|
||||||
background = VIEW_BACKGROUND_COLOR
|
|
||||||
}
|
|
||||||
|
|
||||||
val popupMenu = createResetMenu()
|
|
||||||
|
|
||||||
// Create a custom MouseListener
|
|
||||||
val rightClickListener = object : MouseAdapter() {
|
|
||||||
override fun mousePressed(e: MouseEvent) {
|
|
||||||
if (e.isPopupTrigger) {
|
|
||||||
popupMenu.show(e.component, e.x, e.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mouseReleased(e: MouseEvent) {
|
|
||||||
if (e.isPopupTrigger) {
|
|
||||||
popupMenu.show(e.component, e.x, e.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the XP widget
|
|
||||||
totalXPWidget = createTotalXPWidget()
|
|
||||||
val wrapped = wrappedWidget(totalXPWidget!!.container)
|
|
||||||
addMouseListenerToAll(wrapped,rightClickListener)
|
|
||||||
wrapped.addMouseListener(rightClickListener)
|
|
||||||
widgetViewPanel.add(Box.createVerticalStrut(5))
|
|
||||||
widgetViewPanel.add(wrapped)
|
|
||||||
widgetViewPanel.add(Box.createVerticalStrut(5))
|
|
||||||
|
|
||||||
xpTrackerView = widgetViewPanel
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun createResetMenu(): JPopupMenu {
|
|
||||||
// Create a popup menu
|
|
||||||
val popupMenu = JPopupMenu()
|
|
||||||
|
|
||||||
val rFont = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
|
|
||||||
|
|
||||||
popupMenu.background = POPUP_BACKGROUND
|
|
||||||
|
|
||||||
// Create menu items with custom font and colors
|
|
||||||
val menuItem1 = JMenuItem("Reset Tracker").apply {
|
|
||||||
font = rFont // Set custom font
|
|
||||||
background = POPUP_BACKGROUND // Dark background for item
|
|
||||||
foreground = POPUP_FOREGROUND // Light text color for item
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add menu items to the popup menu
|
|
||||||
popupMenu.add(menuItem1)
|
|
||||||
|
|
||||||
// Add action listeners to each menu item (optional)
|
|
||||||
menuItem1.addActionListener { plugin.registerDrawAction { resetXPTracker(xpTrackerView!!) } }
|
|
||||||
return popupMenu
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun createXPWidget(skillId: Int, previousXp: Int): XPWidget {
|
|
||||||
val widgetPanel = Panel().apply {
|
|
||||||
layout = BorderLayout(5, 5)
|
|
||||||
background = WIDGET_COLOR
|
|
||||||
preferredSize = WIDGET_SIZE
|
|
||||||
maximumSize = WIDGET_SIZE
|
|
||||||
minimumSize = WIDGET_SIZE
|
|
||||||
}
|
|
||||||
|
|
||||||
val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(getSpriteId(skillId)))
|
|
||||||
val imageContainer = Panel(FlowLayout()).apply {
|
|
||||||
background = WIDGET_COLOR
|
|
||||||
preferredSize = IMAGE_SIZE
|
|
||||||
maximumSize = IMAGE_SIZE
|
|
||||||
minimumSize = IMAGE_SIZE
|
|
||||||
size = IMAGE_SIZE
|
|
||||||
}
|
|
||||||
|
|
||||||
bufferedImageSprite.let { image ->
|
|
||||||
val imageCanvas = ImageCanvas(image).apply {
|
|
||||||
background = WIDGET_COLOR
|
|
||||||
preferredSize = Dimension(image.width, image.height)
|
|
||||||
maximumSize = Dimension(image.width, image.height)
|
|
||||||
minimumSize = Dimension(image.width, image.height)
|
|
||||||
size = Dimension(image.width, image.height) // Explicitly set the size
|
|
||||||
}
|
|
||||||
|
|
||||||
imageContainer.add(imageCanvas)
|
|
||||||
imageContainer.size = Dimension(image.width, image.height) // Ensure container respects the image size
|
|
||||||
|
|
||||||
imageContainer.revalidate()
|
|
||||||
if(focusedView == VIEW_NAME)
|
|
||||||
imageContainer.repaint()
|
|
||||||
}
|
|
||||||
|
|
||||||
val textPanel = Panel().apply {
|
|
||||||
layout = GridLayout(2, 2, 5, 0)
|
|
||||||
background = WIDGET_COLOR
|
|
||||||
}
|
|
||||||
|
|
||||||
val font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
|
|
||||||
|
|
||||||
val xpGainedLabel = JLabel(
|
|
||||||
formatHtmlLabelText("XP Gained: ", primaryColor, "0", secondaryColor)
|
|
||||||
).apply {
|
|
||||||
this.horizontalAlignment = JLabel.LEFT
|
|
||||||
this.font = font
|
|
||||||
}
|
|
||||||
|
|
||||||
val xpLeftLabel = JLabel(
|
|
||||||
formatHtmlLabelText("XP Left: ", primaryColor, "0K", secondaryColor)
|
|
||||||
).apply {
|
|
||||||
this.horizontalAlignment = JLabel.LEFT
|
|
||||||
this.font = font
|
|
||||||
}
|
|
||||||
|
|
||||||
val xpPerHourLabel = JLabel(
|
|
||||||
formatHtmlLabelText("XP /hr: ", primaryColor, "0", secondaryColor)
|
|
||||||
).apply {
|
|
||||||
this.horizontalAlignment = JLabel.LEFT
|
|
||||||
this.font = font
|
|
||||||
}
|
|
||||||
|
|
||||||
val killsLabel = JLabel(
|
|
||||||
formatHtmlLabelText("Kills: ", primaryColor, "0", secondaryColor)
|
|
||||||
).apply {
|
|
||||||
this.horizontalAlignment = JLabel.LEFT
|
|
||||||
this.font = font
|
|
||||||
}
|
|
||||||
|
|
||||||
val levelPanel = Panel().apply {
|
|
||||||
layout = BorderLayout(5, 0)
|
|
||||||
background = WIDGET_COLOR
|
|
||||||
}
|
|
||||||
|
|
||||||
val progressBarPanel = ProgressBar(0.0, getProgressBarColor(skillId)).apply {
|
|
||||||
preferredSize = Dimension(160, 22)
|
|
||||||
}
|
|
||||||
|
|
||||||
levelPanel.add(progressBarPanel, BorderLayout.CENTER)
|
|
||||||
|
|
||||||
textPanel.add(xpGainedLabel)
|
|
||||||
textPanel.add(xpLeftLabel)
|
|
||||||
textPanel.add(xpPerHourLabel)
|
|
||||||
textPanel.add(killsLabel)
|
|
||||||
|
|
||||||
widgetPanel.add(imageContainer, BorderLayout.WEST)
|
|
||||||
widgetPanel.add(textPanel, BorderLayout.CENTER)
|
|
||||||
widgetPanel.add(levelPanel, BorderLayout.SOUTH)
|
|
||||||
|
|
||||||
widgetPanel.revalidate()
|
|
||||||
if(focusedView == VIEW_NAME)
|
|
||||||
widgetPanel.repaint()
|
|
||||||
|
|
||||||
return XPWidget(
|
|
||||||
skillId = skillId,
|
|
||||||
container = widgetPanel,
|
|
||||||
xpGainedLabel = xpGainedLabel,
|
|
||||||
xpLeftLabel = xpLeftLabel,
|
|
||||||
xpPerHourLabel = xpPerHourLabel,
|
|
||||||
progressBar = progressBarPanel,
|
|
||||||
totalXpGained = 0,
|
|
||||||
actionsRemainingLabel = killsLabel,
|
|
||||||
startTime = System.currentTimeMillis(),
|
|
||||||
previousXp = previousXp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun wrappedWidget(component: Component, padding: Int = 7): Container {
|
|
||||||
val outerPanelSize = Dimension(
|
|
||||||
component.preferredSize.width + 2 * padding,
|
|
||||||
component.preferredSize.height + 2 * padding
|
|
||||||
)
|
|
||||||
val outerPanel = JPanel(GridBagLayout()).apply {
|
|
||||||
background = WIDGET_COLOR
|
|
||||||
preferredSize = outerPanelSize
|
|
||||||
maximumSize = outerPanelSize
|
|
||||||
minimumSize = outerPanelSize
|
|
||||||
}
|
|
||||||
val innerPanel = JPanel(BorderLayout()).apply {
|
|
||||||
background = WIDGET_COLOR
|
|
||||||
preferredSize = component.preferredSize
|
|
||||||
maximumSize = component.preferredSize
|
|
||||||
minimumSize = component.preferredSize
|
|
||||||
add(component, BorderLayout.CENTER)
|
|
||||||
}
|
|
||||||
val gbc = GridBagConstraints().apply {
|
|
||||||
anchor = GridBagConstraints.CENTER
|
|
||||||
}
|
|
||||||
outerPanel.add(innerPanel, gbc)
|
|
||||||
return outerPanel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
data class XPWidget(
|
|
||||||
val container: Container,
|
|
||||||
val skillId: Int,
|
|
||||||
val xpGainedLabel: JLabel,
|
|
||||||
val xpLeftLabel: JLabel,
|
|
||||||
val xpPerHourLabel: JLabel,
|
|
||||||
val actionsRemainingLabel: JLabel,
|
|
||||||
val progressBar: ProgressBar,
|
|
||||||
var totalXpGained: Int = 0,
|
|
||||||
var startTime: Long = System.currentTimeMillis(),
|
|
||||||
var previousXp: Int = 0
|
|
||||||
)
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
package KondoKit.components
|
||||||
|
|
||||||
|
import KondoKit.plugin.Companion.TITLE_BAR_COLOR
|
||||||
|
import KondoKit.plugin.Companion.WIDGET_COLOR
|
||||||
|
import KondoKit.plugin.Companion.secondaryColor
|
||||||
|
import java.awt.*
|
||||||
|
import javax.swing.*
|
||||||
|
|
||||||
|
class ButtonPanel(
|
||||||
|
private val alignment: Int = FlowLayout.CENTER,
|
||||||
|
private val hgap: Int = 5,
|
||||||
|
private val vgap: Int = 0
|
||||||
|
) : JPanel() {
|
||||||
|
|
||||||
|
init {
|
||||||
|
layout = FlowLayout(alignment, hgap, vgap)
|
||||||
|
background = WIDGET_COLOR
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addButton(text: String, action: () -> Unit): JButton {
|
||||||
|
val button = JButton(text).apply {
|
||||||
|
background = TITLE_BAR_COLOR
|
||||||
|
foreground = secondaryColor
|
||||||
|
font = Font("RuneScape Small", Font.PLAIN, 14)
|
||||||
|
addActionListener {
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add(button)
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addIcon(icon: Icon, action: () -> Unit): JButton {
|
||||||
|
val button = JButton(icon).apply {
|
||||||
|
background = TITLE_BAR_COLOR
|
||||||
|
foreground = secondaryColor
|
||||||
|
font = Font("RuneScape Small", Font.PLAIN, 14)
|
||||||
|
addActionListener {
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add(button)
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
package KondoKit.components
|
||||||
|
|
||||||
|
import KondoKit.ImageCanvas
|
||||||
|
import KondoKit.SpriteToBufferedImage
|
||||||
|
import KondoKit.plugin.Companion.WIDGET_COLOR
|
||||||
|
import KondoKit.plugin.Companion.primaryColor
|
||||||
|
import KondoKit.plugin.Companion.secondaryColor
|
||||||
|
import plugin.api.API
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.Color
|
||||||
|
import java.awt.Dimension
|
||||||
|
import javax.swing.BorderFactory
|
||||||
|
import javax.swing.JPanel
|
||||||
|
|
||||||
|
class IconComponent(
|
||||||
|
spriteId: Int,
|
||||||
|
private val iconWidth: Int = 25,
|
||||||
|
private val iconHeight: Int = 23,
|
||||||
|
private val useThemeColors: Boolean = true,
|
||||||
|
private val tint: Color? = null,
|
||||||
|
private val grayscale: Boolean = false,
|
||||||
|
private val brightnessBoost: Float = 1.0f
|
||||||
|
) : JPanel() {
|
||||||
|
|
||||||
|
private val imageCanvas: ImageCanvas
|
||||||
|
|
||||||
|
init {
|
||||||
|
layout = BorderLayout()
|
||||||
|
background = if (useThemeColors) WIDGET_COLOR else Color.WHITE
|
||||||
|
|
||||||
|
val bufferedImageSprite = SpriteToBufferedImage.getBufferedImageFromSprite(
|
||||||
|
API.GetSprite(spriteId),
|
||||||
|
tint,
|
||||||
|
grayscale,
|
||||||
|
brightnessBoost
|
||||||
|
)
|
||||||
|
|
||||||
|
imageCanvas = ImageCanvas(bufferedImageSprite).apply {
|
||||||
|
val size = Dimension(iconWidth, iconHeight)
|
||||||
|
preferredSize = size
|
||||||
|
maximumSize = size
|
||||||
|
minimumSize = size
|
||||||
|
background = if (useThemeColors) WIDGET_COLOR else Color.WHITE
|
||||||
|
}
|
||||||
|
|
||||||
|
add(imageCanvas, BorderLayout.CENTER)
|
||||||
|
border = BorderFactory.createEmptyBorder(2, 2, 2, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateFillColor(color: Color) {
|
||||||
|
imageCanvas.fillColor = color
|
||||||
|
background = color
|
||||||
|
imageCanvas.repaint()
|
||||||
|
repaint()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun applyThemeColors() {
|
||||||
|
updateFillColor(WIDGET_COLOR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package KondoKit.components
|
||||||
|
|
||||||
|
import KondoKit.Helpers.formatHtmlLabelText
|
||||||
|
import KondoKit.plugin.Companion.primaryColor
|
||||||
|
import KondoKit.plugin.Companion.secondaryColor
|
||||||
|
import java.awt.Font
|
||||||
|
import java.awt.Color
|
||||||
|
import javax.swing.JLabel
|
||||||
|
import javax.swing.SwingConstants
|
||||||
|
|
||||||
|
class LabelComponent(
|
||||||
|
text: String = "",
|
||||||
|
private val isHtml: Boolean = false,
|
||||||
|
private val alignment: Int = SwingConstants.LEFT
|
||||||
|
) : JLabel() {
|
||||||
|
|
||||||
|
init {
|
||||||
|
this.text = text
|
||||||
|
this.horizontalAlignment = alignment
|
||||||
|
this.font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateText(plainText: String, color: Color = secondaryColor) {
|
||||||
|
this.text = plainText
|
||||||
|
this.foreground = color
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateHtmlText(text1: String, color1: Color = primaryColor, text2: String = "", color2: Color = secondaryColor) {
|
||||||
|
this.text = formatHtmlLabelText(text1, color1, text2, color2)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAsHeader() {
|
||||||
|
font = Font("RuneScape Small", Font.BOLD, 16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package KondoKit.components
|
||||||
|
|
||||||
|
import KondoKit.plugin.Companion.POPUP_BACKGROUND
|
||||||
|
import KondoKit.plugin.Companion.POPUP_FOREGROUND
|
||||||
|
import java.awt.Font
|
||||||
|
import javax.swing.JMenuItem
|
||||||
|
import javax.swing.JPopupMenu
|
||||||
|
|
||||||
|
class PopupMenuComponent : JPopupMenu() {
|
||||||
|
|
||||||
|
init {
|
||||||
|
background = POPUP_BACKGROUND
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addMenuItem(text: String, action: () -> Unit): JMenuItem {
|
||||||
|
val menuItem = JMenuItem(text).apply {
|
||||||
|
font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
|
||||||
|
background = POPUP_BACKGROUND
|
||||||
|
foreground = POPUP_FOREGROUND
|
||||||
|
addActionListener {
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add(menuItem)
|
||||||
|
return menuItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package KondoKit
|
package KondoKit.components
|
||||||
|
|
||||||
import KondoKit.plugin.Companion.PROGRESS_BAR_FILL
|
import KondoKit.plugin.Companion.PROGRESS_BAR_FILL
|
||||||
import KondoKit.plugin.Companion.secondaryColor
|
import KondoKit.plugin.Companion.secondaryColor
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
package KondoKit.components.ReflectiveEditorComponents
|
||||||
|
|
||||||
|
import KondoKit.ImageCanvas
|
||||||
|
import KondoKit.plugin
|
||||||
|
import KondoKit.plugin.Companion.WIDGET_COLOR
|
||||||
|
import KondoKit.plugin.Companion.secondaryColor
|
||||||
|
import plugin.api.API
|
||||||
|
import java.awt.*
|
||||||
|
import java.awt.datatransfer.DataFlavor
|
||||||
|
import java.awt.event.ActionEvent
|
||||||
|
import java.awt.event.ActionListener
|
||||||
|
import java.awt.event.KeyAdapter
|
||||||
|
import java.awt.event.KeyEvent
|
||||||
|
import java.awt.event.MouseAdapter
|
||||||
|
import java.awt.event.MouseEvent
|
||||||
|
import java.awt.image.BufferedImage
|
||||||
|
import javax.imageio.ImageIO
|
||||||
|
import javax.swing.*
|
||||||
|
|
||||||
|
class CustomSearchField(private val parentPanel: JPanel, private val onSearch: (String) -> Unit) : Canvas() {
|
||||||
|
private var cursorVisible: Boolean = true
|
||||||
|
private var text: String = ""
|
||||||
|
|
||||||
|
private val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(1423)) // MAG_SPRITE
|
||||||
|
private val imageCanvas = bufferedImageSprite.let {
|
||||||
|
ImageCanvas(it).apply {
|
||||||
|
preferredSize = Dimension(12, 12) // ICON_DIMENSION_SMALL
|
||||||
|
size = preferredSize
|
||||||
|
minimumSize = preferredSize
|
||||||
|
maximumSize = preferredSize
|
||||||
|
fillColor = WIDGET_COLOR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to set the text programmatically
|
||||||
|
fun setText(newText: String) {
|
||||||
|
text = newText
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
repaint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to get the current text
|
||||||
|
fun getText(): String {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
preferredSize = Dimension(230, 30) // SEARCH_FIELD_DIMENSION
|
||||||
|
background = WIDGET_COLOR // COLOR_BACKGROUND_DARK
|
||||||
|
foreground = secondaryColor // COLOR_FOREGROUND_LIGHT
|
||||||
|
font = Font("Arial", Font.PLAIN, 14) // FONT_ARIAL_PLAIN_14
|
||||||
|
minimumSize = preferredSize
|
||||||
|
maximumSize = preferredSize
|
||||||
|
|
||||||
|
addKeyListener(object : KeyAdapter() {
|
||||||
|
override fun keyTyped(e: KeyEvent) {
|
||||||
|
// Prevent null character from being typed on Ctrl+A & Ctrl+V
|
||||||
|
if (e.isControlDown && (e.keyChar == '\u0001' || e.keyChar == '\u0016')) {
|
||||||
|
e.consume()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.keyChar == '\b') {
|
||||||
|
if (text.isNotEmpty()) {
|
||||||
|
text = text.dropLast(1)
|
||||||
|
}
|
||||||
|
} else if (e.keyChar == '\n') {
|
||||||
|
onSearch(text)
|
||||||
|
} else {
|
||||||
|
text += e.keyChar
|
||||||
|
}
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
repaint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun keyPressed(e: KeyEvent) {
|
||||||
|
if (e.isControlDown) {
|
||||||
|
when (e.keyCode) {
|
||||||
|
KeyEvent.VK_A -> {
|
||||||
|
text = ""
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
repaint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyEvent.VK_V -> {
|
||||||
|
val clipboard = Toolkit.getDefaultToolkit().systemClipboard
|
||||||
|
val pasteText = clipboard.getData(DataFlavor.stringFlavor) as String
|
||||||
|
text += pasteText
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
repaint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
addMouseListener(object : MouseAdapter() {
|
||||||
|
override fun mouseClicked(e: MouseEvent) {
|
||||||
|
if (e.x > width - 20 && e.y < 20) {
|
||||||
|
text = ""
|
||||||
|
onSearch(text)
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
repaint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
javax.swing.Timer(500, ActionListener { _ ->
|
||||||
|
cursorVisible = !cursorVisible
|
||||||
|
if (plugin.StateManager.focusedView == "REFLECTIVE_EDITOR_VIEW")
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
repaint()
|
||||||
|
}
|
||||||
|
}).start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun paint(g: Graphics) {
|
||||||
|
super.paint(g)
|
||||||
|
g.color = foreground
|
||||||
|
g.font = font
|
||||||
|
|
||||||
|
val fm = g.fontMetrics
|
||||||
|
val cursorX = fm.stringWidth(text) + 30
|
||||||
|
|
||||||
|
// Draw magnifying glass icon
|
||||||
|
imageCanvas.let { canvas ->
|
||||||
|
val imgG = g.create(5, 5, canvas.width, canvas.height)
|
||||||
|
canvas.paint(imgG)
|
||||||
|
imgG.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a local copy of the text to avoid threading issues
|
||||||
|
val currentText = text
|
||||||
|
|
||||||
|
// Draw placeholder text if field is empty, otherwise draw actual text
|
||||||
|
if (currentText == "") {
|
||||||
|
g.color = Color.GRAY // Use a lighter color for placeholder text
|
||||||
|
g.drawString("Search plugins...", 30, 20)
|
||||||
|
} else {
|
||||||
|
g.color = foreground // Use normal color for actual text
|
||||||
|
g.drawString(currentText, 30, 20)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursorVisible && hasFocus()) {
|
||||||
|
g.color = foreground
|
||||||
|
g.drawLine(cursorX, 5, cursorX, 25)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only draw the "x" button if there's text
|
||||||
|
if (currentText != "") {
|
||||||
|
g.color = Color.RED
|
||||||
|
g.drawString("x", width - 20, 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBufferedImageFromSprite(sprite: Any?): BufferedImage {
|
||||||
|
// This is a simplified version - you might need to adjust based on your actual implementation
|
||||||
|
// For now, let's just return a placeholder image
|
||||||
|
val image = BufferedImage(12, 12, BufferedImage.TYPE_INT_ARGB)
|
||||||
|
val g2d = image.createGraphics()
|
||||||
|
g2d.color = Color.GRAY
|
||||||
|
g2d.fillOval(2, 2, 8, 8)
|
||||||
|
g2d.drawLine(8, 8, 11, 11)
|
||||||
|
g2d.dispose()
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package KondoKit.components.ReflectiveEditorComponents
|
||||||
|
|
||||||
|
data class GitLabPlugin(
|
||||||
|
val id: String,
|
||||||
|
val path: String,
|
||||||
|
val pluginProperties: PluginProperties?,
|
||||||
|
val pluginError: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PluginProperties(
|
||||||
|
val author: String,
|
||||||
|
val version: String,
|
||||||
|
val description: String
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,185 @@
|
||||||
|
package KondoKit.components.ReflectiveEditorComponents
|
||||||
|
|
||||||
|
import KondoKit.views.ReflectiveEditorView
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import java.awt.EventQueue
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
import java.util.concurrent.*
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
|
object GitLabPluginFetcher {
|
||||||
|
private const val GITLAB_PROJECT_ID = "38297322"
|
||||||
|
private const val GITLAB_BRANCH = "master"
|
||||||
|
private const val CHROME_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
|
||||||
|
private const val DEBUG = true // Set to false to disable debug logging
|
||||||
|
|
||||||
|
// Thread pool for concurrent plugin property fetching
|
||||||
|
private val executorService = Executors.newFixedThreadPool(5)
|
||||||
|
|
||||||
|
// Debug logging function
|
||||||
|
private fun debugLog(message: String) {
|
||||||
|
if (DEBUG) {
|
||||||
|
println("[GitLabPluginFetcher] $message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to fetch plugins from GitLab
|
||||||
|
fun fetchGitLabPlugins(onComplete: (List<GitLabPlugin>) -> Unit) {
|
||||||
|
Thread {
|
||||||
|
try {
|
||||||
|
debugLog("Starting to fetch GitLab plugins...")
|
||||||
|
val plugins = mutableListOf<GitLabPlugin>()
|
||||||
|
val apiUrl = "https://gitlab.com/api/v4/projects/${GITLAB_PROJECT_ID}/repository/tree?ref=${GITLAB_BRANCH}&per_page=100"
|
||||||
|
debugLog("API URL: $apiUrl")
|
||||||
|
|
||||||
|
// Create URL connection
|
||||||
|
val url = URL(apiUrl)
|
||||||
|
val connection = url.openConnection() as HttpURLConnection
|
||||||
|
connection.requestMethod = "GET"
|
||||||
|
connection.setRequestProperty("User-Agent", CHROME_USER_AGENT)
|
||||||
|
debugLog("Set User-Agent header to: $CHROME_USER_AGENT")
|
||||||
|
|
||||||
|
// Read response
|
||||||
|
val responseCode = connection.responseCode
|
||||||
|
debugLog("Response code: $responseCode")
|
||||||
|
|
||||||
|
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||||
|
val reader = BufferedReader(InputStreamReader(connection.inputStream))
|
||||||
|
val response = reader.use { it.readText() }
|
||||||
|
debugLog("Response length: ${response.length} characters")
|
||||||
|
debugLog("Response preview: ${response.take(200)}...")
|
||||||
|
|
||||||
|
// Parse JSON response
|
||||||
|
val gson = Gson()
|
||||||
|
val treeItems = gson.fromJson(response, Array<JsonObject>::class.java)
|
||||||
|
debugLog("Parsed ${treeItems.size} items from JSON")
|
||||||
|
|
||||||
|
// Filter for directories (trees) with mode "040000"
|
||||||
|
val pluginDirectories = mutableListOf<Pair<String, String>>()
|
||||||
|
for (jsonObject in treeItems) {
|
||||||
|
if (jsonObject["type"].asString == "tree" && jsonObject["mode"].asString == "040000") {
|
||||||
|
val folderId = jsonObject["id"].asString
|
||||||
|
val folderPath = jsonObject["path"].asString
|
||||||
|
pluginDirectories.add(folderId to folderPath)
|
||||||
|
debugLog("Found directory: $folderPath (ID: $folderId)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debugLog("Found ${pluginDirectories.size} plugin directories")
|
||||||
|
|
||||||
|
// Fetch plugin properties in parallel
|
||||||
|
val pluginFutures = mutableListOf<Future<GitLabPlugin>>()
|
||||||
|
for ((folderId, folderPath) in pluginDirectories) {
|
||||||
|
val future = executorService.submit(Callable<GitLabPlugin> {
|
||||||
|
try {
|
||||||
|
val pluginProperties = fetchPluginProperties(folderPath)
|
||||||
|
debugLog("Successfully fetched properties for: $folderPath")
|
||||||
|
GitLabPlugin(folderId, folderPath, pluginProperties, null)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
debugLog("Error fetching plugin.properties for $folderPath: ${e.message}")
|
||||||
|
GitLabPlugin(folderId, folderPath, null, "Error fetching plugin.properties")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
pluginFutures.add(future)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect results
|
||||||
|
for (future in pluginFutures) {
|
||||||
|
try {
|
||||||
|
plugins.add(future.get())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
debugLog("Error getting plugin result: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debugLog("HTTP error: $responseCode")
|
||||||
|
val errorReader = BufferedReader(InputStreamReader(connection.errorStream))
|
||||||
|
val errorResponse = errorReader.use { it.readText() }
|
||||||
|
debugLog("Error response: $errorResponse")
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog("Completed fetching plugins. Total plugins: ${plugins.size}")
|
||||||
|
// Update on UI thread
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
onComplete(plugins)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
debugLog("Exception occurred: ${e.message}")
|
||||||
|
e.printStackTrace()
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
onComplete(emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to fetch plugin.properties from a specific folder
|
||||||
|
private fun fetchPluginProperties(folderPath: String): PluginProperties {
|
||||||
|
val pluginFilePath = "$folderPath/plugin.properties"
|
||||||
|
val pluginUrl = "https://gitlab.com/api/v4/projects/${GITLAB_PROJECT_ID}/repository/files/${pluginFilePath.replace("/", "%2F")}/raw?ref=${GITLAB_BRANCH}"
|
||||||
|
debugLog("Fetching plugin.properties from: $pluginUrl")
|
||||||
|
|
||||||
|
// Create URL connection
|
||||||
|
val url = URL(pluginUrl)
|
||||||
|
val connection = url.openConnection() as HttpURLConnection
|
||||||
|
connection.requestMethod = "GET"
|
||||||
|
connection.setRequestProperty("User-Agent", CHROME_USER_AGENT)
|
||||||
|
|
||||||
|
// Read response
|
||||||
|
val responseCode = connection.responseCode
|
||||||
|
debugLog("plugin.properties response code: $responseCode")
|
||||||
|
|
||||||
|
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||||
|
val reader = BufferedReader(InputStreamReader(connection.inputStream))
|
||||||
|
val rawContent = reader.use { it.readText() }
|
||||||
|
debugLog("plugin.properties content length: ${rawContent.length} characters")
|
||||||
|
debugLog("plugin.properties content preview: ${rawContent.take(200)}...")
|
||||||
|
|
||||||
|
return parseProperties(rawContent)
|
||||||
|
} else {
|
||||||
|
debugLog("Failed to fetch plugin.properties. Response code: $responseCode")
|
||||||
|
val errorReader = BufferedReader(InputStreamReader(connection.errorStream))
|
||||||
|
val errorResponse = errorReader.use { it.readText() }
|
||||||
|
debugLog("Error response for plugin.properties: $errorResponse")
|
||||||
|
throw Exception("Plugin file not found for folder: $folderPath")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to parse plugin.properties content
|
||||||
|
private fun parseProperties(content: String): PluginProperties {
|
||||||
|
debugLog("Parsing plugin.properties content")
|
||||||
|
val lines = content.split("\n")
|
||||||
|
var author = "Unknown"
|
||||||
|
var version = "Unknown"
|
||||||
|
var description = "No description available"
|
||||||
|
|
||||||
|
for (line in lines) {
|
||||||
|
val parts = line.split("=")
|
||||||
|
if (parts.size == 2) {
|
||||||
|
val key = parts[0].trim()
|
||||||
|
val value = parts[1].replace("\"", "").replace("'", "").trim()
|
||||||
|
|
||||||
|
when {
|
||||||
|
key.startsWith("AUTHOR", ignoreCase = true) -> {
|
||||||
|
author = value
|
||||||
|
debugLog("Found author: $value")
|
||||||
|
}
|
||||||
|
key.startsWith("VERSION", ignoreCase = true) -> {
|
||||||
|
version = value
|
||||||
|
debugLog("Found version: $value")
|
||||||
|
}
|
||||||
|
key.startsWith("DESCRIPTION", ignoreCase = true) -> {
|
||||||
|
description = value
|
||||||
|
debugLog("Found description: $value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog("Parsed properties - Author: $author, Version: $version, Description: $description")
|
||||||
|
return PluginProperties(author, version, description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package KondoKit.components.ReflectiveEditorComponents
|
||||||
|
|
||||||
|
import java.util.concurrent.ExecutorService
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple test to verify GitLabPluginFetcher debug logging works
|
||||||
|
*/
|
||||||
|
fun main() {
|
||||||
|
println("Testing GitLabPluginFetcher with debug logging...")
|
||||||
|
|
||||||
|
GitLabPluginFetcher.fetchGitLabPlugins { plugins ->
|
||||||
|
println("Fetched ${plugins.size} plugins")
|
||||||
|
plugins.forEach { plugin ->
|
||||||
|
println("- ${plugin.path}: ${plugin.pluginProperties?.description ?: plugin.pluginError}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown the executor service
|
||||||
|
try {
|
||||||
|
val field = GitLabPluginFetcher::class.java.getDeclaredField("executorService")
|
||||||
|
field.isAccessible = true
|
||||||
|
val executorService = field.get(GitLabPluginFetcher) as ExecutorService
|
||||||
|
executorService.shutdown()
|
||||||
|
if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||||
|
executorService.shutdownNow()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give the async fetch some time to complete
|
||||||
|
Thread.sleep(10000)
|
||||||
|
println("Test completed")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,413 @@
|
||||||
|
package KondoKit.components.ReflectiveEditorComponents
|
||||||
|
|
||||||
|
import KondoKit.views.ReflectiveEditorView
|
||||||
|
import plugin.PluginRepository
|
||||||
|
import java.awt.EventQueue
|
||||||
|
import java.io.*
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
import java.util.concurrent.*
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipInputStream
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages downloading and installing plugins from GitLab with concurrent support
|
||||||
|
*/
|
||||||
|
object PluginDownloadManager {
|
||||||
|
private const val GITLAB_PROJECT_PATH = "2009scape/tools/client-plugins"
|
||||||
|
private const val GITLAB_BRANCH = "master"
|
||||||
|
private const val CHROME_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
|
||||||
|
private const val MAX_CONCURRENT_DOWNLOADS = 3
|
||||||
|
private const val DEBUG = true // Set to false to disable debug logging
|
||||||
|
|
||||||
|
// Thread pool for concurrent downloads
|
||||||
|
private val downloadExecutor = Executors.newFixedThreadPool(MAX_CONCURRENT_DOWNLOADS)
|
||||||
|
|
||||||
|
// Callback for download progress updates
|
||||||
|
interface DownloadProgressCallback {
|
||||||
|
fun onProgress(pluginName: String, progress: Int)
|
||||||
|
fun onComplete(pluginName: String, success: Boolean, errorMessage: String? = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug logging function
|
||||||
|
private fun debugLog(message: String) {
|
||||||
|
if (DEBUG) {
|
||||||
|
println("[PluginDownloadManager] $message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test URL method for debugging
|
||||||
|
fun testDownloadUrl(plugin: GitLabPlugin): String {
|
||||||
|
return "https://gitlab.com/$GITLAB_PROJECT_PATH/-/archive/$GITLAB_BRANCH/${GITLAB_PROJECT_PATH.replace("/", "-")}-$GITLAB_BRANCH.zip?path=${plugin.path}"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download a single plugin
|
||||||
|
*/
|
||||||
|
fun downloadPlugin(plugin: GitLabPlugin, callback: (String, Boolean, String?) -> Unit) {
|
||||||
|
downloadExecutor.submit {
|
||||||
|
try {
|
||||||
|
debugLog("Starting download for plugin: ${plugin.path}")
|
||||||
|
|
||||||
|
// Download the plugin as a ZIP archive
|
||||||
|
val success = downloadAndExtractPlugin(plugin, object : DownloadProgressCallback {
|
||||||
|
override fun onProgress(pluginName: String, progress: Int) {
|
||||||
|
// We don't need to do anything here for the simple callback
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onComplete(pluginName: String, success: Boolean, errorMessage: String?) {
|
||||||
|
callback(pluginName, success, errorMessage)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
debugLog("Successfully downloaded and extracted plugin: ${plugin.path}")
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
callback(plugin.path, true, null)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debugLog("Failed to download plugin: ${plugin.path}")
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
callback(plugin.path, false, "Failed to download plugin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
debugLog("Exception during download of ${plugin.path}: ${e.message}")
|
||||||
|
e.printStackTrace()
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
callback(plugin.path, false, e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download a single plugin with progress updates
|
||||||
|
*/
|
||||||
|
fun downloadPlugin(plugin: GitLabPlugin, callback: DownloadProgressCallback) {
|
||||||
|
downloadExecutor.submit {
|
||||||
|
try {
|
||||||
|
debugLog("Starting download for plugin: ${plugin.path}")
|
||||||
|
callback.onProgress(plugin.path, 0)
|
||||||
|
|
||||||
|
// Download the plugin as a ZIP archive
|
||||||
|
val success = downloadAndExtractPlugin(plugin, callback)
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
debugLog("Successfully downloaded and extracted plugin: ${plugin.path}")
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
callback.onComplete(plugin.path, true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debugLog("Failed to download plugin: ${plugin.path}")
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
callback.onComplete(plugin.path, false, "Failed to download plugin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
debugLog("Exception during download of ${plugin.path}: ${e.message}")
|
||||||
|
e.printStackTrace()
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
callback.onComplete(plugin.path, false, e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download multiple plugins concurrently
|
||||||
|
*/
|
||||||
|
fun downloadPlugins(plugins: List<GitLabPlugin>, callback: (String, Boolean, String?) -> Unit) {
|
||||||
|
debugLog("Starting concurrent download of ${plugins.size} plugins")
|
||||||
|
|
||||||
|
// Submit all downloads to the executor
|
||||||
|
for (plugin in plugins) {
|
||||||
|
downloadPlugin(plugin, object : DownloadProgressCallback {
|
||||||
|
override fun onProgress(pluginName: String, progress: Int) {
|
||||||
|
// We don't need to do anything here for the simple callback
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onComplete(pluginName: String, success: Boolean, errorMessage: String?) {
|
||||||
|
callback(pluginName, success, errorMessage)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download and extract a plugin from GitLab
|
||||||
|
*/
|
||||||
|
private fun downloadAndExtractPlugin(plugin: GitLabPlugin, callback: DownloadProgressCallback): Boolean {
|
||||||
|
try {
|
||||||
|
// Validate plugin path
|
||||||
|
if (plugin.path.isBlank()) {
|
||||||
|
debugLog("Plugin path is blank for plugin: ${plugin.path}")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog("Starting download for plugin: ${plugin.path}")
|
||||||
|
|
||||||
|
// Try multiple URL formats since GitLab has changed their API
|
||||||
|
val downloadUrls = listOf(
|
||||||
|
// Format 1: Using ref_type parameter
|
||||||
|
"https://gitlab.com/$GITLAB_PROJECT_PATH/-/archive/$GITLAB_BRANCH/${GITLAB_PROJECT_PATH.replace("/", "-")}-$GITLAB_BRANCH.zip?path=${plugin.path}&ref_type=heads",
|
||||||
|
// Format 2: Using ref parameter
|
||||||
|
"https://gitlab.com/$GITLAB_PROJECT_PATH/-/archive/$GITLAB_BRANCH/${GITLAB_PROJECT_PATH.replace("/", "-")}-$GITLAB_BRANCH.zip?path=${plugin.path}",
|
||||||
|
// Format 3: Direct archive URL without path parameter (we'll filter later)
|
||||||
|
"https://gitlab.com/$GITLAB_PROJECT_PATH/-/archive/$GITLAB_BRANCH/${GITLAB_PROJECT_PATH.replace("/", "-")}-$GITLAB_BRANCH.zip"
|
||||||
|
)
|
||||||
|
|
||||||
|
for (downloadUrl in downloadUrls) {
|
||||||
|
debugLog("Trying download URL: $downloadUrl")
|
||||||
|
|
||||||
|
// Validate URL
|
||||||
|
val url = try {
|
||||||
|
URL(downloadUrl)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
debugLog("Invalid URL: $downloadUrl for plugin: ${plugin.path}. Error: ${e.message}")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create URL connection
|
||||||
|
val connection = url.openConnection() as HttpURLConnection
|
||||||
|
connection.requestMethod = "GET"
|
||||||
|
connection.setRequestProperty("User-Agent", CHROME_USER_AGENT)
|
||||||
|
// Add Accept header to avoid 406 errors
|
||||||
|
connection.setRequestProperty("Accept", "*/*")
|
||||||
|
|
||||||
|
debugLog("Connection opened for plugin: ${plugin.path}")
|
||||||
|
debugLog("Request headers - User-Agent: ${connection.getRequestProperty("User-Agent")}")
|
||||||
|
debugLog("Request headers - Accept: ${connection.getRequestProperty("Accept")}")
|
||||||
|
|
||||||
|
// Get content length for progress tracking
|
||||||
|
val contentLength = connection.contentLength
|
||||||
|
debugLog("Content length: $contentLength bytes for plugin: ${plugin.path}")
|
||||||
|
|
||||||
|
// Read response
|
||||||
|
val responseCode = connection.responseCode
|
||||||
|
debugLog("Response code: $responseCode for plugin: ${plugin.path}")
|
||||||
|
|
||||||
|
// Log response message
|
||||||
|
val responseMessage = connection.responseMessage
|
||||||
|
debugLog("Response message: $responseMessage for plugin: ${plugin.path}")
|
||||||
|
|
||||||
|
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||||
|
// Check if input stream is available
|
||||||
|
val inputStream = connection.inputStream
|
||||||
|
if (inputStream == null) {
|
||||||
|
debugLog("Input stream is null for plugin: ${plugin.path}")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog("Input stream available for plugin: ${plugin.path}")
|
||||||
|
|
||||||
|
// Create output directory
|
||||||
|
val pluginsDir = ReflectiveEditorView.pluginsDirectory
|
||||||
|
debugLog("Plugins directory: ${pluginsDir.absolutePath}")
|
||||||
|
|
||||||
|
// Validate plugins directory
|
||||||
|
if (!pluginsDir.exists()) {
|
||||||
|
debugLog("Plugins directory does not exist: ${pluginsDir.absolutePath}")
|
||||||
|
if (!pluginsDir.mkdirs()) {
|
||||||
|
debugLog("Failed to create plugins directory: ${pluginsDir.absolutePath}")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
debugLog("Created plugins directory: ${pluginsDir.absolutePath}")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pluginsDir.isDirectory) {
|
||||||
|
debugLog("Plugins path is not a directory: ${pluginsDir.absolutePath}")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val pluginDir = File(pluginsDir, plugin.path)
|
||||||
|
debugLog("Plugin directory: ${pluginDir.absolutePath}")
|
||||||
|
|
||||||
|
// Create directory if it doesn't exist
|
||||||
|
if (!pluginDir.exists()) {
|
||||||
|
pluginDir.mkdirs()
|
||||||
|
debugLog("Created plugin directory: ${pluginDir.absolutePath}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download the ZIP file
|
||||||
|
val tempZipFile = File.createTempFile("plugin_", ".zip")
|
||||||
|
tempZipFile.deleteOnExit()
|
||||||
|
debugLog("Created temp file: ${tempZipFile.absolutePath}")
|
||||||
|
|
||||||
|
var totalBytesRead = 0L
|
||||||
|
inputStream.use { input ->
|
||||||
|
FileOutputStream(tempZipFile).use { outputStream ->
|
||||||
|
val buffer = ByteArray(8192)
|
||||||
|
var bytesRead: Int
|
||||||
|
|
||||||
|
debugLog("Starting to read data for plugin: ${plugin.path}")
|
||||||
|
|
||||||
|
while (input.read(buffer).also { bytesRead = it } != -1) {
|
||||||
|
outputStream.write(buffer, 0, bytesRead)
|
||||||
|
totalBytesRead += bytesRead
|
||||||
|
|
||||||
|
// Report progress if content length is known
|
||||||
|
if (contentLength > 0) {
|
||||||
|
val progress = (totalBytesRead * 100 / contentLength).toInt()
|
||||||
|
// Limit progress updates to avoid flooding the UI
|
||||||
|
if (progress % 5 == 0) {
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
callback.onProgress(plugin.path, progress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog("Finished reading data for plugin: ${plugin.path}. Total bytes: $totalBytesRead")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog("Downloaded ${totalBytesRead} bytes to ${tempZipFile.absolutePath}")
|
||||||
|
|
||||||
|
// Extract the ZIP file
|
||||||
|
if (extractZipFile(tempZipFile, pluginDir, plugin.path)) {
|
||||||
|
debugLog("Successfully extracted plugin to ${pluginDir.absolutePath}")
|
||||||
|
// Clean up temp file
|
||||||
|
tempZipFile.delete()
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
debugLog("Failed to extract plugin")
|
||||||
|
tempZipFile.delete()
|
||||||
|
continue // Try next URL format
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debugLog("HTTP error: $responseCode for plugin: ${plugin.path} with URL: $downloadUrl")
|
||||||
|
|
||||||
|
// Try to read error stream for more details
|
||||||
|
try {
|
||||||
|
val errorStream = connection.errorStream
|
||||||
|
if (errorStream != null) {
|
||||||
|
val errorReader = BufferedReader(InputStreamReader(errorStream))
|
||||||
|
val errorResponse = errorReader.use { it.readText() }
|
||||||
|
debugLog("Error response: $errorResponse for plugin: ${plugin.path}")
|
||||||
|
} else {
|
||||||
|
debugLog("Error stream is null for plugin: ${plugin.path}")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
debugLog("Exception while reading error stream: ${e.message} for plugin: ${plugin.path}")
|
||||||
|
}
|
||||||
|
|
||||||
|
continue // Try next URL format
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog("All URL formats failed for plugin: ${plugin.path}")
|
||||||
|
return false
|
||||||
|
} catch (e: Exception) {
|
||||||
|
debugLog("Exception during download and extraction for plugin ${plugin.path}: ${e.message}")
|
||||||
|
e.printStackTrace()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a ZIP file to the specified directory
|
||||||
|
*/
|
||||||
|
private fun extractZipFile(zipFile: File, targetDir: File, pluginPath: String): Boolean {
|
||||||
|
try {
|
||||||
|
debugLog("Extracting ZIP file: ${zipFile.absolutePath} to ${targetDir.absolutePath}")
|
||||||
|
|
||||||
|
// Validate inputs
|
||||||
|
if (!zipFile.exists()) {
|
||||||
|
debugLog("ZIP file does not exist: ${zipFile.absolutePath}")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetDir.exists() && !targetDir.mkdirs()) {
|
||||||
|
debugLog("Failed to create target directory: ${targetDir.absolutePath}")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
ZipInputStream(FileInputStream(zipFile)).use { zis ->
|
||||||
|
var entry: ZipEntry?
|
||||||
|
var entryCount = 0
|
||||||
|
|
||||||
|
while (zis.nextEntry.also { entry = it } != null) {
|
||||||
|
entryCount++
|
||||||
|
val entryName = entry!!.name
|
||||||
|
|
||||||
|
debugLog("Processing ZIP entry $entryCount: $entryName")
|
||||||
|
|
||||||
|
// Skip the top-level directory in the ZIP (GitLab adds a project-branch-hash directory)
|
||||||
|
// We want to extract the contents of the plugin directory directly
|
||||||
|
val relativePath = if (entryName.contains("/")) {
|
||||||
|
entryName.substring(entryName.indexOf("/") + 1)
|
||||||
|
} else {
|
||||||
|
entryName
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog("Relative path for entry: $relativePath")
|
||||||
|
|
||||||
|
// Only extract files that are part of this specific plugin
|
||||||
|
if (relativePath.startsWith(pluginPath) && relativePath != pluginPath) {
|
||||||
|
val fileName = relativePath.substring(pluginPath.length + 1) // +1 for the trailing slash
|
||||||
|
|
||||||
|
debugLog("File name to extract: $fileName")
|
||||||
|
|
||||||
|
if (fileName.isNotEmpty()) {
|
||||||
|
val file = File(targetDir, fileName)
|
||||||
|
|
||||||
|
debugLog("Target file path: ${file.absolutePath}")
|
||||||
|
|
||||||
|
// Create parent directories if needed
|
||||||
|
val parent = file.parentFile
|
||||||
|
if (parent != null && !parent.exists()) {
|
||||||
|
if (parent.mkdirs()) {
|
||||||
|
debugLog("Created parent directories: ${parent.absolutePath}")
|
||||||
|
} else {
|
||||||
|
debugLog("Failed to create parent directories: ${parent.absolutePath}")
|
||||||
|
zis.closeEntry()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract file or directory
|
||||||
|
if (entry!!.isDirectory) {
|
||||||
|
if (!file.exists()) {
|
||||||
|
if (file.mkdirs()) {
|
||||||
|
debugLog("Created directory: ${file.absolutePath}")
|
||||||
|
} else {
|
||||||
|
debugLog("Failed to create directory: ${file.absolutePath}")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debugLog("Directory already exists: ${file.absolutePath}")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create file
|
||||||
|
try {
|
||||||
|
FileOutputStream(file).use { fos ->
|
||||||
|
zis.copyTo(fos)
|
||||||
|
}
|
||||||
|
debugLog("Extracted file: ${file.absolutePath}")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
debugLog("Failed to extract file ${file.absolutePath}: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debugLog("Skipping empty file name")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debugLog("Skipping entry (not part of plugin): $relativePath")
|
||||||
|
}
|
||||||
|
|
||||||
|
zis.closeEntry()
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog("Processed $entryCount entries from ZIP file")
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog("Successfully extracted ZIP file")
|
||||||
|
return true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
debugLog("Exception during ZIP extraction: ${e.message}")
|
||||||
|
e.printStackTrace()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package KondoKit.components.ReflectiveEditorComponents
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data class representing a plugin with its installation status and update information
|
||||||
|
*/
|
||||||
|
data class PluginStatus(
|
||||||
|
val name: String,
|
||||||
|
val installedVersion: String?,
|
||||||
|
val remoteVersion: String?,
|
||||||
|
val description: String?,
|
||||||
|
val author: String?,
|
||||||
|
val gitLabPlugin: GitLabPlugin?,
|
||||||
|
val isInstalled: Boolean,
|
||||||
|
val needsUpdate: Boolean,
|
||||||
|
val isDownloading: Boolean = false,
|
||||||
|
val downloadProgress: Int = 0
|
||||||
|
)
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package KondoKit
|
package KondoKit.components
|
||||||
|
|
||||||
import KondoKit.plugin.Companion.SCROLL_BAR_COLOR
|
import KondoKit.plugin.Companion.SCROLL_BAR_COLOR
|
||||||
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
|
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
package KondoKit.components
|
||||||
|
|
||||||
|
import KondoKit.ImageCanvas
|
||||||
|
import KondoKit.SpriteToBufferedImage
|
||||||
|
import KondoKit.plugin.Companion.WIDGET_COLOR
|
||||||
|
import KondoKit.plugin.Companion.secondaryColor
|
||||||
|
import KondoKit.plugin.StateManager.focusedView
|
||||||
|
import plugin.api.API
|
||||||
|
import java.awt.*
|
||||||
|
import java.awt.datatransfer.DataFlavor
|
||||||
|
import java.awt.event.*
|
||||||
|
import javax.swing.*
|
||||||
|
|
||||||
|
open class SearchField(
|
||||||
|
protected val onSearch: (String) -> Unit,
|
||||||
|
private val placeholderText: String = "Search...",
|
||||||
|
private val fieldWidth: Int = 230,
|
||||||
|
private val fieldHeight: Int = 30,
|
||||||
|
private val viewName: String? = null
|
||||||
|
) : Canvas() {
|
||||||
|
|
||||||
|
private var cursorVisible: Boolean = true
|
||||||
|
private var text: String = ""
|
||||||
|
|
||||||
|
private val bufferedImageSprite = SpriteToBufferedImage.getBufferedImageFromSprite(API.GetSprite(1423)) // MAG_SPRITE
|
||||||
|
private val imageCanvas = bufferedImageSprite.let {
|
||||||
|
ImageCanvas(it).apply {
|
||||||
|
preferredSize = Dimension(12, 12)
|
||||||
|
size = preferredSize
|
||||||
|
minimumSize = preferredSize
|
||||||
|
maximumSize = preferredSize
|
||||||
|
fillColor = WIDGET_COLOR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
val dimension = Dimension(fieldWidth, fieldHeight)
|
||||||
|
preferredSize = dimension
|
||||||
|
background = WIDGET_COLOR
|
||||||
|
foreground = secondaryColor
|
||||||
|
font = Font("Arial", Font.PLAIN, 14)
|
||||||
|
minimumSize = dimension
|
||||||
|
maximumSize = dimension
|
||||||
|
|
||||||
|
addKeyListener(object : KeyAdapter() {
|
||||||
|
override fun keyTyped(e: KeyEvent) {
|
||||||
|
// Prevent null character from being typed on Ctrl+A & Ctrl+V
|
||||||
|
if (e.isControlDown && (e.keyChar == '\u0001' || e.keyChar == '\u0016')) {
|
||||||
|
e.consume()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.keyChar == '\b') {
|
||||||
|
if (text.isNotEmpty()) {
|
||||||
|
text = text.dropLast(1)
|
||||||
|
}
|
||||||
|
} else if (e.keyChar == '\n') {
|
||||||
|
triggerSearch()
|
||||||
|
} else {
|
||||||
|
text += e.keyChar
|
||||||
|
}
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
repaint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun keyPressed(e: KeyEvent) {
|
||||||
|
if (e.isControlDown) {
|
||||||
|
when (e.keyCode) {
|
||||||
|
KeyEvent.VK_A -> {
|
||||||
|
text = ""
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
repaint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyEvent.VK_V -> {
|
||||||
|
try {
|
||||||
|
val clipboard = Toolkit.getDefaultToolkit().systemClipboard
|
||||||
|
val pasteText = clipboard.getData(DataFlavor.stringFlavor) as String
|
||||||
|
text += pasteText
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
repaint()
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
// Ignore clipboard errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
addMouseListener(object : MouseAdapter() {
|
||||||
|
override fun mouseClicked(e: MouseEvent) {
|
||||||
|
if (e.x > width - 20 && e.y < 20) {
|
||||||
|
text = ""
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
repaint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Timer(500) { _ ->
|
||||||
|
cursorVisible = !cursorVisible
|
||||||
|
// Only repaint if the view is active or if viewName is not specified
|
||||||
|
if (viewName == null || focusedView == viewName) {
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
repaint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun paint(g: Graphics) {
|
||||||
|
super.paint(g)
|
||||||
|
g.color = foreground
|
||||||
|
g.font = font
|
||||||
|
|
||||||
|
val fm = g.fontMetrics
|
||||||
|
val cursorX = fm.stringWidth(text) + 30
|
||||||
|
|
||||||
|
// Draw magnifying glass icon
|
||||||
|
imageCanvas.let { canvas ->
|
||||||
|
val imgG = g.create(5, 5, canvas.width, canvas.height)
|
||||||
|
canvas.paint(imgG)
|
||||||
|
imgG.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a local copy of the text to avoid threading issues
|
||||||
|
val currentText = text
|
||||||
|
|
||||||
|
// Draw placeholder text if field is empty, otherwise draw actual text
|
||||||
|
if (currentText.isEmpty()) {
|
||||||
|
g.color = Color.GRAY // Use a lighter color for placeholder text
|
||||||
|
g.drawString(placeholderText, 30, 20)
|
||||||
|
} else {
|
||||||
|
g.color = foreground // Use normal color for actual text
|
||||||
|
g.drawString(currentText, 30, 20)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursorVisible && hasFocus()) {
|
||||||
|
g.color = foreground
|
||||||
|
g.drawLine(cursorX, 5, cursorX, 25)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only draw the "x" button if there's text
|
||||||
|
if (currentText.isNotEmpty()) {
|
||||||
|
g.color = Color.RED
|
||||||
|
g.drawString("x", width - 20, 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setText(newText: String) {
|
||||||
|
text = newText
|
||||||
|
repaint()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getText(): String = text
|
||||||
|
|
||||||
|
private fun triggerSearch() {
|
||||||
|
val query = text.trim()
|
||||||
|
if (query.isNotEmpty()) {
|
||||||
|
text = query
|
||||||
|
repaint()
|
||||||
|
onSearch(query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,503 @@
|
||||||
|
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)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
} ?: ""
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// Add mouse listener to the label only if a description is available
|
||||||
|
if (description.isNotBlank()) {
|
||||||
|
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||||
|
addMouseListener(object : MouseAdapter() {
|
||||||
|
override fun mouseEntered(e: MouseEvent) {
|
||||||
|
showCustomToolTip(description, this@apply)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mouseExited(e: MouseEvent) {
|
||||||
|
SettingsPanel.customToolTipWindow?.isVisible = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(120, 30)
|
||||||
|
addActionListener {
|
||||||
|
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>")) {
|
||||||
|
try {
|
||||||
|
val intValue = value.toInt()
|
||||||
|
intValue
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
Helpers.showToast(
|
||||||
|
this@SettingsPanel,
|
||||||
|
"Invalid number format for key '$key': '$value'. Using 0 as default.",
|
||||||
|
JOptionPane.WARNING_MESSAGE
|
||||||
|
)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
"${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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
package KondoKit.components
|
||||||
|
|
||||||
|
import java.awt.*
|
||||||
|
import java.awt.event.MouseAdapter
|
||||||
|
import java.awt.event.MouseEvent
|
||||||
|
import java.awt.image.BufferedImage
|
||||||
|
import javax.swing.JPanel
|
||||||
|
|
||||||
|
class ToggleSwitch : JPanel() {
|
||||||
|
private var activated = false
|
||||||
|
private var switchColor = Color(200, 200, 200)
|
||||||
|
private var buttonColor = Color(255, 255, 255)
|
||||||
|
private var borderColor = Color(50, 50, 50)
|
||||||
|
private var activeSwitch = Color(0, 125, 255)
|
||||||
|
private var puffer: BufferedImage? = null
|
||||||
|
private var g: Graphics2D? = null
|
||||||
|
|
||||||
|
var onToggleListener: ((Boolean) -> Unit)? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
isVisible = true
|
||||||
|
addMouseListener(object : MouseAdapter() {
|
||||||
|
override fun mouseReleased(arg0: MouseEvent) {
|
||||||
|
activated = !activated
|
||||||
|
onToggleListener?.invoke(activated)
|
||||||
|
repaint()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
cursor = Cursor(Cursor.HAND_CURSOR)
|
||||||
|
preferredSize = Dimension(32, 20)
|
||||||
|
maximumSize = Dimension(32, 20)
|
||||||
|
minimumSize = Dimension(32, 20)
|
||||||
|
isOpaque = false // Make the panel background transparent
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun paint(gr: Graphics) {
|
||||||
|
if (g == null || puffer?.width != width || puffer?.height != height) {
|
||||||
|
puffer = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)
|
||||||
|
g = puffer?.createGraphics()
|
||||||
|
val rh = RenderingHints(
|
||||||
|
RenderingHints.KEY_ANTIALIASING,
|
||||||
|
RenderingHints.VALUE_ANTIALIAS_ON
|
||||||
|
)
|
||||||
|
g?.setRenderingHints(rh)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the buffer with transparent background
|
||||||
|
g?.color = Color(0, 0, 0, 0)
|
||||||
|
g?.fillRect(0, 0, width, height)
|
||||||
|
|
||||||
|
// Draw the track with circular ends (rounded rectangle)
|
||||||
|
val trackHeight = height - 2
|
||||||
|
val trackWidth = width - 2
|
||||||
|
val trackArc = trackHeight // Makes it fully rounded at the ends
|
||||||
|
|
||||||
|
g?.color = if (activated) activeSwitch else switchColor
|
||||||
|
g?.fillRoundRect(1, 1, trackWidth, trackHeight, trackArc, trackArc)
|
||||||
|
g?.color = borderColor
|
||||||
|
g?.drawRoundRect(1, 1, trackWidth, trackHeight, trackArc, trackArc)
|
||||||
|
|
||||||
|
// Draw the thumb (circular button)
|
||||||
|
val thumbDiameter = trackHeight - 4
|
||||||
|
val thumbX = if (activated) {
|
||||||
|
width - thumbDiameter - 3 // Right side when activated
|
||||||
|
} else {
|
||||||
|
3 // Left side when not activated
|
||||||
|
}
|
||||||
|
val thumbY = (height - thumbDiameter) / 2
|
||||||
|
|
||||||
|
g?.color = buttonColor
|
||||||
|
g?.fillOval(thumbX, thumbY, thumbDiameter, thumbDiameter)
|
||||||
|
g?.color = borderColor
|
||||||
|
g?.drawOval(thumbX, thumbY, thumbDiameter, thumbDiameter)
|
||||||
|
|
||||||
|
gr.drawImage(puffer, 0, 0, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isActivated(): Boolean {
|
||||||
|
return activated
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setActivated(activated: Boolean) {
|
||||||
|
this.activated = activated
|
||||||
|
repaint()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSwitchColor(): Color {
|
||||||
|
return switchColor
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unactivated Background Color of switch
|
||||||
|
*/
|
||||||
|
fun setSwitchColor(switchColor: Color) {
|
||||||
|
this.switchColor = switchColor
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getButtonColor(): Color {
|
||||||
|
return buttonColor
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch-Button color
|
||||||
|
*/
|
||||||
|
fun setButtonColor(buttonColor: Color) {
|
||||||
|
this.buttonColor = buttonColor
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBorderColor(): Color {
|
||||||
|
return borderColor
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Border-color of whole switch and switch-button
|
||||||
|
*/
|
||||||
|
fun setBorderColor(borderColor: Color) {
|
||||||
|
this.borderColor = borderColor
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getActiveSwitch(): Color {
|
||||||
|
return activeSwitch
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setActiveSwitch(activeSwitch: Color) {
|
||||||
|
this.activeSwitch = activeSwitch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package KondoKit.components
|
||||||
|
|
||||||
|
import KondoKit.plugin.Companion.TITLE_BAR_COLOR
|
||||||
|
import KondoKit.plugin.Companion.secondaryColor
|
||||||
|
import java.awt.*
|
||||||
|
import javax.swing.*
|
||||||
|
|
||||||
|
class ViewHeader(
|
||||||
|
title: String,
|
||||||
|
private val headerHeight: Int = 40
|
||||||
|
) : JPanel() {
|
||||||
|
|
||||||
|
private val titleLabel = JLabel(title)
|
||||||
|
|
||||||
|
init {
|
||||||
|
background = TITLE_BAR_COLOR
|
||||||
|
preferredSize = Dimension(Int.MAX_VALUE, headerHeight)
|
||||||
|
border = BorderFactory.createEmptyBorder(5, 10, 5, 10)
|
||||||
|
layout = BorderLayout()
|
||||||
|
|
||||||
|
titleLabel.foreground = secondaryColor
|
||||||
|
titleLabel.font = Font("RuneScape Small", Font.PLAIN, 16)
|
||||||
|
titleLabel.horizontalAlignment = SwingConstants.CENTER
|
||||||
|
|
||||||
|
add(titleLabel, BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTitle(title: String) {
|
||||||
|
titleLabel.text = title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package KondoKit.components
|
||||||
|
|
||||||
|
import KondoKit.plugin.Companion.WIDGET_COLOR
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.Dimension
|
||||||
|
import javax.swing.BorderFactory
|
||||||
|
import javax.swing.JPanel
|
||||||
|
|
||||||
|
class WidgetPanel(
|
||||||
|
private val widgetWidth: Int = 220,
|
||||||
|
private val widgetHeight: Int = 50,
|
||||||
|
private val addDefaultPadding: Boolean = true
|
||||||
|
) : JPanel() {
|
||||||
|
|
||||||
|
init {
|
||||||
|
layout = BorderLayout(5, 5)
|
||||||
|
background = WIDGET_COLOR
|
||||||
|
|
||||||
|
val size = Dimension(widgetWidth, widgetHeight)
|
||||||
|
preferredSize = size
|
||||||
|
maximumSize = size
|
||||||
|
minimumSize = size
|
||||||
|
|
||||||
|
if (addDefaultPadding) {
|
||||||
|
border = BorderFactory.createEmptyBorder(5, 5, 5, 5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFixedSize(width: Int, height: Int) {
|
||||||
|
val size = Dimension(width, height)
|
||||||
|
preferredSize = size
|
||||||
|
maximumSize = size
|
||||||
|
minimumSize = size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,35 +1,18 @@
|
||||||
package KondoKit
|
package KondoKit
|
||||||
|
|
||||||
import KondoKit.Constants.COMBAT_LVL_SPRITE
|
|
||||||
import KondoKit.Helpers.formatHtmlLabelText
|
|
||||||
import KondoKit.Helpers.formatNumber
|
|
||||||
import KondoKit.Helpers.getSpriteId
|
import KondoKit.Helpers.getSpriteId
|
||||||
import KondoKit.Helpers.showAlert
|
import KondoKit.Helpers.showAlert
|
||||||
import KondoKit.HiscoresView.createHiscoreSearchView
|
import KondoKit.views.*
|
||||||
import KondoKit.HiscoresView.hiScoreView
|
import KondoKit.views.OnUpdateCallback
|
||||||
import KondoKit.LootTrackerView.BAG_ICON
|
import KondoKit.views.OnDrawCallback
|
||||||
import KondoKit.LootTrackerView.createLootTrackerView
|
import KondoKit.views.OnXPUpdateCallback
|
||||||
import KondoKit.LootTrackerView.lootTrackerView
|
import KondoKit.views.OnKillingBlowNPCCallback
|
||||||
import KondoKit.LootTrackerView.npcDeathSnapshots
|
import KondoKit.views.OnPostClientTickCallback
|
||||||
import KondoKit.LootTrackerView.onPostClientTick
|
|
||||||
import KondoKit.LootTrackerView.takeGroundSnapshot
|
|
||||||
import KondoKit.ReflectiveEditorView.addPlugins
|
|
||||||
import KondoKit.ReflectiveEditorView.createReflectiveEditorView
|
|
||||||
import KondoKit.ReflectiveEditorView.reflectiveEditorView
|
|
||||||
import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite
|
import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite
|
||||||
import KondoKit.Themes.Theme
|
import KondoKit.Themes.Theme
|
||||||
import KondoKit.Themes.ThemeType
|
import KondoKit.Themes.ThemeType
|
||||||
import KondoKit.Themes.getTheme
|
import KondoKit.Themes.getTheme
|
||||||
import KondoKit.XPTrackerView.createXPTrackerView
|
import KondoKit.components.ScrollablePanel
|
||||||
import KondoKit.XPTrackerView.createXPWidget
|
|
||||||
import KondoKit.XPTrackerView.initialXP
|
|
||||||
import KondoKit.XPTrackerView.resetXPTracker
|
|
||||||
import KondoKit.XPTrackerView.totalXPWidget
|
|
||||||
import KondoKit.XPTrackerView.updateWidget
|
|
||||||
import KondoKit.XPTrackerView.wrappedWidget
|
|
||||||
import KondoKit.XPTrackerView.xpTrackerView
|
|
||||||
import KondoKit.XPTrackerView.xpWidgets
|
|
||||||
import KondoKit.plugin.StateManager.focusedView
|
|
||||||
import plugin.Plugin
|
import plugin.Plugin
|
||||||
import plugin.api.*
|
import plugin.api.*
|
||||||
import plugin.api.API.*
|
import plugin.api.API.*
|
||||||
|
|
@ -98,9 +81,9 @@ class plugin : Plugin() {
|
||||||
const val FIXED_HEIGHT = 503
|
const val FIXED_HEIGHT = 503
|
||||||
private const val NAVBAR_WIDTH = 30
|
private const val NAVBAR_WIDTH = 30
|
||||||
private const val MAIN_CONTENT_WIDTH = 242
|
private const val MAIN_CONTENT_WIDTH = 242
|
||||||
private const val WRENCH_ICON = 907
|
const val WRENCH_ICON = 907
|
||||||
private const val LOOT_ICON = 777
|
const val LOOT_ICON = 777
|
||||||
private const val MAG_SPRITE = 1423
|
const val MAG_SPRITE = 1423
|
||||||
const val LVL_ICON = 898
|
const val LVL_ICON = 898
|
||||||
private lateinit var cardLayout: CardLayout
|
private lateinit var cardLayout: CardLayout
|
||||||
private lateinit var mainContentPanel: JPanel
|
private lateinit var mainContentPanel: JPanel
|
||||||
|
|
@ -118,12 +101,38 @@ class plugin : Plugin() {
|
||||||
private const val HIDDEN_VIEW = "HIDDEN"
|
private const val HIDDEN_VIEW = "HIDDEN"
|
||||||
private var altCanvas: AltCanvas? = null
|
private var altCanvas: AltCanvas? = null
|
||||||
private val drawActions = mutableListOf<() -> Unit>()
|
private val drawActions = mutableListOf<() -> Unit>()
|
||||||
|
private val views = mutableListOf<View>()
|
||||||
|
private val updateCallbacks = mutableListOf<OnUpdateCallback>()
|
||||||
|
private val drawCallbacks = mutableListOf<OnDrawCallback>()
|
||||||
|
private val xpUpdateCallbacks = mutableListOf<OnXPUpdateCallback>()
|
||||||
|
private val killingBlowNPCCallbacks = mutableListOf<OnKillingBlowNPCCallback>()
|
||||||
|
private val postClientTickCallbacks = mutableListOf<OnPostClientTickCallback>()
|
||||||
|
|
||||||
fun registerDrawAction(action: () -> Unit) {
|
fun registerDrawAction(action: () -> Unit) {
|
||||||
synchronized(drawActions) {
|
synchronized(drawActions) {
|
||||||
drawActions.add(action)
|
drawActions.add(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun registerUpdateCallback(callback: OnUpdateCallback) {
|
||||||
|
updateCallbacks.add(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun registerDrawCallback(callback: OnDrawCallback) {
|
||||||
|
drawCallbacks.add(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun registerXPUpdateCallback(callback: OnXPUpdateCallback) {
|
||||||
|
xpUpdateCallbacks.add(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun registerKillingBlowNPCCallback(callback: OnKillingBlowNPCCallback) {
|
||||||
|
killingBlowNPCCallbacks.add(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun registerPostClientTickCallback(callback: OnPostClientTickCallback) {
|
||||||
|
postClientTickCallbacks.add(callback)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun Init() {
|
override fun Init() {
|
||||||
|
|
@ -137,7 +146,7 @@ class plugin : Plugin() {
|
||||||
if (lastLogin != "" && lastLogin != Player.usernameInput.toString()) {
|
if (lastLogin != "" && lastLogin != Player.usernameInput.toString()) {
|
||||||
// if we logged in with a new character
|
// if we logged in with a new character
|
||||||
// we need to reset the trackers
|
// we need to reset the trackers
|
||||||
xpTrackerView?.let { resetXPTracker(it) }
|
XPTrackerView.xpTrackerView?.let { XPTrackerView.resetXPTracker(it) }
|
||||||
}
|
}
|
||||||
lastLogin = Player.usernameInput.toString()
|
lastLogin = Player.usernameInput.toString()
|
||||||
}
|
}
|
||||||
|
|
@ -164,41 +173,28 @@ class plugin : Plugin() {
|
||||||
|
|
||||||
override fun OnPluginsReloaded(): Boolean {
|
override fun OnPluginsReloaded(): Boolean {
|
||||||
if (!initialized) return true
|
if (!initialized) return true
|
||||||
updateDisplaySettings()
|
// Ensure Swing updates happen on the EDT to avoid flicker
|
||||||
frame.remove(rightPanelWrapper)
|
SwingUtilities.invokeLater {
|
||||||
frame.layout = BorderLayout()
|
updateDisplaySettings()
|
||||||
rightPanelWrapper?.let { frame.add(it, BorderLayout.EAST) }
|
frame.remove(rightPanelWrapper)
|
||||||
frame.revalidate()
|
frame.layout = BorderLayout()
|
||||||
|
rightPanelWrapper?.let { frame.add(it, BorderLayout.EAST) }
|
||||||
|
frame.revalidate()
|
||||||
|
frame.repaint()
|
||||||
|
|
||||||
|
// Rebuild the reflective editor UI on the EDT and in one batch
|
||||||
|
ReflectiveEditorView.addPlugins(ReflectiveEditorView.panel)
|
||||||
|
}
|
||||||
pluginsReloaded = true
|
pluginsReloaded = true
|
||||||
reloadInterfaces = true
|
reloadInterfaces = true
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun OnXPUpdate(skillId: Int, xp: Int) {
|
override fun OnXPUpdate(skillId: Int, xp: Int) {
|
||||||
if (!initialXP.containsKey(skillId)) {
|
// Call registered XP update callbacks
|
||||||
initialXP[skillId] = xp
|
xpUpdateCallbacks.forEach { callback ->
|
||||||
return
|
callback.onXPUpdate(skillId, xp)
|
||||||
}
|
}
|
||||||
var xpWidget = xpWidgets[skillId]
|
|
||||||
if (xpWidget != null) {
|
|
||||||
updateWidget(xpWidget, xp)
|
|
||||||
} else {
|
|
||||||
val previousXp = initialXP[skillId] ?: xp
|
|
||||||
if (xp == initialXP[skillId]) return
|
|
||||||
|
|
||||||
xpWidget = createXPWidget(skillId, previousXp)
|
|
||||||
xpWidgets[skillId] = xpWidget
|
|
||||||
|
|
||||||
xpTrackerView?.add(wrappedWidget(xpWidget.container))
|
|
||||||
xpTrackerView?.add(Box.createVerticalStrut(5))
|
|
||||||
|
|
||||||
if(focusedView == XPTrackerView.VIEW_NAME) {
|
|
||||||
xpTrackerView?.revalidate()
|
|
||||||
xpTrackerView?.repaint()
|
|
||||||
}
|
|
||||||
|
|
||||||
updateWidget(xpWidget, xp)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun Draw(timeDelta: Long) {
|
override fun Draw(timeDelta: Long) {
|
||||||
|
|
@ -208,7 +204,10 @@ class plugin : Plugin() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pluginsReloaded) {
|
if (pluginsReloaded) {
|
||||||
reflectiveEditorView?.let { addPlugins(it) }
|
// Rebuild the reflective editor UI on the EDT and in one batch
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
ReflectiveEditorView.addPlugins(ReflectiveEditorView.panel)
|
||||||
|
}
|
||||||
pluginsReloaded = false
|
pluginsReloaded = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -219,10 +218,18 @@ class plugin : Plugin() {
|
||||||
|
|
||||||
accumulatedTime += timeDelta
|
accumulatedTime += timeDelta
|
||||||
if (accumulatedTime >= TICK_INTERVAL) {
|
if (accumulatedTime >= TICK_INTERVAL) {
|
||||||
lootTrackerView?.let { onPostClientTick(it) }
|
// Call registered post client tick callbacks
|
||||||
|
postClientTickCallbacks.forEach { callback ->
|
||||||
|
callback.onPostClientTick()
|
||||||
|
}
|
||||||
accumulatedTime = 0L
|
accumulatedTime = 0L
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Call registered draw callbacks
|
||||||
|
drawCallbacks.forEach { callback ->
|
||||||
|
callback.onDraw(timeDelta)
|
||||||
|
}
|
||||||
|
|
||||||
// Draw synced actions (that require to be done between glBegin and glEnd)
|
// Draw synced actions (that require to be done between glBegin and glEnd)
|
||||||
if (drawActions.isNotEmpty()) {
|
if (drawActions.isNotEmpty()) {
|
||||||
synchronized(drawActions) {
|
synchronized(drawActions) {
|
||||||
|
|
@ -260,32 +267,17 @@ class plugin : Plugin() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun Update() {
|
override fun Update() {
|
||||||
|
// Call registered update callbacks
|
||||||
val widgets = xpWidgets.values
|
updateCallbacks.forEach { callback ->
|
||||||
val totalXP = totalXPWidget
|
callback.onUpdate()
|
||||||
|
|
||||||
widgets.forEach { xpWidget ->
|
|
||||||
val elapsedTime = (System.currentTimeMillis() - xpWidget.startTime) / 1000.0 / 60.0 / 60.0
|
|
||||||
val xpPerHour = if (elapsedTime > 0) (xpWidget.totalXpGained / elapsedTime).toInt() else 0
|
|
||||||
val formattedXpPerHour = formatNumber(xpPerHour)
|
|
||||||
xpWidget.xpPerHourLabel.text =
|
|
||||||
formatHtmlLabelText("XP /hr: ", primaryColor, formattedXpPerHour, secondaryColor)
|
|
||||||
xpWidget.container.repaint()
|
|
||||||
}
|
|
||||||
|
|
||||||
totalXP?.let { totalXPWidget ->
|
|
||||||
val elapsedTime = (System.currentTimeMillis() - totalXPWidget.startTime) / 1000.0 / 60.0 / 60.0
|
|
||||||
val totalXPPerHour = if (elapsedTime > 0) (totalXPWidget.totalXpGained / elapsedTime).toInt() else 0
|
|
||||||
val formattedTotalXpPerHour = formatNumber(totalXPPerHour)
|
|
||||||
totalXPWidget.xpPerHourLabel.text =
|
|
||||||
formatHtmlLabelText("XP /hr: ", primaryColor, formattedTotalXpPerHour, secondaryColor)
|
|
||||||
totalXPWidget.container.repaint()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun OnKillingBlowNPC(npcID: Int, x: Int, z: Int) {
|
override fun OnKillingBlowNPC(npcID: Int, x: Int, z: Int) {
|
||||||
val preDeathSnapshot = takeGroundSnapshot(Pair(x,z))
|
// Call registered killing blow NPC callbacks
|
||||||
npcDeathSnapshots[npcID] = LootTrackerView.GroundSnapshot(preDeathSnapshot, Pair(x, z), 0)
|
killingBlowNPCCallbacks.forEach { callback ->
|
||||||
|
callback.onKillingBlowNPC(npcID, x, z)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun allSpritesLoaded() : Boolean {
|
private fun allSpritesLoaded() : Boolean {
|
||||||
|
|
@ -296,7 +288,7 @@ class plugin : Plugin() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val otherIcons = arrayOf(LVL_ICON, MAG_SPRITE, LOOT_ICON, WRENCH_ICON, COMBAT_LVL_SPRITE, BAG_ICON)
|
val otherIcons = arrayOf(LVL_ICON, MAG_SPRITE, LOOT_ICON, WRENCH_ICON, Constants.COMBAT_LVL_SPRITE, LootTrackerView.BAG_ICON)
|
||||||
for (icon in otherIcons) {
|
for (icon in otherIcons) {
|
||||||
if(!js5Archive8.isFileReady(icon)){
|
if(!js5Archive8.isFileReady(icon)){
|
||||||
return false
|
return false
|
||||||
|
|
@ -317,6 +309,11 @@ class plugin : Plugin() {
|
||||||
destroyAltCanvas()
|
destroyAltCanvas()
|
||||||
} else if (useScaledFixed && altCanvas == null) {
|
} else if (useScaledFixed && altCanvas == null) {
|
||||||
initAltCanvas()
|
initAltCanvas()
|
||||||
|
} else if (!useScaledFixed && altCanvas != null) {
|
||||||
|
// Was using scaled fixed but toggled the setting
|
||||||
|
// restore the original canvas
|
||||||
|
moveCanvasToFront()
|
||||||
|
destroyAltCanvas()
|
||||||
}
|
}
|
||||||
|
|
||||||
when (mode) {
|
when (mode) {
|
||||||
|
|
@ -409,7 +406,7 @@ class plugin : Plugin() {
|
||||||
private fun searchHiscore(username: String): Runnable {
|
private fun searchHiscore(username: String): Runnable {
|
||||||
return Runnable {
|
return Runnable {
|
||||||
setActiveView(HiscoresView.VIEW_NAME)
|
setActiveView(HiscoresView.VIEW_NAME)
|
||||||
val customSearchField = hiScoreView?.let { HiscoresView.CustomSearchField(it) }
|
val customSearchField = HiscoresView.hiScoreView?.let { HiscoresView.CustomSearchField(it) }
|
||||||
|
|
||||||
customSearchField?.searchPlayer(username) ?: run {
|
customSearchField?.searchPlayer(username) ?: run {
|
||||||
println("searchView is null or CustomSearchField creation failed.")
|
println("searchView is null or CustomSearchField creation failed.")
|
||||||
|
|
@ -447,15 +444,33 @@ class plugin : Plugin() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register Views
|
// Register Views
|
||||||
createXPTrackerView()
|
val xpTrackerView = XPTrackerView
|
||||||
createHiscoreSearchView()
|
val hiscoresView = HiscoresView
|
||||||
createLootTrackerView()
|
val lootTrackerView = LootTrackerView
|
||||||
createReflectiveEditorView()
|
val reflectiveEditorView = ReflectiveEditorView
|
||||||
|
|
||||||
mainContentPanel.add(ScrollablePanel(xpTrackerView!!), XPTrackerView.VIEW_NAME)
|
// Create views
|
||||||
mainContentPanel.add(ScrollablePanel(hiScoreView!!), HiscoresView.VIEW_NAME)
|
xpTrackerView.createView()
|
||||||
mainContentPanel.add(ScrollablePanel(lootTrackerView!!), LootTrackerView.VIEW_NAME)
|
hiscoresView.createView()
|
||||||
mainContentPanel.add(ScrollablePanel(reflectiveEditorView!!), ReflectiveEditorView.VIEW_NAME)
|
lootTrackerView.createView()
|
||||||
|
reflectiveEditorView.createView()
|
||||||
|
|
||||||
|
// Register views
|
||||||
|
views.add(xpTrackerView)
|
||||||
|
views.add(hiscoresView)
|
||||||
|
views.add(lootTrackerView)
|
||||||
|
views.add(reflectiveEditorView)
|
||||||
|
|
||||||
|
// Register view functions
|
||||||
|
xpTrackerView.registerFunctions()
|
||||||
|
hiscoresView.registerFunctions()
|
||||||
|
lootTrackerView.registerFunctions()
|
||||||
|
reflectiveEditorView.registerFunctions()
|
||||||
|
|
||||||
|
mainContentPanel.add(ScrollablePanel(xpTrackerView.panel), xpTrackerView.name)
|
||||||
|
mainContentPanel.add(ScrollablePanel(hiscoresView.panel), hiscoresView.name)
|
||||||
|
mainContentPanel.add(ScrollablePanel(lootTrackerView.panel), lootTrackerView.name)
|
||||||
|
mainContentPanel.add(ScrollablePanel(reflectiveEditorView.panel), reflectiveEditorView.name)
|
||||||
|
|
||||||
val navPanel = Panel().apply {
|
val navPanel = Panel().apply {
|
||||||
layout = BoxLayout(this, BoxLayout.Y_AXIS)
|
layout = BoxLayout(this, BoxLayout.Y_AXIS)
|
||||||
|
|
@ -463,10 +478,10 @@ class plugin : Plugin() {
|
||||||
preferredSize = Dimension(NAVBAR_WIDTH, frame.height)
|
preferredSize = Dimension(NAVBAR_WIDTH, frame.height)
|
||||||
}
|
}
|
||||||
|
|
||||||
navPanel.add(createNavButton(LVL_ICON, XPTrackerView.VIEW_NAME))
|
navPanel.add(createNavButton(xpTrackerView.iconSpriteId, xpTrackerView.name))
|
||||||
navPanel.add(createNavButton(MAG_SPRITE, HiscoresView.VIEW_NAME))
|
navPanel.add(createNavButton(hiscoresView.iconSpriteId, hiscoresView.name))
|
||||||
navPanel.add(createNavButton(LOOT_ICON, LootTrackerView.VIEW_NAME))
|
navPanel.add(createNavButton(lootTrackerView.iconSpriteId, lootTrackerView.name))
|
||||||
navPanel.add(createNavButton(WRENCH_ICON, ReflectiveEditorView.VIEW_NAME))
|
navPanel.add(createNavButton(reflectiveEditorView.iconSpriteId, reflectiveEditorView.name))
|
||||||
|
|
||||||
val rightPanel = Panel(BorderLayout()).apply {
|
val rightPanel = Panel(BorderLayout()).apply {
|
||||||
add(mainContentPanel, BorderLayout.CENTER)
|
add(mainContentPanel, BorderLayout.CENTER)
|
||||||
|
|
@ -481,45 +496,80 @@ class plugin : Plugin() {
|
||||||
verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_NEVER
|
verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_NEVER
|
||||||
}
|
}
|
||||||
|
|
||||||
frame.layout = BorderLayout()
|
val desiredView = if (launchMinimized) HIDDEN_VIEW else xpTrackerView.name
|
||||||
rightPanelWrapper?.let {
|
// Commit layout synchronously on the EDT to avoid initial misplacement
|
||||||
frame.add(it, BorderLayout.EAST)
|
val commit = Runnable {
|
||||||
|
frame.layout = BorderLayout()
|
||||||
|
rightPanelWrapper?.let { frame.add(it, BorderLayout.EAST) }
|
||||||
|
setActiveView(desiredView)
|
||||||
|
frame.validate()
|
||||||
|
frame.repaint()
|
||||||
}
|
}
|
||||||
|
if (SwingUtilities.isEventDispatchThread()) {
|
||||||
if(launchMinimized){
|
commit.run()
|
||||||
setActiveView(HIDDEN_VIEW)
|
|
||||||
} else {
|
} else {
|
||||||
setActiveView(XPTrackerView.VIEW_NAME)
|
try {
|
||||||
|
SwingUtilities.invokeAndWait(commit)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Fallback to async if invokeAndWait fails for any reason
|
||||||
|
SwingUtilities.invokeLater(commit)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
initialized = true
|
initialized = true
|
||||||
pluginsReloaded = true
|
pluginsReloaded = true
|
||||||
|
updateDisplaySettings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setActiveView(viewName: String) {
|
private fun setActiveView(viewName: String) {
|
||||||
// Handle the visibility of the main content panel
|
val runUpdate: () -> Unit = {
|
||||||
if (viewName == HIDDEN_VIEW) {
|
// Track visibility change to decide if we need to resize/reload interfaces
|
||||||
mainContentPanel.isVisible = false
|
val wasVisible = mainContentPanel.isVisible
|
||||||
} else {
|
|
||||||
if (!mainContentPanel.isVisible) {
|
// Handle the visibility of the main content panel and card switch
|
||||||
mainContentPanel.isVisible = true
|
if (viewName == HIDDEN_VIEW) {
|
||||||
|
mainContentPanel.isVisible = false
|
||||||
|
} else {
|
||||||
|
if (!mainContentPanel.isVisible) {
|
||||||
|
mainContentPanel.isVisible = true
|
||||||
|
}
|
||||||
|
cardLayout.show(mainContentPanel, viewName)
|
||||||
}
|
}
|
||||||
cardLayout.show(mainContentPanel, viewName)
|
|
||||||
|
val visibilityChanged = wasVisible != mainContentPanel.isVisible
|
||||||
|
|
||||||
|
// Batch painting to avoid intermediate repaints
|
||||||
|
rightPanelWrapper?.ignoreRepaint = true
|
||||||
|
try {
|
||||||
|
if (visibilityChanged) {
|
||||||
|
// Only touch layout and client interfaces if width actually changes
|
||||||
|
updateDisplaySettings()
|
||||||
|
reloadInterfaces = true
|
||||||
|
rightPanelWrapper?.revalidate()
|
||||||
|
frame?.validate()
|
||||||
|
} else {
|
||||||
|
// Just a card switch; avoid full frame revalidate
|
||||||
|
mainContentPanel.revalidate()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
rightPanelWrapper?.ignoreRepaint = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Targeted repaint for snappy feedback
|
||||||
|
if (visibilityChanged) {
|
||||||
|
rightPanelWrapper?.repaint()
|
||||||
|
frame?.repaint()
|
||||||
|
} else {
|
||||||
|
mainContentPanel.repaint()
|
||||||
|
}
|
||||||
|
StateManager.focusedView = viewName
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadInterfaces = true
|
if (SwingUtilities.isEventDispatchThread()) {
|
||||||
updateDisplaySettings()
|
runUpdate()
|
||||||
|
} else {
|
||||||
// Revalidate and repaint necessary panels
|
SwingUtilities.invokeLater { runUpdate() }
|
||||||
mainContentPanel.revalidate()
|
}
|
||||||
rightPanelWrapper?.revalidate()
|
|
||||||
frame?.revalidate()
|
|
||||||
|
|
||||||
mainContentPanel.repaint()
|
|
||||||
rightPanelWrapper?.repaint()
|
|
||||||
frame?.repaint()
|
|
||||||
|
|
||||||
focusedView = viewName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createNavButton(spriteId: Int, viewName: String): JPanel {
|
private fun createNavButton(spriteId: Int, viewName: String): JPanel {
|
||||||
|
|
@ -535,7 +585,7 @@ class plugin : Plugin() {
|
||||||
}
|
}
|
||||||
lastClickTime = currentTime
|
lastClickTime = currentTime
|
||||||
|
|
||||||
if (focusedView == viewName) {
|
if (StateManager.focusedView == viewName) {
|
||||||
setActiveView("HIDDEN")
|
setActiveView("HIDDEN")
|
||||||
} else {
|
} else {
|
||||||
setActiveView(viewName)
|
setActiveView(viewName)
|
||||||
|
|
@ -666,7 +716,7 @@ class plugin : Plugin() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadFont(): Font? {
|
private fun loadFont(): Font? {
|
||||||
val fontStream = plugin::class.java.getResourceAsStream("res/runescape_small.ttf")
|
val fontStream = Helpers.openResource("res/runescape_small.ttf")
|
||||||
return if (fontStream != null) {
|
return if (fontStream != null) {
|
||||||
try {
|
try {
|
||||||
val font = Font.createFont(Font.TRUETYPE_FONT, fontStream)
|
val font = Font.createFont(Font.TRUETYPE_FONT, fontStream)
|
||||||
|
|
|
||||||
|
|
@ -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.'
|
||||||
VERSION=2.0
|
VERSION=2.1
|
||||||
BIN
plugin-playground/src/main/kotlin/KondoKit/res/cog.png
Normal file
BIN
plugin-playground/src/main/kotlin/KondoKit/res/cog.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 893 B |
33
plugin-playground/src/main/kotlin/KondoKit/views/BaseView.kt
Normal file
33
plugin-playground/src/main/kotlin/KondoKit/views/BaseView.kt
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
package KondoKit.views
|
||||||
|
|
||||||
|
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
|
||||||
|
import java.awt.Dimension
|
||||||
|
import javax.swing.BorderFactory
|
||||||
|
import javax.swing.Box
|
||||||
|
import javax.swing.BoxLayout
|
||||||
|
import javax.swing.JPanel
|
||||||
|
|
||||||
|
open class BaseView(
|
||||||
|
private val viewName: String,
|
||||||
|
private val preferredWidth: Int = 242,
|
||||||
|
private val addDefaultSpacing: Boolean = true
|
||||||
|
) : JPanel() {
|
||||||
|
|
||||||
|
init {
|
||||||
|
layout = BoxLayout(this, BoxLayout.Y_AXIS)
|
||||||
|
name = viewName
|
||||||
|
background = VIEW_BACKGROUND_COLOR
|
||||||
|
border = BorderFactory.createEmptyBorder(0, 0, 0, 0)
|
||||||
|
|
||||||
|
if (addDefaultSpacing) {
|
||||||
|
add(Box.createVerticalStrut(5))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setViewSize(height: Int) {
|
||||||
|
val dimension = Dimension(preferredWidth, height)
|
||||||
|
preferredSize = dimension
|
||||||
|
maximumSize = dimension
|
||||||
|
minimumSize = dimension
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,28 +1,21 @@
|
||||||
package KondoKit
|
package KondoKit.views
|
||||||
|
|
||||||
import KondoKit.Constants.COLOR_BACKGROUND_DARK
|
|
||||||
import KondoKit.Constants.SKILL_DISPLAY_ORDER
|
|
||||||
import KondoKit.Constants.SKILL_SPRITE_DIMENSION
|
|
||||||
import KondoKit.Helpers.formatHtmlLabelText
|
|
||||||
import KondoKit.Helpers.getSpriteId
|
import KondoKit.Helpers.getSpriteId
|
||||||
import KondoKit.Helpers.showToast
|
import KondoKit.Helpers.showToast
|
||||||
|
import KondoKit.ImageCanvas
|
||||||
import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite
|
import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite
|
||||||
import KondoKit.plugin.Companion.POPUP_FOREGROUND
|
import KondoKit.components.LabelComponent
|
||||||
import KondoKit.plugin.Companion.TOOLTIP_BACKGROUND
|
import KondoKit.components.SearchField
|
||||||
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
|
|
||||||
import KondoKit.plugin.Companion.WIDGET_COLOR
|
import KondoKit.plugin.Companion.WIDGET_COLOR
|
||||||
import KondoKit.plugin.Companion.primaryColor
|
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
|
||||||
|
import KondoKit.plugin.Companion.POPUP_FOREGROUND
|
||||||
import KondoKit.plugin.Companion.secondaryColor
|
import KondoKit.plugin.Companion.secondaryColor
|
||||||
import KondoKit.plugin.StateManager.focusedView
|
import KondoKit.plugin.Companion.primaryColor
|
||||||
|
import KondoKit.plugin.Companion.TOOLTIP_BACKGROUND
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import plugin.api.API
|
import plugin.api.API
|
||||||
import rt4.Sprites
|
import rt4.Sprites
|
||||||
import java.awt.*
|
import java.awt.*
|
||||||
import java.awt.datatransfer.DataFlavor
|
|
||||||
import java.awt.event.KeyAdapter
|
|
||||||
import java.awt.event.KeyEvent
|
|
||||||
import java.awt.event.MouseAdapter
|
|
||||||
import java.awt.event.MouseEvent
|
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
|
|
@ -64,125 +57,41 @@ object Constants {
|
||||||
val SKILL_DISPLAY_ORDER = arrayOf(0,3,14,2,16,13,1,15,10,4,17,7,5,12,11,6,9,8,20,18,19,22,21,23)
|
val SKILL_DISPLAY_ORDER = arrayOf(0,3,14,2,16,13,1,15,10,4,17,7,5,12,11,6,9,8,20,18,19,22,21,23)
|
||||||
}
|
}
|
||||||
|
|
||||||
var text: String = ""
|
object HiscoresView : View {
|
||||||
|
|
||||||
object HiscoresView {
|
|
||||||
|
|
||||||
const val VIEW_NAME = "HISCORE_SEARCH_VIEW"
|
const val VIEW_NAME = "HISCORE_SEARCH_VIEW"
|
||||||
var hiScoreView: JPanel? = null
|
var hiScoreView: JPanel? = null
|
||||||
class CustomSearchField(private val hiscoresPanel: JPanel) : Canvas() {
|
override val name: String = VIEW_NAME
|
||||||
|
override val iconSpriteId: Int = Constants.MAG_SPRITE
|
||||||
|
|
||||||
private var cursorVisible: Boolean = true
|
override val panel: JPanel
|
||||||
|
get() = hiScoreView ?: JPanel()
|
||||||
|
|
||||||
|
override fun createView() {
|
||||||
|
createHiscoreSearchView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun registerFunctions() {
|
||||||
|
// Hiscores functions are handled within the view itself
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomSearchField(private val hiscoresPanel: JPanel) : SearchField(
|
||||||
|
onSearch = { _ -> }, // Placeholder, will be replaced
|
||||||
|
viewName = VIEW_NAME
|
||||||
|
) {
|
||||||
private val gson = Gson()
|
private val gson = Gson()
|
||||||
|
|
||||||
private val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(Constants.MAG_SPRITE))
|
|
||||||
private val imageCanvas = bufferedImageSprite.let {
|
|
||||||
ImageCanvas(it).apply {
|
|
||||||
preferredSize = Constants.ICON_DIMENSION_SMALL
|
|
||||||
size = preferredSize
|
|
||||||
minimumSize = preferredSize
|
|
||||||
maximumSize = preferredSize
|
|
||||||
fillColor = COLOR_BACKGROUND_DARK
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
preferredSize = Constants.SEARCH_FIELD_DIMENSION
|
// This is a workaround to set the onSearch callback after the class is fully initialized
|
||||||
background = Constants.COLOR_BACKGROUND_DARK
|
val onSearchField = javaClass.superclass.getDeclaredField("onSearch")
|
||||||
foreground = Constants.COLOR_FOREGROUND_LIGHT
|
onSearchField.isAccessible = true
|
||||||
font = Constants.FONT_ARIAL_PLAIN_14
|
onSearchField.set(this, { username: String -> searchPlayer(username) })
|
||||||
minimumSize = preferredSize
|
|
||||||
maximumSize = preferredSize
|
|
||||||
|
|
||||||
addKeyListener(object : KeyAdapter() {
|
|
||||||
override fun keyTyped(e: KeyEvent) {
|
|
||||||
// Prevent null character from being typed on Ctrl+A & Ctrl+V
|
|
||||||
if (e.isControlDown && (e.keyChar == '\u0001' || e.keyChar == '\u0016')) {
|
|
||||||
e.consume()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (e.keyChar == '\b') {
|
|
||||||
if (text.isNotEmpty()) {
|
|
||||||
text = text.dropLast(1)
|
|
||||||
}
|
|
||||||
} else if (e.keyChar == '\n') {
|
|
||||||
searchPlayer(text)
|
|
||||||
} else {
|
|
||||||
text += e.keyChar
|
|
||||||
}
|
|
||||||
SwingUtilities.invokeLater {
|
|
||||||
repaint()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override fun keyPressed(e: KeyEvent) {
|
|
||||||
if (e.isControlDown) {
|
|
||||||
when (e.keyCode) {
|
|
||||||
KeyEvent.VK_A -> {
|
|
||||||
text = ""
|
|
||||||
repaint()
|
|
||||||
}
|
|
||||||
KeyEvent.VK_V -> {
|
|
||||||
val clipboard = Toolkit.getDefaultToolkit().systemClipboard
|
|
||||||
val pasteText = clipboard.getData(DataFlavor.stringFlavor) as String
|
|
||||||
text += pasteText
|
|
||||||
SwingUtilities.invokeLater {
|
|
||||||
repaint()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
addMouseListener(object : MouseAdapter() {
|
|
||||||
override fun mouseClicked(e: MouseEvent) {
|
|
||||||
if (e.x > width - 20 && e.y < 20) {
|
|
||||||
text = ""
|
|
||||||
SwingUtilities.invokeLater {
|
|
||||||
repaint()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
Timer(500) {
|
|
||||||
cursorVisible = !cursorVisible
|
|
||||||
if(focusedView == VIEW_NAME)
|
|
||||||
SwingUtilities.invokeLater {
|
|
||||||
repaint()
|
|
||||||
}
|
|
||||||
}.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun paint(g: Graphics) {
|
|
||||||
super.paint(g)
|
|
||||||
g.color = foreground
|
|
||||||
g.font = font
|
|
||||||
|
|
||||||
val fm = g.fontMetrics
|
|
||||||
val cursorX = fm.stringWidth(text) + 30
|
|
||||||
|
|
||||||
imageCanvas.let { canvas ->
|
|
||||||
val imgG = g.create(5, 5, canvas.width, canvas.height)
|
|
||||||
canvas.paint(imgG)
|
|
||||||
imgG.dispose()
|
|
||||||
}
|
|
||||||
|
|
||||||
g.drawString(text, 30, 20)
|
|
||||||
|
|
||||||
if (cursorVisible && hasFocus()) {
|
|
||||||
g.drawLine(cursorX, 5, cursorX, 25)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (text.isNotEmpty()) {
|
|
||||||
g.color = Color.RED
|
|
||||||
g.drawString("x", width - 20, 20)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun searchPlayer(username: String) {
|
fun searchPlayer(username: String) {
|
||||||
text = username.replace(" ", "_")
|
val cleanUsername = username.replace(" ", "_")
|
||||||
val apiUrl = "http://api.2009scape.org:3000/hiscores/playerSkills/1/${text.toLowerCase()}"
|
setText(cleanUsername)
|
||||||
|
val apiUrl = "http://api.2009scape.org:3000/hiscores/playerSkills/1/${cleanUsername.toLowerCase()}"
|
||||||
|
|
||||||
updateHiscoresView(null, "Searching...")
|
updateHiscoresView(null, "Searching...")
|
||||||
|
|
||||||
|
|
@ -223,7 +132,6 @@ object HiscoresView {
|
||||||
}.start()
|
}.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun updatePlayerData(jsonResponse: String, username: String) {
|
private fun updatePlayerData(jsonResponse: String, username: String) {
|
||||||
val hiscoresResponse = gson.fromJson(jsonResponse, HiscoresResponse::class.java)
|
val hiscoresResponse = gson.fromJson(jsonResponse, HiscoresResponse::class.java)
|
||||||
updateHiscoresView(hiscoresResponse, username)
|
updateHiscoresView(hiscoresResponse, username)
|
||||||
|
|
@ -232,10 +140,12 @@ object HiscoresView {
|
||||||
private fun updateHiscoresView(data: HiscoresResponse?, username: String) {
|
private fun updateHiscoresView(data: HiscoresResponse?, username: String) {
|
||||||
val playerNameLabel = findComponentByName(hiscoresPanel, "playerNameLabel") as? JPanel
|
val playerNameLabel = findComponentByName(hiscoresPanel, "playerNameLabel") as? JPanel
|
||||||
playerNameLabel?.removeAll() // Clear previous components
|
playerNameLabel?.removeAll() // Clear previous components
|
||||||
var nameLabel = JLabel(formatHtmlLabelText(username, secondaryColor, "", primaryColor), JLabel.CENTER).apply {
|
var nameLabel = LabelComponent().apply {
|
||||||
|
updateHtmlText(username, secondaryColor, "", primaryColor)
|
||||||
font = Constants.FONT_ARIAL_BOLD_12
|
font = Constants.FONT_ARIAL_BOLD_12
|
||||||
foreground = Constants.COLOR_FOREGROUND_LIGHT
|
foreground = Constants.COLOR_FOREGROUND_LIGHT
|
||||||
border = BorderFactory.createEmptyBorder(0, 6, 0, 0) // Top, Left, Bottom, Right padding
|
border = BorderFactory.createEmptyBorder(0, 6, 0, 0) // Top, Left, Bottom, Right padding
|
||||||
|
horizontalAlignment = JLabel.CENTER
|
||||||
}
|
}
|
||||||
playerNameLabel?.add(nameLabel)
|
playerNameLabel?.add(nameLabel)
|
||||||
playerNameLabel?.revalidate()
|
playerNameLabel?.revalidate()
|
||||||
|
|
@ -260,10 +170,12 @@ object HiscoresView {
|
||||||
}
|
}
|
||||||
|
|
||||||
val exp_multiplier = data.info.exp_multiplier
|
val exp_multiplier = data.info.exp_multiplier
|
||||||
nameLabel = JLabel(formatHtmlLabelText(username, secondaryColor, " (${exp_multiplier}x)", primaryColor), JLabel.CENTER).apply {
|
nameLabel = LabelComponent().apply {
|
||||||
|
updateHtmlText(username, secondaryColor, " (${exp_multiplier}x)", primaryColor)
|
||||||
font = Constants.FONT_ARIAL_BOLD_12
|
font = Constants.FONT_ARIAL_BOLD_12
|
||||||
foreground = Constants.COLOR_FOREGROUND_LIGHT
|
foreground = Constants.COLOR_FOREGROUND_LIGHT
|
||||||
border = BorderFactory.createEmptyBorder(0, 6, 0, 0) // Top, Left, Bottom, Right padding
|
border = BorderFactory.createEmptyBorder(0, 6, 0, 0) // Top, Left, Bottom, Right padding
|
||||||
|
horizontalAlignment = JLabel.CENTER
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -342,13 +254,9 @@ object HiscoresView {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createHiscoreSearchView() {
|
fun createHiscoreSearchView() {
|
||||||
val hiscorePanel = JPanel().apply {
|
val hiscorePanel = BaseView(VIEW_NAME, Constants.HISCORE_PANEL_DIMENSION.width).apply {
|
||||||
layout = BoxLayout(this, BoxLayout.Y_AXIS)
|
|
||||||
name = VIEW_NAME
|
|
||||||
background = Constants.COLOR_BACKGROUND_MEDIUM
|
background = Constants.COLOR_BACKGROUND_MEDIUM
|
||||||
preferredSize = Constants.HISCORE_PANEL_DIMENSION
|
setViewSize(Constants.HISCORE_PANEL_DIMENSION.height)
|
||||||
maximumSize = preferredSize
|
|
||||||
minimumSize = preferredSize
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val customSearchField = CustomSearchField(hiscorePanel)
|
val customSearchField = CustomSearchField(hiscorePanel)
|
||||||
|
|
@ -392,23 +300,23 @@ object HiscoresView {
|
||||||
minimumSize = preferredSize
|
minimumSize = preferredSize
|
||||||
}
|
}
|
||||||
|
|
||||||
for (i in SKILL_DISPLAY_ORDER) {
|
for (i in Constants.SKILL_DISPLAY_ORDER) {
|
||||||
val skillPanel = JPanel().apply {
|
val skillPanel = JPanel().apply {
|
||||||
layout = BorderLayout()
|
layout = BorderLayout()
|
||||||
background = COLOR_BACKGROUND_DARK
|
background = Constants.COLOR_BACKGROUND_DARK
|
||||||
preferredSize = Constants.SKILL_PANEL_DIMENSION
|
preferredSize = Constants.SKILL_PANEL_DIMENSION
|
||||||
maximumSize = preferredSize
|
maximumSize = preferredSize
|
||||||
minimumSize = preferredSize
|
minimumSize = preferredSize
|
||||||
border = MatteBorder(5, 0, 0, 0, COLOR_BACKGROUND_DARK)
|
border = MatteBorder(5, 0, 0, 0, Constants.COLOR_BACKGROUND_DARK)
|
||||||
}
|
}
|
||||||
|
|
||||||
val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(getSpriteId(i)))
|
val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(getSpriteId(i)))
|
||||||
|
|
||||||
val imageCanvas = bufferedImageSprite.let {
|
val imageCanvas = bufferedImageSprite.let {
|
||||||
ImageCanvas(it).apply {
|
ImageCanvas(it).apply {
|
||||||
preferredSize = SKILL_SPRITE_DIMENSION
|
preferredSize = Constants.SKILL_SPRITE_DIMENSION
|
||||||
size = SKILL_SPRITE_DIMENSION
|
size = Constants.SKILL_SPRITE_DIMENSION
|
||||||
fillColor = COLOR_BACKGROUND_DARK
|
fillColor = Constants.COLOR_BACKGROUND_DARK
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -421,7 +329,7 @@ object HiscoresView {
|
||||||
}
|
}
|
||||||
|
|
||||||
val imageContainer = JPanel(FlowLayout(FlowLayout.CENTER, 5, 0)).apply {
|
val imageContainer = JPanel(FlowLayout(FlowLayout.CENTER, 5, 0)).apply {
|
||||||
background = COLOR_BACKGROUND_DARK
|
background = Constants.COLOR_BACKGROUND_DARK
|
||||||
add(imageCanvas)
|
add(imageCanvas)
|
||||||
add(numberLabel)
|
add(numberLabel)
|
||||||
}
|
}
|
||||||
|
|
@ -433,7 +341,7 @@ object HiscoresView {
|
||||||
hiscorePanel.add(skillsPanel)
|
hiscorePanel.add(skillsPanel)
|
||||||
|
|
||||||
val totalCombatPanel = JPanel(FlowLayout(FlowLayout.CENTER, 0, 0)).apply {
|
val totalCombatPanel = JPanel(FlowLayout(FlowLayout.CENTER, 0, 0)).apply {
|
||||||
background = COLOR_BACKGROUND_DARK
|
background = Constants.COLOR_BACKGROUND_DARK
|
||||||
preferredSize = Constants.TOTAL_COMBAT_PANEL_DIMENSION
|
preferredSize = Constants.TOTAL_COMBAT_PANEL_DIMENSION
|
||||||
maximumSize = preferredSize
|
maximumSize = preferredSize
|
||||||
minimumSize = preferredSize
|
minimumSize = preferredSize
|
||||||
|
|
@ -442,7 +350,7 @@ object HiscoresView {
|
||||||
val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(Constants.LVL_BAR_SPRITE))
|
val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(Constants.LVL_BAR_SPRITE))
|
||||||
|
|
||||||
val totalLevelIcon = ImageCanvas(bufferedImageSprite).apply {
|
val totalLevelIcon = ImageCanvas(bufferedImageSprite).apply {
|
||||||
fillColor = COLOR_BACKGROUND_DARK
|
fillColor = Constants.COLOR_BACKGROUND_DARK
|
||||||
preferredSize = Constants.ICON_DIMENSION_LARGE
|
preferredSize = Constants.ICON_DIMENSION_LARGE
|
||||||
size = Constants.ICON_DIMENSION_LARGE
|
size = Constants.ICON_DIMENSION_LARGE
|
||||||
}
|
}
|
||||||
|
|
@ -464,7 +372,7 @@ object HiscoresView {
|
||||||
val bufferedImageSprite2 = getBufferedImageFromSprite(API.GetSprite(Constants.COMBAT_LVL_SPRITE))
|
val bufferedImageSprite2 = getBufferedImageFromSprite(API.GetSprite(Constants.COMBAT_LVL_SPRITE))
|
||||||
|
|
||||||
val combatLevelIcon = ImageCanvas(bufferedImageSprite2).apply {
|
val combatLevelIcon = ImageCanvas(bufferedImageSprite2).apply {
|
||||||
fillColor = COLOR_BACKGROUND_DARK
|
fillColor = Constants.COLOR_BACKGROUND_DARK
|
||||||
preferredSize = Constants.ICON_DIMENSION_LARGE
|
preferredSize = Constants.ICON_DIMENSION_LARGE
|
||||||
size = Constants.ICON_DIMENSION_LARGE
|
size = Constants.ICON_DIMENSION_LARGE
|
||||||
}
|
}
|
||||||
|
|
@ -478,7 +386,7 @@ object HiscoresView {
|
||||||
}
|
}
|
||||||
|
|
||||||
val combatLevelPanel = JPanel(FlowLayout(FlowLayout.LEFT)).apply {
|
val combatLevelPanel = JPanel(FlowLayout(FlowLayout.LEFT)).apply {
|
||||||
background = COLOR_BACKGROUND_DARK
|
background = Constants.COLOR_BACKGROUND_DARK
|
||||||
add(combatLevelIcon)
|
add(combatLevelIcon)
|
||||||
add(combatLevelLabel)
|
add(combatLevelLabel)
|
||||||
}
|
}
|
||||||
|
|
@ -1,18 +1,23 @@
|
||||||
package KondoKit
|
package KondoKit.views
|
||||||
|
|
||||||
|
import KondoKit.Helpers
|
||||||
import KondoKit.Helpers.addMouseListenerToAll
|
import KondoKit.Helpers.addMouseListenerToAll
|
||||||
import KondoKit.Helpers.formatHtmlLabelText
|
import KondoKit.Helpers.formatHtmlLabelText
|
||||||
|
import KondoKit.ImageCanvas
|
||||||
import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite
|
import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite
|
||||||
import KondoKit.XPTrackerView.wrappedWidget
|
import KondoKit.views.XPTrackerView.wrappedWidget
|
||||||
|
import KondoKit.components.PopupMenuComponent
|
||||||
|
import KondoKit.components.ProgressBar
|
||||||
import KondoKit.plugin.Companion.POPUP_BACKGROUND
|
import KondoKit.plugin.Companion.POPUP_BACKGROUND
|
||||||
import KondoKit.plugin.Companion.POPUP_FOREGROUND
|
import KondoKit.plugin.Companion.POPUP_FOREGROUND
|
||||||
import KondoKit.plugin.Companion.TITLE_BAR_COLOR
|
import KondoKit.plugin.Companion.TITLE_BAR_COLOR
|
||||||
import KondoKit.plugin.Companion.TOOLTIP_BACKGROUND
|
import KondoKit.plugin.Companion.TOOLTIP_BACKGROUND
|
||||||
import KondoKit.plugin.Companion.TOTAL_XP_WIDGET_SIZE
|
import KondoKit.plugin.Companion.TOTAL_XP_WIDGET_SIZE
|
||||||
import KondoKit.plugin.Companion.VIEW_BACKGROUND_COLOR
|
|
||||||
import KondoKit.plugin.Companion.WIDGET_COLOR
|
import KondoKit.plugin.Companion.WIDGET_COLOR
|
||||||
import KondoKit.plugin.Companion.primaryColor
|
import KondoKit.plugin.Companion.primaryColor
|
||||||
|
import KondoKit.plugin.Companion.registerDrawAction
|
||||||
import KondoKit.plugin.Companion.secondaryColor
|
import KondoKit.plugin.Companion.secondaryColor
|
||||||
|
import KondoKit.plugin.Companion.useLiveGEPrices
|
||||||
import KondoKit.plugin.StateManager.focusedView
|
import KondoKit.plugin.StateManager.focusedView
|
||||||
import plugin.api.API
|
import plugin.api.API
|
||||||
import rt4.*
|
import rt4.*
|
||||||
|
|
@ -22,17 +27,16 @@ import java.awt.event.MouseAdapter
|
||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
import java.awt.image.BufferedImage
|
import java.awt.image.BufferedImage
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.InputStreamReader
|
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
|
|
||||||
object LootTrackerView {
|
object LootTrackerView : View, OnPostClientTickCallback, OnKillingBlowNPCCallback {
|
||||||
private const val SNAPSHOT_LIFESPAN = 10
|
private const val SNAPSHOT_LIFESPAN = 10
|
||||||
const val BAG_ICON = 900
|
const val BAG_ICON = 900
|
||||||
|
const val OPEN_BAG = 777
|
||||||
val npcDeathSnapshots = mutableMapOf<Int, GroundSnapshot>()
|
val npcDeathSnapshots = mutableMapOf<Int, GroundSnapshot>()
|
||||||
var gePriceMap = loadGEPrices()
|
var gePriceMap = loadGEPrices()
|
||||||
const val VIEW_NAME = "LOOT_TRACKER_VIEW"
|
const val VIEW_NAME = "LOOT_TRACKER_VIEW"
|
||||||
|
|
@ -42,9 +46,33 @@ object LootTrackerView {
|
||||||
var lastConfirmedKillNpcId = -1
|
var lastConfirmedKillNpcId = -1
|
||||||
private var customToolTipWindow: JWindow? = null
|
private var customToolTipWindow: JWindow? = null
|
||||||
var lootTrackerView: JPanel? = null
|
var lootTrackerView: JPanel? = null
|
||||||
|
override val name: String = VIEW_NAME
|
||||||
|
override val iconSpriteId: Int = OPEN_BAG
|
||||||
|
|
||||||
|
override val panel: JPanel
|
||||||
|
get() = lootTrackerView ?: JPanel()
|
||||||
|
|
||||||
|
override fun createView() {
|
||||||
|
createLootTrackerView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun registerFunctions() {
|
||||||
|
// Register callbacks with the plugin
|
||||||
|
KondoKit.plugin.registerPostClientTickCallback(this)
|
||||||
|
KondoKit.plugin.registerKillingBlowNPCCallback(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostClientTick() {
|
||||||
|
lootTrackerView?.let { onPostClientTick(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onKillingBlowNPC(npcID: Int, x: Int, z: Int) {
|
||||||
|
val preDeathSnapshot = takeGroundSnapshot(Pair(x,z))
|
||||||
|
npcDeathSnapshots[npcID] = GroundSnapshot(preDeathSnapshot, Pair(x, z), 0)
|
||||||
|
}
|
||||||
|
|
||||||
fun loadGEPrices(): Map<String, String> {
|
fun loadGEPrices(): Map<String, String> {
|
||||||
return if (plugin.useLiveGEPrices) {
|
return if (useLiveGEPrices) {
|
||||||
try {
|
try {
|
||||||
println("LootTracker: Loading Remote GE Prices")
|
println("LootTracker: Loading Remote GE Prices")
|
||||||
val url = URL("https://cdn.2009scape.org/gedata/latest.json")
|
val url = URL("https://cdn.2009scape.org/gedata/latest.json")
|
||||||
|
|
@ -79,23 +107,21 @@ object LootTrackerView {
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
println("LootTracker: Loading Local GE Prices")
|
println("LootTracker: Loading Local GE Prices")
|
||||||
BufferedReader(InputStreamReader(plugin::class.java.getResourceAsStream("res/item_configs.json"), StandardCharsets.UTF_8))
|
Helpers.readResourceText("res/item_configs.json")?.let { json ->
|
||||||
.useLines { lines ->
|
val items = json.trim().removeSurrounding("[", "]").split("},").map { it.trim() + "}" }
|
||||||
val json = lines.joinToString("\n")
|
val gePrices = mutableMapOf<String, String>()
|
||||||
val items = json.trim().removeSurrounding("[", "]").split("},").map { it.trim() + "}" }
|
|
||||||
val gePrices = mutableMapOf<String, String>()
|
|
||||||
|
|
||||||
for (item in items) {
|
for (item in items) {
|
||||||
val pairs = item.removeSurrounding("{", "}").split(",")
|
val pairs = item.removeSurrounding("{", "}").split(",")
|
||||||
val id = pairs.find { it.trim().startsWith("\"id\"") }?.split(":")?.get(1)?.trim()?.trim('\"')
|
val id = pairs.find { it.trim().startsWith("\"id\"") }?.split(":")?.get(1)?.trim()?.trim('\"')
|
||||||
val grandExchangePrice = pairs.find { it.trim().startsWith("\"grand_exchange_price\"") }?.split(":")?.get(1)?.trim()?.trim('\"')
|
val grandExchangePrice = pairs.find { it.trim().startsWith("\"grand_exchange_price\"") }?.split(":")?.get(1)?.trim()?.trim('\"')
|
||||||
if (id != null && grandExchangePrice != null) {
|
if (id != null && grandExchangePrice != null) {
|
||||||
gePrices[id] = grandExchangePrice
|
gePrices[id] = grandExchangePrice
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gePrices
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gePrices
|
||||||
|
} ?: emptyMap()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
emptyMap()
|
emptyMap()
|
||||||
}
|
}
|
||||||
|
|
@ -105,9 +131,7 @@ object LootTrackerView {
|
||||||
|
|
||||||
|
|
||||||
fun createLootTrackerView() {
|
fun createLootTrackerView() {
|
||||||
lootTrackerView = JPanel().apply {
|
lootTrackerView = BaseView(VIEW_NAME, addDefaultSpacing = false).apply {
|
||||||
layout = BoxLayout(this, BoxLayout.Y_AXIS) // Use BoxLayout on Y axis to stack widgets vertically
|
|
||||||
background = VIEW_BACKGROUND_COLOR
|
|
||||||
add(Box.createVerticalStrut(5))
|
add(Box.createVerticalStrut(5))
|
||||||
totalTrackerWidget = createTotalLootWidget()
|
totalTrackerWidget = createTotalLootWidget()
|
||||||
|
|
||||||
|
|
@ -376,7 +400,8 @@ object LootTrackerView {
|
||||||
fun onPostClientTick(lootTrackerView: JPanel) {
|
fun onPostClientTick(lootTrackerView: JPanel) {
|
||||||
val toRemove = mutableListOf<Int>()
|
val toRemove = mutableListOf<Int>()
|
||||||
|
|
||||||
npcDeathSnapshots.entries.forEach { (npcId, snapshot) ->
|
npcDeathSnapshots.entries.forEach { entry ->
|
||||||
|
val (npcId, snapshot) = entry
|
||||||
val postDeathSnapshot = takeGroundSnapshot(Pair(snapshot.location.first, snapshot.location.second))
|
val postDeathSnapshot = takeGroundSnapshot(Pair(snapshot.location.first, snapshot.location.second))
|
||||||
val newDrops = postDeathSnapshot.subtract(snapshot.items)
|
val newDrops = postDeathSnapshot.subtract(snapshot.items)
|
||||||
|
|
||||||
|
|
@ -392,7 +417,9 @@ object LootTrackerView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toRemove.forEach { npcDeathSnapshots.remove(it) }
|
toRemove.forEach { npcId ->
|
||||||
|
npcDeathSnapshots.remove(npcId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -427,7 +454,7 @@ object LootTrackerView {
|
||||||
newDrops.forEach { drop ->
|
newDrops.forEach { drop ->
|
||||||
val geValue = (gePriceMap[drop.id.toString()]?.toInt() ?: 0) * drop.quantity
|
val geValue = (gePriceMap[drop.id.toString()]?.toInt() ?: 0) * drop.quantity
|
||||||
updateValueLabel(lootTrackerView, geValue.toString(), npcName)
|
updateValueLabel(lootTrackerView, geValue.toString(), npcName)
|
||||||
plugin.registerDrawAction { addItemToLootPanel(lootTrackerView, drop, npcName) }
|
registerDrawAction { addItemToLootPanel(lootTrackerView, drop, npcName) }
|
||||||
updateTotalValue(geValue)
|
updateTotalValue(geValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -503,7 +530,7 @@ object LootTrackerView {
|
||||||
|
|
||||||
private fun removeLootFrameMenu(toRemove: JPanel, npcName: String): JPopupMenu {
|
private fun removeLootFrameMenu(toRemove: JPanel, npcName: String): JPopupMenu {
|
||||||
// Create a popup menu
|
// Create a popup menu
|
||||||
val popupMenu = JPopupMenu()
|
val popupMenu = PopupMenuComponent()
|
||||||
val rFont = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
|
val rFont = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
|
||||||
|
|
||||||
popupMenu.background = POPUP_BACKGROUND
|
popupMenu.background = POPUP_BACKGROUND
|
||||||
|
|
@ -540,20 +567,17 @@ object LootTrackerView {
|
||||||
|
|
||||||
private fun resetLootTrackerMenu(): JPopupMenu {
|
private fun resetLootTrackerMenu(): JPopupMenu {
|
||||||
// Create a popup menu
|
// Create a popup menu
|
||||||
val popupMenu = JPopupMenu()
|
val popupMenu = PopupMenuComponent()
|
||||||
val rFont = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
|
|
||||||
|
|
||||||
popupMenu.background = POPUP_BACKGROUND
|
|
||||||
|
|
||||||
// Create menu items with custom font and colors
|
// Create menu items with custom font and colors
|
||||||
val menuItem1 = JMenuItem("Reset Loot Tracker").apply {
|
val menuItem1 = JMenuItem("Reset Loot Tracker").apply {
|
||||||
font = rFont // Set custom font
|
font = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
|
||||||
background = POPUP_BACKGROUND // Dark background for item
|
background = POPUP_BACKGROUND
|
||||||
foreground = POPUP_FOREGROUND // Light text color for item
|
foreground = POPUP_FOREGROUND
|
||||||
}
|
}
|
||||||
popupMenu.add(menuItem1)
|
popupMenu.add(menuItem1)
|
||||||
menuItem1.addActionListener {
|
menuItem1.addActionListener {
|
||||||
plugin.registerDrawAction {
|
registerDrawAction {
|
||||||
resetLootTracker()
|
resetLootTracker()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -608,4 +632,18 @@ object LootTrackerView {
|
||||||
|
|
||||||
data class GroundSnapshot(val items: Set<Item>, val location: Pair<Int, Int>, var age: Int)
|
data class GroundSnapshot(val items: Set<Item>, val location: Pair<Int, Int>, var age: Int)
|
||||||
data class Item(val id: Int, val quantity: Int)
|
data class Item(val id: Int, val quantity: Int)
|
||||||
|
|
||||||
|
// XPWidget data class for loot tracking
|
||||||
|
data class XPWidget(
|
||||||
|
val container: Container,
|
||||||
|
val skillId: Int,
|
||||||
|
val xpGainedLabel: JLabel,
|
||||||
|
val xpLeftLabel: JLabel,
|
||||||
|
val xpPerHourLabel: JLabel,
|
||||||
|
val actionsRemainingLabel: JLabel,
|
||||||
|
val progressBar: ProgressBar,
|
||||||
|
var totalXpGained: Int = 0,
|
||||||
|
var startTime: Long = System.currentTimeMillis(),
|
||||||
|
var previousXp: Int = 0
|
||||||
|
)
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load diff
12
plugin-playground/src/main/kotlin/KondoKit/views/View.kt
Normal file
12
plugin-playground/src/main/kotlin/KondoKit/views/View.kt
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
package KondoKit.views
|
||||||
|
|
||||||
|
import javax.swing.JPanel
|
||||||
|
|
||||||
|
interface View {
|
||||||
|
val name: String
|
||||||
|
val iconSpriteId: Int
|
||||||
|
val panel: JPanel
|
||||||
|
|
||||||
|
fun createView()
|
||||||
|
fun registerFunctions()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package KondoKit.views
|
||||||
|
|
||||||
|
interface OnUpdateCallback {
|
||||||
|
fun onUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OnDrawCallback {
|
||||||
|
fun onDraw(timeDelta: Long)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OnXPUpdateCallback {
|
||||||
|
fun onXPUpdate(skillId: Int, xp: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OnKillingBlowNPCCallback {
|
||||||
|
fun onKillingBlowNPC(npcID: Int, x: Int, z: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OnPostClientTickCallback {
|
||||||
|
fun onPostClientTick()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,497 @@
|
||||||
|
package KondoKit.views
|
||||||
|
|
||||||
|
import KondoKit.Helpers
|
||||||
|
import KondoKit.Helpers.addMouseListenerToAll
|
||||||
|
import KondoKit.Helpers.formatHtmlLabelText
|
||||||
|
import KondoKit.Helpers.formatNumber
|
||||||
|
import KondoKit.Helpers.getProgressBarColor
|
||||||
|
import KondoKit.Helpers.getSpriteId
|
||||||
|
import KondoKit.SpriteToBufferedImage.getBufferedImageFromSprite
|
||||||
|
import KondoKit.XPTable
|
||||||
|
import KondoKit.components.PopupMenuComponent
|
||||||
|
import KondoKit.components.ProgressBar
|
||||||
|
import KondoKit.components.WidgetPanel
|
||||||
|
import KondoKit.plugin.Companion.IMAGE_SIZE
|
||||||
|
import KondoKit.plugin.Companion.LVL_ICON
|
||||||
|
import KondoKit.plugin.Companion.TOTAL_XP_WIDGET_SIZE
|
||||||
|
import KondoKit.plugin.Companion.WIDGET_COLOR
|
||||||
|
import KondoKit.plugin.Companion.WIDGET_SIZE
|
||||||
|
import KondoKit.plugin
|
||||||
|
import KondoKit.plugin.Companion.POPUP_BACKGROUND
|
||||||
|
import KondoKit.plugin.Companion.POPUP_FOREGROUND
|
||||||
|
import KondoKit.plugin.Companion.playerXPMultiplier
|
||||||
|
import KondoKit.plugin.Companion.primaryColor
|
||||||
|
import KondoKit.plugin.Companion.secondaryColor
|
||||||
|
import KondoKit.plugin.StateManager.focusedView
|
||||||
|
import plugin.api.API
|
||||||
|
import java.awt.*
|
||||||
|
import java.awt.image.BufferedImage
|
||||||
|
import java.awt.event.MouseAdapter
|
||||||
|
import java.awt.event.MouseEvent
|
||||||
|
import javax.swing.*
|
||||||
|
import javax.swing.SwingConstants
|
||||||
|
|
||||||
|
object XPTrackerView : View, OnUpdateCallback, OnXPUpdateCallback {
|
||||||
|
private val COMBAT_SKILLS = intArrayOf(0,1,2,3,4)
|
||||||
|
val xpWidgets: MutableMap<Int, XPWidget> = HashMap()
|
||||||
|
var totalXPWidget: XPWidget? = null
|
||||||
|
val initialXP: MutableMap<Int, Int> = HashMap()
|
||||||
|
var xpTrackerView: JPanel? = null
|
||||||
|
const val VIEW_NAME = "XP_TRACKER_VIEW"
|
||||||
|
override val name: String = VIEW_NAME
|
||||||
|
override val iconSpriteId: Int = LVL_ICON
|
||||||
|
private val skillIconCache: MutableMap<Int, BufferedImage> = HashMap()
|
||||||
|
|
||||||
|
|
||||||
|
val npcHitpointsMap: Map<Int, Int> = try {
|
||||||
|
val json = Helpers.readResourceText("res/npc_hitpoints_map.json") ?: "{}"
|
||||||
|
val pairs = json.trim().removeSurrounding("{", "}").split(",")
|
||||||
|
val map = mutableMapOf<Int, Int>()
|
||||||
|
|
||||||
|
for (pair in pairs) {
|
||||||
|
val keyValue = pair.split(":")
|
||||||
|
val id = keyValue[0].trim().trim('\"').toIntOrNull()
|
||||||
|
val hitpoints = keyValue[1].trim()
|
||||||
|
|
||||||
|
if (id != null && hitpoints.isNotEmpty()) {
|
||||||
|
map[id] = hitpoints.toIntOrNull() ?: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
map
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("XPTracker Error parsing NPC HP: ${e.message}")
|
||||||
|
emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val widgetFont = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
|
||||||
|
|
||||||
|
private fun createPopupListener(popupMenu: JPopupMenu) = object : MouseAdapter() {
|
||||||
|
override fun mousePressed(e: MouseEvent) {
|
||||||
|
if (e.isPopupTrigger) popupMenu.show(e.component, e.x, e.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mouseReleased(e: MouseEvent) {
|
||||||
|
if (e.isPopupTrigger) popupMenu.show(e.component, e.x, e.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun attachPopup(component: Container, popupMenu: JPopupMenu) {
|
||||||
|
val listener = createPopupListener(popupMenu)
|
||||||
|
addMouseListenerToAll(component, listener)
|
||||||
|
component.addMouseListener(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun BufferedImage.ensureOpaque(): BufferedImage {
|
||||||
|
for (y in 0 until height) {
|
||||||
|
for (x in 0 until width) {
|
||||||
|
val color = getRGB(x, y)
|
||||||
|
if (color != 0) {
|
||||||
|
setRGB(x, y, color or (0xFF shl 24))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createIconContainer(image: BufferedImage): JPanel {
|
||||||
|
val processed = image.ensureOpaque()
|
||||||
|
val icon = ImageIcon(processed)
|
||||||
|
val label = JLabel(icon).apply {
|
||||||
|
horizontalAlignment = SwingConstants.CENTER
|
||||||
|
verticalAlignment = SwingConstants.CENTER
|
||||||
|
isOpaque = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return JPanel(BorderLayout()).apply {
|
||||||
|
background = WIDGET_COLOR
|
||||||
|
val wrapperSize = Dimension(IMAGE_SIZE)
|
||||||
|
preferredSize = wrapperSize
|
||||||
|
minimumSize = wrapperSize
|
||||||
|
maximumSize = wrapperSize
|
||||||
|
add(label, BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createMetricLabel(title: String, initialValue: String = "0", topPadding: Int = 0): JLabel {
|
||||||
|
return JLabel(formatHtmlLabelText("$title ", primaryColor, initialValue, secondaryColor)).apply {
|
||||||
|
horizontalAlignment = JLabel.LEFT
|
||||||
|
font = widgetFont
|
||||||
|
if (topPadding > 0) {
|
||||||
|
border = BorderFactory.createEmptyBorder(topPadding, 0, 0, 0)
|
||||||
|
} else {
|
||||||
|
border = BorderFactory.createEmptyBorder(0, 0, 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createMenuItem(text: String): JMenuItem = JMenuItem(text).apply {
|
||||||
|
font = widgetFont
|
||||||
|
background = POPUP_BACKGROUND
|
||||||
|
foreground = POPUP_FOREGROUND
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSkillIcon(skillId: Int): BufferedImage {
|
||||||
|
return skillIconCache[skillId] ?: getBufferedImageFromSprite(API.GetSprite(getSpriteId(skillId))).also {
|
||||||
|
skillIconCache[skillId] = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createTotalWidgetContainer(popupMenu: JPopupMenu): Container {
|
||||||
|
totalXPWidget = createTotalXPWidget()
|
||||||
|
return wrappedWidget(totalXPWidget!!.container).also { attachPopup(it, popupMenu) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override val panel: JPanel
|
||||||
|
get() = xpTrackerView ?: JPanel()
|
||||||
|
|
||||||
|
override fun createView() {
|
||||||
|
createXPTrackerView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun registerFunctions() {
|
||||||
|
// Register callbacks with the plugin
|
||||||
|
plugin.registerUpdateCallback(this)
|
||||||
|
plugin.registerXPUpdateCallback(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUpdate() {
|
||||||
|
val widgets = xpWidgets.values
|
||||||
|
val totalXP = totalXPWidget
|
||||||
|
|
||||||
|
widgets.forEach { xpWidget ->
|
||||||
|
val elapsedTime = (System.currentTimeMillis() - xpWidget.startTime) / 1000.0 / 60.0 / 60.0
|
||||||
|
val xpPerHour = if (elapsedTime > 0) (xpWidget.totalXpGained / elapsedTime).toInt() else 0
|
||||||
|
val formattedXpPerHour = formatNumber(xpPerHour)
|
||||||
|
xpWidget.xpPerHourLabel.text =
|
||||||
|
formatHtmlLabelText("XP /hr: ", primaryColor, formattedXpPerHour, secondaryColor)
|
||||||
|
xpWidget.container.repaint()
|
||||||
|
}
|
||||||
|
|
||||||
|
totalXP?.let { widget ->
|
||||||
|
val elapsedTime = (System.currentTimeMillis() - widget.startTime) / 1000.0 / 60.0 / 60.0
|
||||||
|
val totalXPPerHour = if (elapsedTime > 0) (widget.totalXpGained / elapsedTime).toInt() else 0
|
||||||
|
val formattedTotalXpPerHour = formatNumber(totalXPPerHour)
|
||||||
|
widget.xpPerHourLabel.text =
|
||||||
|
formatHtmlLabelText("XP /hr: ", primaryColor, formattedTotalXpPerHour, secondaryColor)
|
||||||
|
widget.container.repaint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onXPUpdate(skillId: Int, xp: Int) {
|
||||||
|
if (!initialXP.containsKey(skillId)) {
|
||||||
|
initialXP[skillId] = xp
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val previousXpSnapshot = initialXP[skillId] ?: xp
|
||||||
|
if (xp == initialXP[skillId]) return
|
||||||
|
|
||||||
|
val ensureOnEdt = Runnable {
|
||||||
|
var xpWidget = xpWidgets[skillId]
|
||||||
|
if (xpWidget != null) {
|
||||||
|
updateWidget(xpWidget, xp)
|
||||||
|
} else {
|
||||||
|
xpWidget = createXPWidget(skillId, previousXpSnapshot)
|
||||||
|
xpWidgets[skillId] = xpWidget
|
||||||
|
|
||||||
|
val wrapped = wrappedWidget(xpWidget.container)
|
||||||
|
val popupMenu = removeXPWidgetMenu(wrapped, skillId)
|
||||||
|
attachPopup(wrapped, popupMenu)
|
||||||
|
|
||||||
|
xpTrackerView?.add(wrapped)
|
||||||
|
xpTrackerView?.add(Box.createVerticalStrut(5))
|
||||||
|
|
||||||
|
if(focusedView == VIEW_NAME) {
|
||||||
|
xpTrackerView?.revalidate()
|
||||||
|
xpTrackerView?.repaint()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWidget(xpWidget, xp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SwingUtilities.isEventDispatchThread()) {
|
||||||
|
ensureOnEdt.run()
|
||||||
|
} else {
|
||||||
|
SwingUtilities.invokeLater(ensureOnEdt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateWidget(xpWidget: XPWidget, xp: Int) {
|
||||||
|
val (currentLevel, xpGainedSinceLastLevel) = XPTable.getLevelForXp(xp)
|
||||||
|
|
||||||
|
var xpGainedSinceLastUpdate = xp - xpWidget.previousXp
|
||||||
|
xpWidget.totalXpGained += xpGainedSinceLastUpdate
|
||||||
|
updateTotalXPWidget(xpGainedSinceLastUpdate)
|
||||||
|
|
||||||
|
val progress: Double
|
||||||
|
if (currentLevel >= 99) {
|
||||||
|
progress = 100.0 // Set progress to 100% if the level is 99 or above
|
||||||
|
xpWidget.xpLeftLabel.text = "" // Hide XP Left when level is 99
|
||||||
|
xpWidget.actionsRemainingLabel.text = ""
|
||||||
|
} else {
|
||||||
|
val nextLevelXp = XPTable.getXpRequiredForLevel(currentLevel + 1)
|
||||||
|
val xpLeft = nextLevelXp - xp
|
||||||
|
progress = xpGainedSinceLastLevel.toDouble() / (nextLevelXp - XPTable.getXpRequiredForLevel(currentLevel)) * 100
|
||||||
|
val xpLeftstr = formatNumber(xpLeft)
|
||||||
|
xpWidget.xpLeftLabel.text = formatHtmlLabelText("XP Left: ", primaryColor, xpLeftstr, secondaryColor)
|
||||||
|
if(COMBAT_SKILLS.contains(xpWidget.skillId)) {
|
||||||
|
if(LootTrackerView.lastConfirmedKillNpcId != -1 && npcHitpointsMap.isNotEmpty()) {
|
||||||
|
val npcHP = npcHitpointsMap[LootTrackerView.lastConfirmedKillNpcId]
|
||||||
|
val xpPerKill = when (xpWidget.skillId) {
|
||||||
|
3 -> playerXPMultiplier * (npcHP ?: 1) // Hitpoints
|
||||||
|
else -> playerXPMultiplier * (npcHP ?: 1) * 4 // Combat XP for other skills
|
||||||
|
}
|
||||||
|
val remainingKills = xpLeft / xpPerKill
|
||||||
|
xpWidget.actionsRemainingLabel.text = formatHtmlLabelText("Kills: ", primaryColor, remainingKills.toString(), secondaryColor)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(xpGainedSinceLastUpdate == 0)
|
||||||
|
xpGainedSinceLastUpdate = 1 // Avoid possible divide by 0
|
||||||
|
val remainingActions = (xpLeft / xpGainedSinceLastUpdate).coerceAtLeast(1)
|
||||||
|
xpWidget.actionsRemainingLabel.text = formatHtmlLabelText("Actions: ", primaryColor, remainingActions.toString(), secondaryColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val formattedXp = formatNumber(xpWidget.totalXpGained)
|
||||||
|
xpWidget.xpGainedLabel.text = formatHtmlLabelText("XP Gained: ", primaryColor, formattedXp, secondaryColor)
|
||||||
|
|
||||||
|
// Update the progress bar with current level, progress, and next level
|
||||||
|
xpWidget.progressBar.updateProgress(progress, currentLevel, if (currentLevel < 99) currentLevel + 1 else 99, focusedView == VIEW_NAME)
|
||||||
|
|
||||||
|
xpWidget.previousXp = xp
|
||||||
|
if (focusedView == VIEW_NAME)
|
||||||
|
xpWidget.container.repaint()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun updateTotalXPWidget(xpGainedSinceLastUpdate: Int) {
|
||||||
|
val totalXPWidget = totalXPWidget ?: return
|
||||||
|
totalXPWidget.totalXpGained += xpGainedSinceLastUpdate
|
||||||
|
val formattedXp = formatNumber(totalXPWidget.totalXpGained)
|
||||||
|
totalXPWidget.xpGainedLabel.text = formatHtmlLabelText("Gained: ", primaryColor, formattedXp, secondaryColor)
|
||||||
|
if (focusedView == VIEW_NAME)
|
||||||
|
totalXPWidget.container.repaint()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun resetXPTracker(xpTrackerView: JPanel) {
|
||||||
|
xpTrackerView.removeAll()
|
||||||
|
val popupMenu = createResetMenu()
|
||||||
|
|
||||||
|
xpTrackerView.add(Box.createVerticalStrut(5))
|
||||||
|
xpTrackerView.add(createTotalWidgetContainer(popupMenu))
|
||||||
|
xpTrackerView.add(Box.createVerticalStrut(5))
|
||||||
|
|
||||||
|
initialXP.clear()
|
||||||
|
xpWidgets.clear()
|
||||||
|
|
||||||
|
xpTrackerView.revalidate()
|
||||||
|
if (focusedView == VIEW_NAME) {
|
||||||
|
xpTrackerView.repaint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createTotalXPWidget(): XPWidget {
|
||||||
|
val widgetPanel = WidgetPanel(TOTAL_XP_WIDGET_SIZE.width, TOTAL_XP_WIDGET_SIZE.height, addDefaultPadding = false)
|
||||||
|
|
||||||
|
val iconContainer = createIconContainer(getBufferedImageFromSprite(API.GetSprite(LVL_ICON)))
|
||||||
|
|
||||||
|
val textPanel = JPanel(GridLayout(2, 1, 5, 0)).apply {
|
||||||
|
background = WIDGET_COLOR
|
||||||
|
}
|
||||||
|
|
||||||
|
val xpGainedLabel = createMetricLabel("Gained:")
|
||||||
|
val xpPerHourLabel = createMetricLabel("XP /hr:")
|
||||||
|
|
||||||
|
textPanel.add(xpGainedLabel)
|
||||||
|
textPanel.add(xpPerHourLabel)
|
||||||
|
|
||||||
|
widgetPanel.add(iconContainer, BorderLayout.WEST)
|
||||||
|
widgetPanel.add(textPanel, BorderLayout.CENTER)
|
||||||
|
|
||||||
|
return XPWidget(
|
||||||
|
skillId = -1,
|
||||||
|
container = widgetPanel,
|
||||||
|
xpGainedLabel = xpGainedLabel,
|
||||||
|
xpLeftLabel = createMetricLabel("XP Left:"),
|
||||||
|
xpPerHourLabel = xpPerHourLabel,
|
||||||
|
progressBar = ProgressBar(0.0, Color.BLACK), // Unused
|
||||||
|
totalXpGained = 0,
|
||||||
|
startTime = System.currentTimeMillis(),
|
||||||
|
previousXp = 0,
|
||||||
|
actionsRemainingLabel = JLabel().apply { font = widgetFont },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun createXPTrackerView() {
|
||||||
|
val widgetViewPanel = BaseView(VIEW_NAME, addDefaultSpacing = false).apply {
|
||||||
|
add(Box.createVerticalStrut(5))
|
||||||
|
}
|
||||||
|
|
||||||
|
val popupMenu = createResetMenu()
|
||||||
|
widgetViewPanel.add(createTotalWidgetContainer(popupMenu))
|
||||||
|
widgetViewPanel.add(Box.createVerticalStrut(5))
|
||||||
|
|
||||||
|
xpTrackerView = widgetViewPanel
|
||||||
|
|
||||||
|
// Preload skill icons to avoid first-drop lag
|
||||||
|
try {
|
||||||
|
for (i in 0 until 24) {
|
||||||
|
getSkillIcon(i)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// Ignore preload errors; fallback at use time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun createResetMenu(): JPopupMenu {
|
||||||
|
val popupMenu = PopupMenuComponent()
|
||||||
|
|
||||||
|
val resetItem = createMenuItem("Reset Tracker")
|
||||||
|
popupMenu.add(resetItem)
|
||||||
|
|
||||||
|
resetItem.addActionListener { plugin.registerDrawAction { resetXPTracker(xpTrackerView!!) } }
|
||||||
|
return popupMenu
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeXPWidgetMenu(toRemove: Container, skillId: Int): JPopupMenu {
|
||||||
|
val popupMenu = PopupMenuComponent()
|
||||||
|
|
||||||
|
val resetItem = createMenuItem("Reset")
|
||||||
|
popupMenu.add(resetItem)
|
||||||
|
|
||||||
|
val removeItem = createMenuItem("Remove")
|
||||||
|
popupMenu.add(removeItem)
|
||||||
|
|
||||||
|
resetItem.addActionListener {
|
||||||
|
xpWidgets[skillId]?.let { widget ->
|
||||||
|
// Baseline at current XP and clear per-widget counters
|
||||||
|
initialXP[skillId] = widget.previousXp
|
||||||
|
widget.totalXpGained = 0
|
||||||
|
widget.startTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
// Recompute labels/progress for current XP without adding totals
|
||||||
|
updateWidget(widget, widget.previousXp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeItem.addActionListener {
|
||||||
|
// Reset the per-skill baseline to the current XP so next widget starts fresh
|
||||||
|
xpWidgets[skillId]?.let { widget ->
|
||||||
|
initialXP[skillId] = widget.previousXp
|
||||||
|
}
|
||||||
|
// Remove widget container and following spacer if present
|
||||||
|
xpTrackerView?.let { parent ->
|
||||||
|
val components = parent.components
|
||||||
|
val toRemoveIndex = components.indexOf(toRemove)
|
||||||
|
if (toRemoveIndex >= 0 && toRemoveIndex < components.size - 1) {
|
||||||
|
val nextComponent = components[toRemoveIndex + 1]
|
||||||
|
if (nextComponent is Box.Filler) {
|
||||||
|
parent.remove(nextComponent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parent.remove(toRemove)
|
||||||
|
xpWidgets.remove(skillId)
|
||||||
|
parent.revalidate()
|
||||||
|
if (focusedView == VIEW_NAME) parent.repaint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return popupMenu
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun createXPWidget(skillId: Int, previousXp: Int): XPWidget {
|
||||||
|
val widgetPanel = WidgetPanel(WIDGET_SIZE.width, WIDGET_SIZE.height, addDefaultPadding = false)
|
||||||
|
|
||||||
|
val iconContainer = createIconContainer(getSkillIcon(skillId))
|
||||||
|
|
||||||
|
val textPanel = JPanel(GridLayout(2, 2, 5, 0)).apply {
|
||||||
|
background = WIDGET_COLOR
|
||||||
|
}
|
||||||
|
|
||||||
|
val xpGainedLabel = createMetricLabel("XP Gained:")
|
||||||
|
val xpLeftLabel = createMetricLabel("XP Left:", "0K")
|
||||||
|
val xpPerHourLabel = createMetricLabel("XP /hr:")
|
||||||
|
val actionsTitle = if (COMBAT_SKILLS.contains(skillId)) "Kills:" else "Actions:"
|
||||||
|
val actionsLabel = createMetricLabel(actionsTitle)
|
||||||
|
|
||||||
|
val progressBar = ProgressBar(0.0, getProgressBarColor(skillId)).apply {
|
||||||
|
preferredSize = Dimension(160, 22)
|
||||||
|
minimumSize = preferredSize
|
||||||
|
maximumSize = preferredSize
|
||||||
|
}
|
||||||
|
|
||||||
|
val progressPanel = JPanel(BorderLayout()).apply {
|
||||||
|
background = WIDGET_COLOR
|
||||||
|
add(progressBar, BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
|
||||||
|
textPanel.add(xpGainedLabel)
|
||||||
|
textPanel.add(xpLeftLabel)
|
||||||
|
textPanel.add(xpPerHourLabel)
|
||||||
|
textPanel.add(actionsLabel)
|
||||||
|
|
||||||
|
widgetPanel.add(iconContainer, BorderLayout.WEST)
|
||||||
|
widgetPanel.add(textPanel, BorderLayout.CENTER)
|
||||||
|
widgetPanel.add(progressPanel, BorderLayout.SOUTH)
|
||||||
|
|
||||||
|
widgetPanel.revalidate()
|
||||||
|
if(focusedView == VIEW_NAME)
|
||||||
|
widgetPanel.repaint()
|
||||||
|
|
||||||
|
return XPWidget(
|
||||||
|
skillId = skillId,
|
||||||
|
container = widgetPanel,
|
||||||
|
xpGainedLabel = xpGainedLabel,
|
||||||
|
xpLeftLabel = xpLeftLabel,
|
||||||
|
xpPerHourLabel = xpPerHourLabel,
|
||||||
|
progressBar = progressBar,
|
||||||
|
totalXpGained = 0,
|
||||||
|
actionsRemainingLabel = actionsLabel,
|
||||||
|
startTime = System.currentTimeMillis(),
|
||||||
|
previousXp = previousXp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun wrappedWidget(component: Component, padding: Int = 7): Container {
|
||||||
|
val outerPanelSize = Dimension(
|
||||||
|
component.preferredSize.width + 2 * padding,
|
||||||
|
component.preferredSize.height + 2 * padding
|
||||||
|
)
|
||||||
|
val outerPanel = JPanel(GridBagLayout()).apply {
|
||||||
|
background = WIDGET_COLOR
|
||||||
|
preferredSize = outerPanelSize
|
||||||
|
maximumSize = outerPanelSize
|
||||||
|
minimumSize = outerPanelSize
|
||||||
|
}
|
||||||
|
val innerPanel = JPanel(BorderLayout()).apply {
|
||||||
|
background = WIDGET_COLOR
|
||||||
|
preferredSize = component.preferredSize
|
||||||
|
maximumSize = component.preferredSize
|
||||||
|
minimumSize = component.preferredSize
|
||||||
|
add(component, BorderLayout.CENTER)
|
||||||
|
}
|
||||||
|
val gbc = GridBagConstraints().apply {
|
||||||
|
anchor = GridBagConstraints.CENTER
|
||||||
|
}
|
||||||
|
outerPanel.add(innerPanel, gbc)
|
||||||
|
return outerPanel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
data class XPWidget(
|
||||||
|
val container: Container,
|
||||||
|
val skillId: Int,
|
||||||
|
val xpGainedLabel: JLabel,
|
||||||
|
val xpLeftLabel: JLabel,
|
||||||
|
val xpPerHourLabel: JLabel,
|
||||||
|
val actionsRemainingLabel: JLabel,
|
||||||
|
val progressBar: ProgressBar,
|
||||||
|
var totalXpGained: Int = 0,
|
||||||
|
var startTime: Long = System.currentTimeMillis(),
|
||||||
|
var previousXp: Int = 0
|
||||||
|
)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue