mirror of
https://gitlab.com/2009scape/rt4-client.git
synced 2025-12-20 05:20:24 -07:00
reorganize
This commit is contained in:
parent
529f0c22b0
commit
cf6bb51d2c
20 changed files with 815 additions and 411 deletions
|
|
@ -1,898 +0,0 @@
|
|||
package KondoKit
|
||||
|
||||
import KondoKit.Helpers.convertValue
|
||||
import KondoKit.Helpers.showToast
|
||||
import KondoKit.ReflectiveEditorComponents.CustomSearchField
|
||||
import KondoKit.ReflectiveEditorComponents.GitLabPlugin
|
||||
import KondoKit.ReflectiveEditorComponents.GitLabPluginFetcher
|
||||
import KondoKit.ReflectiveEditorComponents.PluginProperties
|
||||
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.secondaryColor
|
||||
import plugin.Plugin
|
||||
import plugin.PluginInfo
|
||||
import plugin.PluginRepository
|
||||
import rt4.GlobalJsonConfig
|
||||
import java.awt.*
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.event.*
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import java.util.Timer
|
||||
import javax.imageio.ImageIO
|
||||
import javax.swing.*
|
||||
import javax.swing.border.AbstractBorder
|
||||
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"
|
||||
const val PLUGIN_LIST_VIEW = "PLUGIN_LIST"
|
||||
const val PLUGIN_DETAIL_VIEW = "PLUGIN_DETAIL"
|
||||
val pluginsDirectory: File = File(GlobalJsonConfig.instance.pluginsFolder)
|
||||
|
||||
// GitLab API configuration
|
||||
private const val GITLAB_ACCESS_TOKEN = "glpat-dE2Cs2e4b32-H7c9oGuS"
|
||||
private const val GITLAB_PROJECT_ID = "38297322"
|
||||
private const val GITLAB_BRANCH = "master"
|
||||
|
||||
// Store fetched GitLab plugins
|
||||
private var gitLabPlugins: List<GitLabPlugin> = listOf()
|
||||
|
||||
// Store the cog icon to be reused
|
||||
private var cogIcon: Icon? = null
|
||||
|
||||
private fun loadCogIcon() {
|
||||
try {
|
||||
val imageStream = this::class.java.getResourceAsStream("res/cog.png")
|
||||
if (imageStream != null) {
|
||||
val image: BufferedImage = ImageIO.read(imageStream)
|
||||
val scaledImage = image.getScaledInstance(12, 12, Image.SCALE_SMOOTH)
|
||||
cogIcon = ImageIcon(scaledImage)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
// Card layout for switching between views within the reflective editor view
|
||||
private lateinit var cardLayout: CardLayout
|
||||
private lateinit var mainPanel: JPanel
|
||||
private var currentPluginInfo: PluginInfo? = null
|
||||
private var currentPlugin: Plugin? = null
|
||||
|
||||
// Search text for filtering plugins
|
||||
private var pluginSearchText: String = ""
|
||||
|
||||
fun createReflectiveEditorView() {
|
||||
// Load the cog icon once
|
||||
loadCogIcon()
|
||||
|
||||
// Create the main panel with card layout
|
||||
cardLayout = CardLayout()
|
||||
mainPanel = JPanel(cardLayout)
|
||||
mainPanel.background = VIEW_BACKGROUND_COLOR
|
||||
mainPanel.border = BorderFactory.createEmptyBorder(0, 0, 0, 0)
|
||||
// Help minimize flicker during dynamic swaps
|
||||
mainPanel.isDoubleBuffered = true
|
||||
|
||||
// Create the plugin list view
|
||||
val pluginListView = createPluginListView()
|
||||
pluginListView.name = PLUGIN_LIST_VIEW
|
||||
mainPanel.add(pluginListView, PLUGIN_LIST_VIEW)
|
||||
|
||||
// Create a placeholder for the plugin detail view
|
||||
val pluginDetailView = JPanel(BorderLayout())
|
||||
pluginDetailView.name = PLUGIN_DETAIL_VIEW
|
||||
pluginDetailView.background = VIEW_BACKGROUND_COLOR
|
||||
mainPanel.add(pluginDetailView, PLUGIN_DETAIL_VIEW)
|
||||
|
||||
// Set the reflectiveEditorView to our main panel
|
||||
reflectiveEditorView = mainPanel
|
||||
|
||||
// Show the plugin list view by default
|
||||
cardLayout.show(mainPanel, PLUGIN_LIST_VIEW)
|
||||
|
||||
// Fetch GitLab plugins in the background
|
||||
GitLabPluginFetcher.fetchGitLabPlugins { plugins ->
|
||||
gitLabPlugins = plugins
|
||||
// We'll update the UI when needed
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPluginListView(): JPanel {
|
||||
val panel = JPanel()
|
||||
panel.layout = BoxLayout(panel, BoxLayout.Y_AXIS)
|
||||
panel.background = VIEW_BACKGROUND_COLOR
|
||||
|
||||
// Add search field at the top
|
||||
val searchField = CustomSearchField(panel) { searchText ->
|
||||
pluginSearchText = searchText
|
||||
// Refresh the plugin list to apply filtering
|
||||
SwingUtilities.invokeLater {
|
||||
addPlugins(reflectiveEditorView!!)
|
||||
}
|
||||
}
|
||||
|
||||
val searchFieldWrapper = JPanel().apply {
|
||||
layout = BoxLayout(this, BoxLayout.X_AXIS)
|
||||
background = VIEW_BACKGROUND_COLOR
|
||||
preferredSize = Dimension(230, 30)
|
||||
maximumSize = preferredSize
|
||||
minimumSize = preferredSize
|
||||
alignmentX = Component.CENTER_ALIGNMENT
|
||||
add(searchField)
|
||||
}
|
||||
|
||||
panel.add(Box.createVerticalStrut(10)) // Spacer
|
||||
panel.add(searchFieldWrapper)
|
||||
panel.add(Box.createVerticalStrut(10)) // Spacer
|
||||
|
||||
try {
|
||||
panel.add(Box.createVerticalStrut(10)) // Spacer
|
||||
val loadedPluginsField = PluginRepository::class.java.getDeclaredField("loadedPlugins")
|
||||
loadedPluginsField.isAccessible = true
|
||||
val loadedPlugins = loadedPluginsField.get(null) as HashMap<*, *>
|
||||
|
||||
// Separate plugins with and without exposed attributes
|
||||
val pluginsWithExposed = mutableListOf<Pair<PluginInfo, Plugin>>()
|
||||
val pluginsWithoutExposed = mutableListOf<Pair<PluginInfo, Plugin>>()
|
||||
|
||||
for ((pluginInfo, plugin) in loadedPlugins) {
|
||||
val exposedFields = (plugin as Plugin).javaClass.declaredFields.filter { field ->
|
||||
field.annotations.any { annotation ->
|
||||
annotation.annotationClass.simpleName == "Exposed"
|
||||
}
|
||||
}
|
||||
|
||||
// Apply search filter
|
||||
val pluginName = plugin.javaClass.`package`.name
|
||||
if (pluginSearchText.isBlank() || pluginName.contains(pluginSearchText, ignoreCase = true)) {
|
||||
if (exposedFields.isNotEmpty()) {
|
||||
pluginsWithExposed.add(pluginInfo as PluginInfo to plugin as Plugin)
|
||||
} else {
|
||||
pluginsWithoutExposed.add(pluginInfo as PluginInfo to plugin as Plugin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort both lists by package name
|
||||
pluginsWithExposed.sortBy { it.second.javaClass.`package`.name }
|
||||
pluginsWithoutExposed.sortBy { it.second.javaClass.`package`.name }
|
||||
|
||||
// Add plugins with exposed attributes first
|
||||
for ((pluginInfo, plugin) in pluginsWithExposed) {
|
||||
val pluginPanel = createPluginItemPanel(pluginInfo, plugin)
|
||||
panel.add(pluginPanel)
|
||||
panel.add(Box.createVerticalStrut(5))
|
||||
}
|
||||
|
||||
// Add a separator if we have plugins with exposed attributes
|
||||
if (pluginsWithExposed.isNotEmpty() && pluginsWithoutExposed.isNotEmpty()) {
|
||||
val separator = JPanel()
|
||||
separator.background = VIEW_BACKGROUND_COLOR
|
||||
separator.preferredSize = Dimension(Int.MAX_VALUE, 1)
|
||||
separator.maximumSize = Dimension(Int.MAX_VALUE, 1)
|
||||
panel.add(separator)
|
||||
panel.add(Box.createVerticalStrut(5))
|
||||
}
|
||||
|
||||
// Add plugins without exposed attributes
|
||||
for ((pluginInfo, plugin) in pluginsWithoutExposed) {
|
||||
val pluginPanel = createPluginItemPanel(pluginInfo, plugin)
|
||||
panel.add(pluginPanel)
|
||||
panel.add(Box.createVerticalStrut(5))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
// Add disabled plugins to the list (filtered by search text)
|
||||
val disabledDir = File(pluginsDirectory, "disabled")
|
||||
if (disabledDir.exists() && disabledDir.isDirectory) {
|
||||
val disabledPlugins = disabledDir.listFiles { file -> file.isDirectory } ?: arrayOf()
|
||||
|
||||
// Add disabled plugins to the list without exposed attributes (filtered by search text)
|
||||
for (pluginDir in disabledPlugins.sortedBy { it.name }) {
|
||||
// Apply search filter
|
||||
if (pluginSearchText.isBlank() || pluginDir.name.contains(pluginSearchText, ignoreCase = true)) {
|
||||
val pluginPanel = createDisabledPluginItemPanel(pluginDir.name)
|
||||
panel.add(pluginPanel)
|
||||
panel.add(Box.createVerticalStrut(5))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add a section for available plugins from GitLab that are not installed
|
||||
if (pluginSearchText.isNotBlank()) {
|
||||
val matchingGitLabPlugins = gitLabPlugins.filter { plugin ->
|
||||
plugin.pluginProperties != null &&
|
||||
(plugin.path.contains(pluginSearchText, ignoreCase = true) ||
|
||||
plugin.pluginProperties!!.description.contains(pluginSearchText, ignoreCase = true))
|
||||
}
|
||||
|
||||
if (matchingGitLabPlugins.isNotEmpty()) {
|
||||
// Add a separator
|
||||
val separator = JPanel()
|
||||
separator.background = VIEW_BACKGROUND_COLOR
|
||||
separator.preferredSize = Dimension(Int.MAX_VALUE, 1)
|
||||
separator.maximumSize = Dimension(Int.MAX_VALUE, 1)
|
||||
panel.add(Box.createVerticalStrut(10))
|
||||
panel.add(separator)
|
||||
panel.add(Box.createVerticalStrut(5))
|
||||
|
||||
// Add a header for available plugins
|
||||
val headerPanel = JPanel(FlowLayout(FlowLayout.LEFT))
|
||||
headerPanel.background = VIEW_BACKGROUND_COLOR
|
||||
val headerLabel = JLabel("Available Plugins")
|
||||
headerLabel.foreground = secondaryColor
|
||||
headerLabel.font = Font("RuneScape Small", Font.BOLD, 16)
|
||||
headerPanel.add(headerLabel)
|
||||
panel.add(headerPanel)
|
||||
panel.add(Box.createVerticalStrut(5))
|
||||
|
||||
// Add matching GitLab plugins
|
||||
for (gitLabPlugin in matchingGitLabPlugins) {
|
||||
val pluginPanel = createGitLabPluginItemPanel(gitLabPlugin)
|
||||
panel.add(pluginPanel)
|
||||
panel.add(Box.createVerticalStrut(5))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap the panel in a ScrollablePanel for custom scrolling
|
||||
val scrollablePanel = ScrollablePanel(panel)
|
||||
|
||||
// Reset scroll position to top
|
||||
SwingUtilities.invokeLater {
|
||||
// For ScrollablePanel, we need to reset the internal offset
|
||||
resetScrollablePanel(scrollablePanel)
|
||||
}
|
||||
|
||||
val container = JPanel(BorderLayout())
|
||||
container.background = VIEW_BACKGROUND_COLOR
|
||||
container.add(scrollablePanel, BorderLayout.CENTER)
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
private fun createPluginItemPanel(pluginInfo: PluginInfo, plugin: Plugin): JPanel {
|
||||
val panel = JPanel(BorderLayout())
|
||||
panel.background = WIDGET_COLOR
|
||||
panel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10)
|
||||
panel.maximumSize = Dimension(220, 60)
|
||||
|
||||
// Plugin name and version (using package name as in original implementation)
|
||||
val packageName = plugin.javaClass.`package`.name
|
||||
val nameLabel = JLabel(packageName)
|
||||
nameLabel.foreground = secondaryColor
|
||||
nameLabel.font = Font("RuneScape Small", Font.PLAIN, 16)
|
||||
|
||||
// Check if plugin has exposed attributes
|
||||
val exposedFields = plugin.javaClass.declaredFields.filter { field ->
|
||||
field.annotations.any { annotation ->
|
||||
annotation.annotationClass.simpleName == "Exposed"
|
||||
}
|
||||
}
|
||||
|
||||
// Edit button (only show if plugin has exposed attributes)
|
||||
val editButton = if (exposedFields.isNotEmpty()) {
|
||||
val button = JButton()
|
||||
if (cogIcon != null) {
|
||||
button.icon = cogIcon
|
||||
} else {
|
||||
button.text = "Edit"
|
||||
}
|
||||
button.background = TITLE_BAR_COLOR
|
||||
button.foreground = secondaryColor
|
||||
button.font = Font("RuneScape Small", Font.PLAIN, 14)
|
||||
button.addActionListener {
|
||||
showPluginDetails(pluginInfo, plugin)
|
||||
}
|
||||
button
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
// Plugin toggle switch (iOS style)
|
||||
val toggleSwitch = createToggleSwitch(plugin, pluginInfo)
|
||||
|
||||
// Layout
|
||||
val infoPanel = JPanel(BorderLayout())
|
||||
infoPanel.background = WIDGET_COLOR
|
||||
infoPanel.add(nameLabel, BorderLayout.WEST)
|
||||
|
||||
val controlsPanel = JPanel(FlowLayout(FlowLayout.RIGHT, 5, 0))
|
||||
controlsPanel.background = WIDGET_COLOR
|
||||
|
||||
// Add edit button first (left side of controls)
|
||||
editButton?.let { controlsPanel.add(it) }
|
||||
|
||||
// Add toggle switch second (right side of controls)
|
||||
controlsPanel.add(toggleSwitch)
|
||||
|
||||
panel.add(infoPanel, BorderLayout.CENTER)
|
||||
panel.add(controlsPanel, BorderLayout.EAST)
|
||||
|
||||
return panel
|
||||
}
|
||||
|
||||
private fun createDisabledPluginItemPanel(pluginName: String): JPanel {
|
||||
val panel = JPanel(BorderLayout())
|
||||
panel.background = WIDGET_COLOR
|
||||
panel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10)
|
||||
panel.maximumSize = Dimension(220, 60)
|
||||
|
||||
// Plugin name
|
||||
val nameLabel = JLabel(pluginName)
|
||||
nameLabel.foreground = secondaryColor
|
||||
nameLabel.font = Font("RuneScape Small", Font.PLAIN, 16)
|
||||
|
||||
// Plugin toggle switch (iOS style) - initially off for disabled plugins
|
||||
val toggleSwitch = ToggleSwitch()
|
||||
toggleSwitch.setActivated(false)
|
||||
toggleSwitch.onToggleListener = { activated ->
|
||||
if (activated) {
|
||||
enablePlugin(pluginName)
|
||||
}
|
||||
// If trying to disable an already disabled plugin, reset the toggle
|
||||
else {
|
||||
toggleSwitch.setActivated(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Layout
|
||||
val infoPanel = JPanel(BorderLayout())
|
||||
infoPanel.background = WIDGET_COLOR
|
||||
infoPanel.add(nameLabel, BorderLayout.WEST)
|
||||
|
||||
val controlsPanel = JPanel(FlowLayout(FlowLayout.RIGHT, 5, 0))
|
||||
controlsPanel.background = WIDGET_COLOR
|
||||
controlsPanel.add(toggleSwitch)
|
||||
|
||||
panel.add(infoPanel, BorderLayout.CENTER)
|
||||
panel.add(controlsPanel, BorderLayout.EAST)
|
||||
|
||||
return panel
|
||||
}
|
||||
|
||||
private fun createGitLabPluginItemPanel(gitLabPlugin: GitLabPlugin): JPanel {
|
||||
val panel = JPanel(BorderLayout())
|
||||
panel.background = WIDGET_COLOR
|
||||
panel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10)
|
||||
panel.maximumSize = Dimension(220, 60)
|
||||
|
||||
// Plugin name
|
||||
val nameLabel = JLabel(gitLabPlugin.path)
|
||||
nameLabel.foreground = secondaryColor
|
||||
nameLabel.font = Font("RuneScape Small", Font.PLAIN, 16)
|
||||
|
||||
// Download button (placeholder for now)
|
||||
val downloadButton = JButton("Download")
|
||||
downloadButton.background = TITLE_BAR_COLOR
|
||||
downloadButton.foreground = secondaryColor
|
||||
downloadButton.font = Font("RuneScape Small", Font.PLAIN, 14)
|
||||
downloadButton.addActionListener {
|
||||
// TODO: Implement download functionality
|
||||
showToast(mainPanel, "Download functionality not yet implemented", JOptionPane.INFORMATION_MESSAGE)
|
||||
}
|
||||
|
||||
// Plugin toggle switch (iOS style) - initially off for GitLab plugins
|
||||
val toggleSwitch = ToggleSwitch()
|
||||
toggleSwitch.setActivated(false)
|
||||
toggleSwitch.isEnabled = false // Disable toggle for GitLab plugins until downloaded
|
||||
|
||||
// Layout
|
||||
val infoPanel = JPanel(BorderLayout())
|
||||
infoPanel.background = WIDGET_COLOR
|
||||
infoPanel.add(nameLabel, BorderLayout.WEST)
|
||||
|
||||
val controlsPanel = JPanel(FlowLayout(FlowLayout.RIGHT, 5, 0))
|
||||
controlsPanel.background = WIDGET_COLOR
|
||||
controlsPanel.add(downloadButton)
|
||||
controlsPanel.add(toggleSwitch)
|
||||
|
||||
panel.add(infoPanel, BorderLayout.CENTER)
|
||||
panel.add(controlsPanel, BorderLayout.EAST)
|
||||
|
||||
return panel
|
||||
}
|
||||
|
||||
private fun enablePlugin(pluginName: String) {
|
||||
try {
|
||||
// Source and destination directories
|
||||
val disabledDir = File(pluginsDirectory, "disabled")
|
||||
val sourceDir = File(disabledDir, pluginName)
|
||||
val destDir = File(pluginsDirectory, pluginName)
|
||||
|
||||
// Check if source directory exists
|
||||
if (!sourceDir.exists()) {
|
||||
showToast(mainPanel, "Plugin directory not found: ${sourceDir.absolutePath}", JOptionPane.ERROR_MESSAGE)
|
||||
return
|
||||
}
|
||||
|
||||
// Move the directory
|
||||
if (sourceDir.renameTo(destDir)) {
|
||||
showToast(mainPanel, "Plugin enabled")
|
||||
|
||||
// Reload plugins to apply the change
|
||||
PluginRepository.reloadPlugins()
|
||||
|
||||
// Refresh the plugin list view
|
||||
SwingUtilities.invokeLater {
|
||||
addPlugins(reflectiveEditorView!!)
|
||||
}
|
||||
} else {
|
||||
showToast(mainPanel, "Failed to enable plugin", JOptionPane.ERROR_MESSAGE)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
showToast(mainPanel, "Error enabling plugin: ${e.message}", JOptionPane.ERROR_MESSAGE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createToggleSwitch(plugin: Plugin, pluginInfo: PluginInfo): ToggleSwitch {
|
||||
val toggleSwitch = ToggleSwitch()
|
||||
|
||||
// Set initial state
|
||||
toggleSwitch.setActivated(isPluginEnabled(plugin, pluginInfo))
|
||||
|
||||
// Add toggle listener
|
||||
toggleSwitch.onToggleListener = { activated ->
|
||||
togglePlugin(plugin, pluginInfo, toggleSwitch, activated)
|
||||
}
|
||||
|
||||
return toggleSwitch
|
||||
}
|
||||
|
||||
private fun isPluginEnabled(plugin: Plugin, pluginInfo: PluginInfo): Boolean {
|
||||
// Get the plugin directory name from the plugin's class package
|
||||
val pluginDirName = getPluginDirName(plugin)
|
||||
val pluginDir = File(pluginsDirectory, pluginDirName)
|
||||
return pluginDir.exists() && pluginDir.isDirectory
|
||||
}
|
||||
|
||||
private fun getPluginDirName(plugin: Plugin): String {
|
||||
// Extract the directory name from the plugin's package
|
||||
// The package name is typically like "GroundItems.plugin" so we take the first part
|
||||
val packageName = plugin.javaClass.`package`.name
|
||||
return packageName.substringBefore(".")
|
||||
}
|
||||
|
||||
private fun togglePlugin(plugin: Plugin, pluginInfo: PluginInfo, toggleSwitch: ToggleSwitch, activated: Boolean) {
|
||||
try {
|
||||
// Get the plugin directory name from the plugin's class package
|
||||
val pluginDirName = getPluginDirName(plugin)
|
||||
|
||||
// Source and destination directories
|
||||
val sourceDir = if (activated) {
|
||||
// Moving from disabled to enabled
|
||||
File(File(pluginsDirectory, "disabled"), pluginDirName)
|
||||
} else {
|
||||
// Moving from enabled to disabled
|
||||
File(pluginsDirectory, pluginDirName)
|
||||
}
|
||||
|
||||
val destDir = if (activated) {
|
||||
// Moving to main plugins directory
|
||||
File(pluginsDirectory, pluginDirName)
|
||||
} else {
|
||||
// Moving to disabled directory
|
||||
val disabledDir = File(pluginsDirectory, "disabled")
|
||||
if (!disabledDir.exists()) {
|
||||
disabledDir.mkdirs()
|
||||
}
|
||||
File(disabledDir, pluginDirName)
|
||||
}
|
||||
|
||||
// Check if source directory exists
|
||||
if (!sourceDir.exists()) {
|
||||
showToast(mainPanel, "Plugin directory not found: ${sourceDir.absolutePath}", JOptionPane.ERROR_MESSAGE)
|
||||
// Reset toggle switch to previous state
|
||||
toggleSwitch.setActivated(!activated)
|
||||
return
|
||||
}
|
||||
|
||||
// Move the directory
|
||||
if (sourceDir.renameTo(destDir)) {
|
||||
showToast(mainPanel, if (activated) "Plugin enabled" else "Plugin disabled")
|
||||
|
||||
// Reload plugins to apply the change
|
||||
PluginRepository.reloadPlugins()
|
||||
|
||||
// Refresh the plugin list view
|
||||
SwingUtilities.invokeLater {
|
||||
addPlugins(reflectiveEditorView!!)
|
||||
}
|
||||
} else {
|
||||
showToast(mainPanel, "Failed to ${if (activated) "enable" else "disable"} plugin", JOptionPane.ERROR_MESSAGE)
|
||||
// Reset toggle switch to previous state
|
||||
toggleSwitch.setActivated(!activated)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
showToast(mainPanel, "Error toggling plugin: ${e.message}", JOptionPane.ERROR_MESSAGE)
|
||||
// Reset toggle switch to previous state
|
||||
toggleSwitch.setActivated(!activated)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showPluginDetails(pluginInfo: PluginInfo, plugin: Plugin) {
|
||||
currentPluginInfo = pluginInfo
|
||||
currentPlugin = plugin
|
||||
|
||||
// Remove the existing detail view if it exists
|
||||
val existingDetailView = mainPanel.components.find { it.name == PLUGIN_DETAIL_VIEW }
|
||||
if (existingDetailView != null) {
|
||||
mainPanel.remove(existingDetailView)
|
||||
}
|
||||
|
||||
// Create a new detail view
|
||||
val detailView = JPanel(BorderLayout())
|
||||
detailView.name = PLUGIN_DETAIL_VIEW
|
||||
detailView.background = VIEW_BACKGROUND_COLOR
|
||||
|
||||
// Header with back button
|
||||
val headerPanel = JPanel(BorderLayout())
|
||||
headerPanel.background = TITLE_BAR_COLOR
|
||||
headerPanel.preferredSize = Dimension(Int.MAX_VALUE, 40)
|
||||
headerPanel.border = BorderFactory.createEmptyBorder(5, 10, 5, 10)
|
||||
|
||||
val backButton = JButton("Back")
|
||||
backButton.background = TITLE_BAR_COLOR
|
||||
backButton.foreground = secondaryColor
|
||||
backButton.font = Font("RuneScape Small", Font.PLAIN, 14)
|
||||
backButton.addActionListener {
|
||||
// Reset scroll position to top when returning to the list view
|
||||
SwingUtilities.invokeLater {
|
||||
// Find the ScrollablePanel in the plugin list view and reset its scroll position
|
||||
val listView = mainPanel.components.find { it.name == PLUGIN_LIST_VIEW }
|
||||
if (listView != null && listView is Container) {
|
||||
findAndResetScrollablePanel(listView)
|
||||
}
|
||||
}
|
||||
cardLayout.show(mainPanel, PLUGIN_LIST_VIEW)
|
||||
}
|
||||
|
||||
val packageName = plugin.javaClass.`package`.name
|
||||
val titleLabel = JLabel("$packageName v${pluginInfo.version}")
|
||||
titleLabel.foreground = secondaryColor
|
||||
titleLabel.font = Font("RuneScape Small", Font.PLAIN, 16)
|
||||
titleLabel.horizontalAlignment = SwingConstants.CENTER
|
||||
|
||||
headerPanel.add(backButton, BorderLayout.WEST)
|
||||
headerPanel.add(titleLabel, BorderLayout.CENTER)
|
||||
|
||||
// Content panel for settings
|
||||
val contentPanel = JPanel()
|
||||
contentPanel.layout = BoxLayout(contentPanel, BoxLayout.Y_AXIS)
|
||||
contentPanel.background = VIEW_BACKGROUND_COLOR
|
||||
contentPanel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10)
|
||||
|
||||
// Add plugin info
|
||||
val infoPanel = JPanel(BorderLayout())
|
||||
infoPanel.background = WIDGET_COLOR
|
||||
infoPanel.border = BorderFactory.createEmptyBorder(10, 10, 10, 10)
|
||||
infoPanel.maximumSize = Dimension(Int.MAX_VALUE, 80)
|
||||
|
||||
val infoText = """
|
||||
Version: ${pluginInfo.version}
|
||||
Author: ${pluginInfo.author}
|
||||
Description: ${pluginInfo.description}
|
||||
""".trimIndent()
|
||||
|
||||
val infoLabel = JLabel("<html>$infoText</html>")
|
||||
infoLabel.foreground = secondaryColor
|
||||
infoLabel.font = Font("RuneScape Small", Font.PLAIN, 14)
|
||||
infoPanel.add(infoLabel, BorderLayout.CENTER)
|
||||
|
||||
contentPanel.add(infoPanel)
|
||||
contentPanel.add(Box.createVerticalStrut(10))
|
||||
|
||||
// Add exposed fields
|
||||
addPluginSettings(contentPanel, plugin)
|
||||
|
||||
// Wrap the content panel in a ScrollablePanel for custom scrolling
|
||||
val scrollablePanel = ScrollablePanel(contentPanel)
|
||||
|
||||
// Reset scroll position to top when entering the edit page
|
||||
SwingUtilities.invokeLater {
|
||||
resetScrollablePanel(scrollablePanel)
|
||||
}
|
||||
|
||||
detailView.add(headerPanel, BorderLayout.NORTH)
|
||||
detailView.add(scrollablePanel, BorderLayout.CENTER)
|
||||
|
||||
// Add the new detail view to the main panel
|
||||
mainPanel.add(detailView, PLUGIN_DETAIL_VIEW)
|
||||
|
||||
// Revalidate and repaint the main panel
|
||||
mainPanel.revalidate()
|
||||
mainPanel.repaint()
|
||||
|
||||
// Show the detail view
|
||||
cardLayout.show(mainPanel, PLUGIN_DETAIL_VIEW)
|
||||
}
|
||||
|
||||
private fun addPluginSettings(contentPanel: JPanel, plugin: Plugin) {
|
||||
val fieldNotifier = Helpers.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()
|
||||
fieldPanel.layout = GridBagLayout()
|
||||
fieldPanel.background = WIDGET_COLOR
|
||||
fieldPanel.foreground = secondaryColor
|
||||
fieldPanel.border = BorderFactory.createEmptyBorder(5, 0, 5, 0)
|
||||
fieldPanel.maximumSize = Dimension(Int.MAX_VALUE, 50)
|
||||
|
||||
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(
|
||||
mainPanel,
|
||||
"${field.name} updated successfully!"
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
showToast(
|
||||
mainPanel,
|
||||
"Failed to update ${field.name}: ${e.message}",
|
||||
JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fieldPanel.add(applyButton, gbc)
|
||||
contentPanel.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()) {
|
||||
contentPanel.add(Box.createVerticalStrut(5))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addPlugins(reflectiveEditorView: JPanel) {
|
||||
// Ensure we run on the EDT; if not, reschedule and return
|
||||
if (!SwingUtilities.isEventDispatchThread()) {
|
||||
SwingUtilities.invokeLater { addPlugins(reflectiveEditorView) }
|
||||
return
|
||||
}
|
||||
|
||||
// Batch updates to avoid intermediate repaints/flicker
|
||||
mainPanel.ignoreRepaint = true
|
||||
try {
|
||||
// Remove the existing plugin list view if present
|
||||
val existingListView = mainPanel.components.find { it.name == PLUGIN_LIST_VIEW }
|
||||
if (existingListView != null) {
|
||||
mainPanel.remove(existingListView)
|
||||
}
|
||||
|
||||
// Create a new plugin list view off-EDT (already on EDT here) and add it
|
||||
val pluginListView = createPluginListView()
|
||||
pluginListView.name = PLUGIN_LIST_VIEW
|
||||
mainPanel.add(pluginListView, PLUGIN_LIST_VIEW)
|
||||
|
||||
// Switch card after the new component is in place
|
||||
cardLayout.show(mainPanel, PLUGIN_LIST_VIEW)
|
||||
|
||||
// Revalidate/repaint once at the end
|
||||
mainPanel.revalidate()
|
||||
mainPanel.repaint()
|
||||
} finally {
|
||||
mainPanel.ignoreRepaint = false
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Helper method to reset a ScrollablePanel to the top
|
||||
private fun resetScrollablePanel(scrollablePanel: ScrollablePanel) {
|
||||
// Access the content panel and reset its position
|
||||
// Since we can't directly access the private fields, we'll use reflection
|
||||
try {
|
||||
val contentField = ScrollablePanel::class.java.getDeclaredField("content")
|
||||
contentField.isAccessible = true
|
||||
val contentPanel = contentField.get(scrollablePanel) as JPanel
|
||||
|
||||
// Reset the location to the top
|
||||
contentPanel.setLocation(0, 0)
|
||||
|
||||
// Force a repaint
|
||||
scrollablePanel.revalidate()
|
||||
scrollablePanel.repaint()
|
||||
} catch (e: Exception) {
|
||||
// If reflection fails, at least try to repaint
|
||||
scrollablePanel.revalidate()
|
||||
scrollablePanel.repaint()
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to find and reset ScrollablePanel components within a container
|
||||
private fun findAndResetScrollablePanel(container: Component) {
|
||||
if (container is ScrollablePanel) {
|
||||
resetScrollablePanel(container)
|
||||
} else if (container is Container) {
|
||||
for (child in container.components) {
|
||||
findAndResetScrollablePanel(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom border for rounded components
|
||||
class RoundedBorder(radius: Int) : AbstractBorder() {
|
||||
private val radius = radius
|
||||
|
||||
override fun paintBorder(c: Component, g: Graphics, x: Int, y: Int, width: Int, height: Int) {
|
||||
val g2 = g as Graphics2D
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
|
||||
g2.setColor(c.background)
|
||||
g2.fillRoundRect(x, y, width - 1, height - 1, radius, radius)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue