From aecffd8f71de33e99ccb77fdb75e1a7d402d89be Mon Sep 17 00:00:00 2001 From: Owain van Brakel Date: Tue, 4 Feb 2020 05:42:26 +0100 Subject: [PATCH] client: Externals rework --- buildSrc/src/main/kotlin/Dependencies.kt | 4 + runelite-client/runelite-client.gradle.kts | 20 +- .../java/net/runelite/client/RuneLite.java | 40 +- .../runelite/client/RuneLiteProperties.java | 8 + .../client/config/OpenOSRSConfig.java | 23 +- .../ExternalPluginChanged.java} | 29 +- .../client/events/ExternalPluginsLoaded.java | 32 + .../client/menus/BankComparableEntry.java | 49 ++ .../client/menus/ComparableEntries.java | 1 - .../menus/EquipmentComparableEntry.java | 31 + .../menus/InventoryComparableEntry.java | 36 + .../client/menus/ShopComparableEntry.java | 45 + .../client/menus/WithdrawComparableEntry.java | 82 ++ .../client/plugins/ExternalPluginManager.java | 521 +++++++++++ .../net/runelite/client/plugins/Plugin.java | 69 +- .../runelite/client/plugins/PluginType.java | 4 +- .../ChatboxPerformancePlugin.java | 37 +- .../client/plugins/config/ConfigPanel.java | 4 +- .../plugins/config/PluginListPanel.java | 10 + .../client/plugins/info/InfoConfig.java | 87 -- .../client/plugins/info/InfoPanel.java | 67 +- .../client/plugins/info/InfoPlugin.java | 43 - .../plugins/openosrs/OpenOSRSPlugin.java | 189 +--- .../openosrs/externals/ExternalBox.java | 97 ++ .../externals/PluginManagerPanel.java | 831 ++++++++++++++++++ .../openosrs/externals/RadioButtonPanel.java | 56 ++ .../util/DeferredDocumentChangedListener.java | 58 ++ .../net/runelite/client/util/SwingUtil.java | 14 + .../src/main/resources/open.osrs.properties | 1 + 29 files changed, 2058 insertions(+), 430 deletions(-) rename runelite-client/src/main/java/net/runelite/client/{plugins/chatboxperformance/ChatboxPerformanceConfig.java => events/ExternalPluginChanged.java} (70%) create mode 100644 runelite-client/src/main/java/net/runelite/client/events/ExternalPluginsLoaded.java create mode 100644 runelite-client/src/main/java/net/runelite/client/menus/BankComparableEntry.java create mode 100644 runelite-client/src/main/java/net/runelite/client/menus/EquipmentComparableEntry.java create mode 100644 runelite-client/src/main/java/net/runelite/client/menus/InventoryComparableEntry.java create mode 100644 runelite-client/src/main/java/net/runelite/client/menus/ShopComparableEntry.java create mode 100644 runelite-client/src/main/java/net/runelite/client/menus/WithdrawComparableEntry.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/ExternalPluginManager.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/info/InfoConfig.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/openosrs/externals/ExternalBox.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/openosrs/externals/PluginManagerPanel.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/openosrs/externals/RadioButtonPanel.java create mode 100644 runelite-client/src/main/java/net/runelite/client/util/DeferredDocumentChangedListener.java diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index feebecafa7..d37a1a3e81 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -80,6 +80,8 @@ object Libraries { const val okhttp3 = "4.3.0" const val orangeExtensions = "1.0" const val petitparser = "2.3.1" + const val pf4j = "3.2.0" + const val pf4jUpdate = "2.2.0" const val plexus = "3.3.0" const val radiance = "2.5.1" const val rxjava = "2.2.16" @@ -139,6 +141,8 @@ object Libraries { const val okhttp3Webserver = "com.squareup.okhttp3:mockwebserver:${Versions.okhttp3}" const val orangeExtensions = "net.runelite:orange-extensions:${Versions.orangeExtensions}" const val petitparser = "com.github.petitparser:java-petitparser:${Versions.petitparser}" + const val pf4j = "org.pf4j:pf4j:${Versions.pf4j}" + const val pf4jUpdate = "org.pf4j:pf4j-update:${Versions.pf4jUpdate}" const val plexus = "org.codehaus.plexus:plexus-utils:${Versions.plexus}" const val rxjava = "io.reactivex.rxjava2:rxjava:${Versions.rxjava}" const val rxrelay = "com.jakewharton.rxrelay2:rxrelay:${Versions.rxrelay}" diff --git a/runelite-client/runelite-client.gradle.kts b/runelite-client/runelite-client.gradle.kts index 438eddf519..d1a9ad75ad 100644 --- a/runelite-client/runelite-client.gradle.kts +++ b/runelite-client/runelite-client.gradle.kts @@ -25,7 +25,7 @@ import org.apache.tools.ant.filters.ReplaceTokens import java.text.SimpleDateFormat -import java.util.* +import java.util.Date plugins { id(Plugins.shadow.first) version Plugins.shadow.second @@ -39,6 +39,7 @@ description = "RuneLite Client" dependencies { annotationProcessor(Libraries.lombok) + annotationProcessor(Libraries.pf4j) compileOnly(Libraries.javax) compileOnly(Libraries.lombok) @@ -58,7 +59,6 @@ dependencies { implementation(Libraries.substance) implementation(Libraries.jopt) implementation(Libraries.apacheCommonsText) - implementation(Libraries.plexus) implementation(Libraries.annotations) implementation(Libraries.jogampGluegen) implementation(Libraries.jogampJogl) @@ -67,6 +67,10 @@ dependencies { implementation(Libraries.jooqMeta) implementation(Libraries.sentry) implementation(Libraries.slf4jApi) + implementation(Libraries.pf4j) { + exclude(group = "org.slf4j") + } + implementation(Libraries.pf4jUpdate) implementation(project(":http-api")) api(project(":runelite-api")) implementation(Libraries.naturalMouse) @@ -108,6 +112,14 @@ fun launcherVersion(): String { return "-1" } +fun pluginPath(): String { + if (project.hasProperty("pluginPath")) { + print(project.property("pluginPath").toString()) + return project.property("pluginPath").toString() + } + return "" +} + tasks { build { finalizedBy("shadowJar") @@ -123,7 +135,8 @@ tasks { "rs.version" to ProjectVersions.rsversion.toString(), "open.osrs.version" to ProjectVersions.openosrsVersion, "open.osrs.builddate" to formatDate(Date()), - "launcher.version" to launcherVersion() + "launcher.version" to launcherVersion(), + "plugin.path" to pluginPath() ) inputs.properties(tokens) @@ -151,7 +164,6 @@ tasks { group = "openosrs" } - register("RuneLite.main()") { group = "openosrs" diff --git a/runelite-client/src/main/java/net/runelite/client/RuneLite.java b/runelite-client/src/main/java/net/runelite/client/RuneLite.java index 488c2520ce..fe2a9f41c2 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLite.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLite.java @@ -69,6 +69,7 @@ import net.runelite.client.game.XpDropManager; import net.runelite.client.game.chatbox.ChatboxPanelManager; import net.runelite.client.graphics.ModelOutlineRenderer; import net.runelite.client.menus.MenuManager; +import net.runelite.client.plugins.ExternalPluginManager; import net.runelite.client.plugins.PluginManager; import net.runelite.client.rs.ClientLoader; import net.runelite.client.rs.ClientUpdateCheckMode; @@ -93,10 +94,13 @@ import org.slf4j.LoggerFactory; @Slf4j public class RuneLite { + public static final String SYSTEM_VERSION = "0.0.1"; + public static final File RUNELITE_DIR = new File(System.getProperty("user.home"), ".runelite"); public static final File CACHE_DIR = new File(RUNELITE_DIR, "cache"); public static final File PROFILES_DIR = new File(RUNELITE_DIR, "profiles"); public static final File PLUGIN_DIR = new File(RUNELITE_DIR, "plugins"); + public static final File EXTERNALPLUGIN_DIR = new File(RUNELITE_DIR, "externalmanager"); public static final File SCREENSHOT_DIR = new File(RUNELITE_DIR, "screenshots"); public static final File LOGS_DIR = new File(RUNELITE_DIR, "logs"); public static final File PLUGINS_DIR = new File(RUNELITE_DIR, "plugins"); @@ -105,16 +109,24 @@ public class RuneLite @Getter private static Injector injector; + @Inject public DiscordService discordService; + @Inject private WorldService worldService; @Inject private PluginManager pluginManager; + + @Inject + private ExternalPluginManager externalPluginManager; + @Inject private ConfigManager configManager; + @Inject private SessionManager sessionManager; + @Inject private ClientSessionManager clientSessionManager; @@ -351,25 +363,34 @@ public class RuneLite // Tell the plugin manager if client is outdated or not pluginManager.setOutdated(isOutdated); - // Load external plugins - pluginManager.loadExternalPlugins(); + // Initialize UI + RuneLiteSplashScreen.stage(.60, "Initialize UI"); + clientUI.init(this); // Load the plugins, but does not start them yet. // This will initialize configuration pluginManager.loadCorePlugins(); - RuneLiteSplashScreen.stage(.70, "Finalizing configuration"); + + // Load external plugins + externalPluginManager.startExternalPluginManager(); + + RuneLiteSplashScreen.stage(.75, "Finalizing configuration"); // Plugins have provided their config, so set default config // to main settings pluginManager.loadDefaultPluginConfiguration(); - // Start client session - RuneLiteSplashScreen.stage(.75, "Starting core interface"); - clientSessionManager.start(); + externalPluginManager.startExternalUpdateManager(); - // Initialize UI - RuneLiteSplashScreen.stage(.80, "Initialize UI"); - clientUI.init(this); + RuneLiteSplashScreen.stage(.77, "Updating external plugins"); + externalPluginManager.update(); + + // Load external plugins + pluginManager.loadExternalPlugins(); + + // Start client session + RuneLiteSplashScreen.stage(.80, "Starting core interface"); + clientSessionManager.start(); //Set the world if specified via CLI args - will not work until clientUI.init is called Optional worldArg = Optional.ofNullable(System.getProperty("cli.world")).map(Integer::parseInt); @@ -409,6 +430,7 @@ public class RuneLite // Start plugins pluginManager.startCorePlugins(); + externalPluginManager.loadPlugins(); // Register additional schedulers if (this.client != null) diff --git a/runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java b/runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java index 17c8c3d1ef..cc0629f25f 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java @@ -42,6 +42,7 @@ public class RuneLiteProperties private static final String WIKI_LINK = "runelite.wiki.link"; private static final String PATREON_LINK = "runelite.patreon.link"; private static final String LAUNCHER_VERSION_PROPERTY = "launcher.version"; + private static final String PLUGIN_PATH = "plugin.path"; private static final String TROUBLESHOOTING_LINK = "runelite.wiki.troubleshooting.link"; private static final String BUILDING_LINK = "runelite.wiki.building.link"; private static final String DNS_CHANGE_LINK = "runelite.dnschange.link"; @@ -137,4 +138,11 @@ public class RuneLiteProperties String launcherVersion = properties.getProperty(LAUNCHER_VERSION_PROPERTY); return launcherVersion.equals("-1") ? null : launcherVersion; } + + @Nullable + public static String getPluginPath() + { + String pluginPath = properties.getProperty(PLUGIN_PATH); + return pluginPath.equals("") ? null : pluginPath; + } } \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/config/OpenOSRSConfig.java b/runelite-client/src/main/java/net/runelite/client/config/OpenOSRSConfig.java index 50803d3751..1d63c87fc7 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/OpenOSRSConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/config/OpenOSRSConfig.java @@ -281,8 +281,8 @@ public interface OpenOSRSConfig extends Config @ConfigItem( keyName = "enablePlugins", - name = "Enable loading of external plugins", - description = "Enable loading of external plugins", + name = "Enable loading of legacy external plugins", + description = "Enable loading of legacy external plugins", position = 16, titleSection = "externalPluginsTitle" ) @@ -364,4 +364,23 @@ public interface OpenOSRSConfig extends Config { return Keybind.NOT_SET; } + + @ConfigItem( + keyName = "externalRepositories", + name = "", + description = "", + hidden = true + ) + default String getExternalRepositories() + { + return "OpenOSRS:https://raw.githubusercontent.com/open-osrs/plugin-hosting/master/"; + } + + @ConfigItem( + keyName = "externalRepositories", + name = "", + description = "", + hidden = true + ) + void setExternalRepositories(String val); } \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/chatboxperformance/ChatboxPerformanceConfig.java b/runelite-client/src/main/java/net/runelite/client/events/ExternalPluginChanged.java similarity index 70% rename from runelite-client/src/main/java/net/runelite/client/plugins/chatboxperformance/ChatboxPerformanceConfig.java rename to runelite-client/src/main/java/net/runelite/client/events/ExternalPluginChanged.java index 58b487b252..6961813bc4 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/chatboxperformance/ChatboxPerformanceConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/events/ExternalPluginChanged.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, Alexander V. + * Copyright (c) 2019 Owain van Brakel * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -22,23 +22,16 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package net.runelite.client.plugins.chatboxperformance; +package net.runelite.client.events; -import net.runelite.client.config.Config; -import net.runelite.client.config.ConfigGroup; -import net.runelite.client.config.ConfigItem; +import lombok.Data; +import net.runelite.api.events.Event; +import net.runelite.client.plugins.Plugin; -@ConfigGroup("chatboxperformance") -public interface ChatboxPerformanceConfig extends Config +@Data +public class ExternalPluginChanged implements Event { - @ConfigItem( - position = 1, - keyName = "Chatbox", - name = "Toggle gradient", - description = "Toggles the gradient inside the chatbox." - ) - default boolean transparentChatBox() - { - return true; //default enabled, just like in game. - } -} \ No newline at end of file + private final String pluginId; + private final Plugin plugin; + private final boolean added; +} diff --git a/runelite-client/src/main/java/net/runelite/client/events/ExternalPluginsLoaded.java b/runelite-client/src/main/java/net/runelite/client/events/ExternalPluginsLoaded.java new file mode 100644 index 0000000000..a674165438 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/events/ExternalPluginsLoaded.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019 Owain van Brakel + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.events; + +import lombok.Data; +import net.runelite.api.events.Event; + +@Data +public class ExternalPluginsLoaded implements Event +{} diff --git a/runelite-client/src/main/java/net/runelite/client/menus/BankComparableEntry.java b/runelite-client/src/main/java/net/runelite/client/menus/BankComparableEntry.java new file mode 100644 index 0000000000..f65f1508fd --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/menus/BankComparableEntry.java @@ -0,0 +1,49 @@ +package net.runelite.client.menus; + +import lombok.EqualsAndHashCode; +import net.runelite.api.MenuEntry; +import net.runelite.api.util.Text; +import net.runelite.api.widgets.WidgetID; +import net.runelite.api.widgets.WidgetInfo; +import org.apache.commons.lang3.StringUtils; + +@EqualsAndHashCode(callSuper = true) +public class BankComparableEntry extends AbstractComparableEntry +{ + public BankComparableEntry(String option, String itemName, boolean strictTarget) + { + this.setOption(option); + this.setTarget(Text.standardize(itemName)); + this.setStrictTarget(strictTarget); + } + + public boolean matches(MenuEntry entry) + { + if (isNotBankWidget(entry.getParam1())) + { + return false; + } + + if (isStrictTarget() && !Text.standardize(entry.getTarget()).equals(this.getTarget())) + { + return false; + } + + return StringUtils.containsIgnoreCase(entry.getOption(), this.getOption()) && Text.standardize(entry.getTarget()).contains(this.getTarget()); + } + + @Override + public int getPriority() + { + return 100; + } + + static boolean isNotBankWidget(int widgetID) + { + final int groupId = WidgetInfo.TO_GROUP(widgetID); + + return groupId != WidgetID.BANK_GROUP_ID + && groupId != WidgetID.BANK_INVENTORY_GROUP_ID + && groupId != WidgetID.GRAND_EXCHANGE_GROUP_ID; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/menus/ComparableEntries.java b/runelite-client/src/main/java/net/runelite/client/menus/ComparableEntries.java index f3e324f283..a20ee70020 100644 --- a/runelite-client/src/main/java/net/runelite/client/menus/ComparableEntries.java +++ b/runelite-client/src/main/java/net/runelite/client/menus/ComparableEntries.java @@ -1,7 +1,6 @@ package net.runelite.client.menus; import net.runelite.api.Client; -import net.runelite.client.plugins.menuentryswapper.comparables.BankComparableEntry; public interface ComparableEntries { diff --git a/runelite-client/src/main/java/net/runelite/client/menus/EquipmentComparableEntry.java b/runelite-client/src/main/java/net/runelite/client/menus/EquipmentComparableEntry.java new file mode 100644 index 0000000000..8d4118a6a8 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/menus/EquipmentComparableEntry.java @@ -0,0 +1,31 @@ +package net.runelite.client.menus; + +import lombok.EqualsAndHashCode; +import net.runelite.api.MenuEntry; +import net.runelite.api.util.Text; +import net.runelite.api.widgets.WidgetID; +import net.runelite.api.widgets.WidgetInfo; +import org.apache.commons.lang3.StringUtils; + +@EqualsAndHashCode(callSuper = true) +public class EquipmentComparableEntry extends AbstractComparableEntry +{ + public EquipmentComparableEntry(String option, String itemName) + { + this.setOption(option); + this.setTarget(Text.standardize(itemName)); + } + + public boolean matches(MenuEntry entry) + { + final int groupId = WidgetInfo.TO_GROUP(entry.getParam1()); + + if (groupId != WidgetID.EQUIPMENT_GROUP_ID) + { + return false; + } + + return StringUtils.equalsIgnoreCase(entry.getOption(), this.getOption()) + && Text.standardize(entry.getTarget()).contains(this.getTarget()); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/menus/InventoryComparableEntry.java b/runelite-client/src/main/java/net/runelite/client/menus/InventoryComparableEntry.java new file mode 100644 index 0000000000..f299afdd6e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/menus/InventoryComparableEntry.java @@ -0,0 +1,36 @@ +package net.runelite.client.menus; + +import lombok.EqualsAndHashCode; +import net.runelite.api.MenuEntry; +import net.runelite.api.util.Text; +import net.runelite.api.widgets.WidgetID; +import net.runelite.api.widgets.WidgetInfo; +import org.apache.commons.lang3.StringUtils; + +@EqualsAndHashCode(callSuper = true) +public class InventoryComparableEntry extends AbstractComparableEntry +{ + public InventoryComparableEntry(String option, String itemName, boolean strictTarget) + { + this.setOption(option); + this.setTarget(Text.standardize(itemName)); + this.setStrictTarget(strictTarget); + } + + public boolean matches(MenuEntry entry) + { + final int groupId = WidgetInfo.TO_GROUP(entry.getParam1()); + + if (groupId != WidgetID.INVENTORY_GROUP_ID) + { + return false; + } + + if (isStrictTarget() && Text.standardize(entry.getTarget()).equals(this.getTarget())) + { + return false; + } + + return StringUtils.containsIgnoreCase(entry.getOption(), this.getOption()) && Text.standardize(entry.getTarget()).contains(this.getTarget()); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/menus/ShopComparableEntry.java b/runelite-client/src/main/java/net/runelite/client/menus/ShopComparableEntry.java new file mode 100644 index 0000000000..6f022433c4 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/menus/ShopComparableEntry.java @@ -0,0 +1,45 @@ +package net.runelite.client.menus; + +import java.util.List; +import net.runelite.api.MenuEntry; +import net.runelite.api.util.Text; + +public class ShopComparableEntry extends AbstractComparableEntry +{ + private ShopComparableEntry(final boolean buy, final int amount, final String item) + { + assert amount == 1 || amount == 5 || amount == 10 || amount == 50 : "Only 1, 5, 10, or 50 are valid amounts"; + + this.setOption((buy ? "buy " : "sell ") + amount); + this.setTarget(Text.standardize(item)); + } + + @Override + public boolean matches(final MenuEntry entry) + { + return Text.standardize(entry.getOption()).equals(this.getOption()) && Text.standardize(entry.getTarget()).equals(this.getTarget()); + } + + @Override + public int getPriority() + { + return 100; + } + + @Override + public boolean equals(Object other) + { + return other instanceof ShopComparableEntry && super.equals(other); + } + + /** + * Fills the array with ShopComparableEntries, getting the items from the fed list + */ + public static void populateArray(final AbstractComparableEntry[] array, final List items, final boolean buy, final int amount) + { + for (int i = 0; i < array.length; i++) + { + array[i] = new ShopComparableEntry(buy, amount, items.get(i)); + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/menus/WithdrawComparableEntry.java b/runelite-client/src/main/java/net/runelite/client/menus/WithdrawComparableEntry.java new file mode 100644 index 0000000000..8c4cc8b98b --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/menus/WithdrawComparableEntry.java @@ -0,0 +1,82 @@ +package net.runelite.client.menus; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import net.runelite.api.MenuEntry; +import net.runelite.api.util.Text; + +@EqualsAndHashCode(callSuper = true) +public class WithdrawComparableEntry extends AbstractComparableEntry +{ + private static String x; + + private final Amount amount; + + private WithdrawComparableEntry(Amount amount, String item) + { + this.amount = amount; + this.setTarget(Text.standardize(item)); + } + + @Override + public boolean matches(MenuEntry entry) + { + if (BankComparableEntry.isNotBankWidget(entry.getParam1())) + { + return false; + } + + final String option = entry.getOption(); + + if (!option.startsWith("Withdraw") && !option.startsWith("Deposit")) + { + return false; + } + + if (amount == Amount.X) + { + if (!option.endsWith(x)) + { + return false; + } + } + else if (!option.endsWith(amount.suffix)) + { + return false; + } + + return Text.standardize(entry.getTarget()).contains(this.getTarget()); + } + + @Override + public int getPriority() + { + return 10; + } + + public static void setX(int amount) + { + x = String.valueOf(amount); + } + + public static void populateArray(AbstractComparableEntry[] array, List items, Amount amount) + { + for (int i = 0; i < array.length; i++) + { + array[i] = new WithdrawComparableEntry(amount, items.get(i)); + } + } + + @AllArgsConstructor + public enum Amount + { + ONE("1"), + FIVE("5"), + TEN("10"), + X(null), + ALL("All"); + + private String suffix; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/ExternalPluginManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/ExternalPluginManager.java new file mode 100644 index 0000000000..2ba0d47371 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/ExternalPluginManager.java @@ -0,0 +1,521 @@ +package net.runelite.client.plugins; + +import com.google.common.collect.ImmutableList; +import com.google.inject.Binder; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import java.io.File; +import java.lang.reflect.InvocationTargetException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.swing.JOptionPane; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.RuneLite; +import static net.runelite.client.RuneLite.EXTERNALPLUGIN_DIR; +import static net.runelite.client.RuneLite.SYSTEM_VERSION; +import net.runelite.client.RuneLiteProperties; +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.config.OpenOSRSConfig; +import net.runelite.client.eventbus.EventBus; +import net.runelite.client.events.ExternalPluginChanged; +import net.runelite.client.events.ExternalPluginsLoaded; +import net.runelite.client.ui.RuneLiteSplashScreen; +import net.runelite.client.util.SwingUtil; +import org.pf4j.DefaultPluginManager; +import org.pf4j.DependencyResolver; +import org.pf4j.JarPluginLoader; +import org.pf4j.JarPluginRepository; +import org.pf4j.ManifestPluginDescriptorFinder; +import org.pf4j.PluginDependency; +import org.pf4j.PluginDescriptorFinder; +import org.pf4j.PluginLoader; +import org.pf4j.PluginRepository; +import org.pf4j.PluginWrapper; +import org.pf4j.RuntimeMode; +import org.pf4j.update.DefaultUpdateRepository; +import org.pf4j.update.PluginInfo; +import org.pf4j.update.UpdateManager; +import org.pf4j.update.UpdateRepository; +import org.pf4j.update.VerifyException; + +@Slf4j +@Singleton +public +class ExternalPluginManager +{ + private final PluginManager runelitePluginManager; + private final org.pf4j.PluginManager externalPluginManager; + + @Getter(AccessLevel.PUBLIC) + private final List repositories = new ArrayList<>(); + private final OpenOSRSConfig openOSRSConfig; + private final ConfigManager configManager; + private final EventBus eventBus; + + @Getter(AccessLevel.PUBLIC) + private UpdateManager updateManager; + + @Inject + public ExternalPluginManager( + PluginManager pluginManager, + OpenOSRSConfig openOSRSConfig, + ConfigManager configManager, + EventBus eventBus) + { + this.runelitePluginManager = pluginManager; + this.openOSRSConfig = openOSRSConfig; + this.configManager = configManager; + this.eventBus = eventBus; + + //noinspection ResultOfMethodCallIgnored + EXTERNALPLUGIN_DIR.mkdirs(); + + boolean debug = RuneLiteProperties.getLauncherVersion() == null && RuneLiteProperties.getPluginPath() != null; + + this.externalPluginManager = new DefaultPluginManager(debug ? Paths.get(RuneLiteProperties.getPluginPath() + File.separator + "release") : EXTERNALPLUGIN_DIR.toPath()) + { + @Override + protected PluginDescriptorFinder createPluginDescriptorFinder() + { + return new ManifestPluginDescriptorFinder(); + } + + @Override + protected PluginRepository createPluginRepository() + { + return new JarPluginRepository(getPluginsRoot()); + } + + @Override + protected PluginLoader createPluginLoader() + { + return new JarPluginLoader(this); + } + + @Override + public RuntimeMode getRuntimeMode() + { + return debug ? RuntimeMode.DEVELOPMENT : RuntimeMode.DEPLOYMENT; + } + }; + this.externalPluginManager.setSystemVersion(SYSTEM_VERSION); + } + + private static URL toRepositoryUrl(String owner, String name) throws MalformedURLException + { + return new URL("https://raw.githubusercontent.com/" + owner + "/" + name + "/master/"); + } + + public static boolean testRepository(String owner, String name) + { + final List repositories = new ArrayList<>(); + try + { + repositories.add(new DefaultUpdateRepository("github", new URL("https://raw.githubusercontent.com/" + owner + "/" + name + "/master/"))); + } + catch (MalformedURLException e) + { + return true; + } + DefaultPluginManager testPluginManager = new DefaultPluginManager(EXTERNALPLUGIN_DIR.toPath()); + UpdateManager updateManager = new UpdateManager(testPluginManager, repositories); + + return updateManager.getPlugins().size() <= 0; + } + + public static Predicate not(Predicate t) + { + return t.negate(); + } + + public void startExternalPluginManager() + { + this.externalPluginManager.loadPlugins(); + } + + public void startExternalUpdateManager() + { + try + { + for (String keyval : openOSRSConfig.getExternalRepositories().split(";")) + { + String[] repository = keyval.split(":", 2); + repositories.add(new DefaultUpdateRepository(repository[0], new URL(repository[1]))); + } + } + catch (MalformedURLException e) + { + e.printStackTrace(); + } + + this.updateManager = new UpdateManager(this.externalPluginManager, repositories); + } + + public void addRepository(String owner, String name) + { + try + { + DefaultUpdateRepository respository = new DefaultUpdateRepository(owner, toRepositoryUrl(owner, name)); + updateManager.addRepository(respository); + saveConfig(); + } + catch (MalformedURLException e) + { + log.error("Repostitory could not be added"); + } + } + + public void removeRepository(String owner) + { + updateManager.removeRepository(owner); + saveConfig(); + } + + private void saveConfig() + { + StringBuilder config = new StringBuilder(); + + for (UpdateRepository repository : updateManager.getRepositories()) + { + config.append(repository.getId()); + config.append(":"); + config.append(repository.getUrl().toString()); + config.append(";"); + } + config.deleteCharAt(config.lastIndexOf(";")); + + openOSRSConfig.setExternalRepositories(config.toString()); + } + + private void instantiatePlugin(String pluginId, Plugin plugin) throws PluginInstantiationException + { + List scannedPlugins = new ArrayList<>(runelitePluginManager.getPlugins()); + Class clazz = plugin.getClass(); + + PluginDescriptor[] pluginDescriptors = clazz.getAnnotationsByType(PluginDescriptor.class); + + for (PluginDescriptor pluginDescriptor : pluginDescriptors) + { + if (pluginDescriptor.type() == PluginType.EXTERNAL) + { + log.error("Class {} is using the the new external plugin loader, it should not use PluginType.EXTERNAL", clazz); + return; + } + } + + net.runelite.client.plugins.PluginDependency[] pluginDependencies = clazz.getAnnotationsByType(net.runelite.client.plugins.PluginDependency.class); + List deps = new ArrayList<>(); + for (net.runelite.client.plugins.PluginDependency pluginDependency : pluginDependencies) + { + Optional dependency = scannedPlugins.stream().filter(p -> p.getClass() == pluginDependency.value()).findFirst(); + if (dependency.isEmpty()) + { + throw new PluginInstantiationException("Unmet dependency for " + clazz.getSimpleName() + ": " + pluginDependency.value().getSimpleName()); + } + deps.add(dependency.get()); + } + + Module pluginModule = (Binder binder) -> + { + //noinspection unchecked + binder.bind((Class) plugin.getClass()).toInstance(plugin); + binder.install(plugin); + + for (Plugin p : deps) + { + Module p2 = (Binder binder2) -> + { + //noinspection unchecked + binder2.bind((Class) p.getClass()).toInstance(p); + binder2.install(p); + }; + binder.install(p2); + } + }; + Injector pluginInjector = RuneLite.getInjector().createChildInjector(pluginModule); + pluginInjector.injectMembers(plugin); + plugin.injector = pluginInjector; + + // Initialize default configuration + Injector injector = plugin.getInjector(); + + for (Key key : injector.getAllBindings().keySet()) + { + Class type = key.getTypeLiteral().getRawType(); + if (Config.class.isAssignableFrom(type)) + { + Config config = (Config) injector.getInstance(key); + configManager.setDefaultConfiguration(config, false); + } + } + + try + { + runelitePluginManager.startPlugin(plugin); + } + catch (PluginInstantiationException ex) + { + log.warn("unable to start plugin", ex); + return; + } + + runelitePluginManager.add(plugin); + eventBus.post(ExternalPluginChanged.class, new ExternalPluginChanged(pluginId, plugin, true)); + } + + public void loadPlugins() + { + this.externalPluginManager.startPlugins(); + List startedPlugins = getStartedPlugins(); + int index = 1; + + for (PluginWrapper plugin : startedPlugins) + { + RuneLiteSplashScreen.stage(.90, 1, "Starting external plugins", index++, startedPlugins.size()); + loadPlugin(plugin.getPluginId()); + } + + eventBus.post(ExternalPluginsLoaded.class, new ExternalPluginsLoaded()); + } + + private void loadPlugin(String pluginId) + { + try + { + List extensions = externalPluginManager.getExtensions(Plugin.class, pluginId); + for (Plugin plugin : extensions) + { + try + { + instantiatePlugin(pluginId, plugin); + } + catch (PluginInstantiationException e) + { + log.warn("Error instantiating plugin!", e); + return; + } + } + } + catch (NoClassDefFoundError ex) + { + try + { + SwingUtil.syncExec(() -> + JOptionPane.showMessageDialog(null, + pluginId + " could not be loaded due to the following error: " + ex.getMessage(), + "External plugin error", + JOptionPane.ERROR_MESSAGE)); + } + catch (InvocationTargetException | InterruptedException ignored) + { + } + } + } + + private void stopPlugins() + { + List startedPlugins = ImmutableList.copyOf(getStartedPlugins()); + + for (PluginWrapper pluginWrapper : startedPlugins) + { + String pluginId = pluginWrapper.getDescriptor().getPluginId(); + List extensions = externalPluginManager.getExtensions(Plugin.class, pluginId); + + for (Plugin plugin : runelitePluginManager.getPlugins()) + { + if (!extensions.get(0).getClass().getName().equals(plugin.getClass().getName())) + { + continue; + } + + try + { + runelitePluginManager.stopPlugin(plugin); + runelitePluginManager.remove(plugin); + + eventBus.post(ExternalPluginChanged.class, new ExternalPluginChanged(pluginId, plugin, false)); + } + catch (PluginInstantiationException ex) + { + log.warn("unable to stop plugin", ex); + return; + } + } + } + } + + private Path stopPlugin(String pluginId) + { + List startedPlugins = ImmutableList.copyOf(getStartedPlugins()); + + for (PluginWrapper pluginWrapper : startedPlugins) + { + if (!pluginId.equals(pluginWrapper.getDescriptor().getPluginId())) + { + continue; + } + + List extensions = externalPluginManager.getExtensions(Plugin.class, pluginId); + + for (Plugin plugin : runelitePluginManager.getPlugins()) + { + if (!extensions.get(0).getClass().getName().equals(plugin.getClass().getName())) + { + continue; + } + + try + { + runelitePluginManager.stopPlugin(plugin); + runelitePluginManager.remove(plugin); + + eventBus.post(ExternalPluginChanged.class, new ExternalPluginChanged(pluginId, plugin, false)); + + return pluginWrapper.getPluginPath(); + } + catch (PluginInstantiationException ex) + { + log.warn("unable to stop plugin", ex); + return null; + } + } + } + + return null; + } + + public void install(String pluginId) throws VerifyException + { + + if (getDisabledPlugins().contains(pluginId)) + { + this.externalPluginManager.enablePlugin(pluginId); + this.externalPluginManager.startPlugin(pluginId); + + loadPlugin(pluginId); + + return; + } + + if (getStartedPlugins().stream().anyMatch(ev -> ev.getPluginId().equals(pluginId))) + { + return; + } + + // Null version returns the last release version of this plugin for given system version + try + { + PluginInfo.PluginRelease latest = updateManager.getLastPluginRelease(pluginId); + + if (latest == null) + { + try + { + SwingUtil.syncExec(() -> + JOptionPane.showMessageDialog(null, + pluginId + " is outdated and cannot be installed", + "Installation error", + JOptionPane.ERROR_MESSAGE)); + } + catch (InvocationTargetException | InterruptedException ignored) + { + } + + return; + } + + updateManager.installPlugin(pluginId, null); + + loadPlugin(pluginId); + } + catch (DependencyResolver.DependenciesNotFoundException ex) + { + uninstall(pluginId); + + for (String dep : ex.getDependencies()) + { + install(dep); + } + + install(pluginId); + } + + } + + public void uninstall(String pluginId) + { + Path pluginPath = stopPlugin(pluginId); + + if (pluginPath == null) + { + return; + } + + externalPluginManager.stopPlugin(pluginId); + externalPluginManager.disablePlugin(pluginId); + } + + public void update() + { + if (updateManager.hasUpdates()) + { + List updates = updateManager.getUpdates(); + for (PluginInfo plugin : updates) + { + PluginInfo.PluginRelease lastRelease = updateManager.getLastPluginRelease(plugin.id); + String lastVersion = lastRelease.version; + boolean updated = updateManager.updatePlugin(plugin.id, lastVersion); + + if (!updated) + { + log.warn("Cannot update plugin '{}'", plugin.id); + } + } + } + } + + public Set getDependencies() + { + Set deps = new HashSet<>(); + List startedPlugins = getStartedPlugins(); + + for (PluginWrapper pluginWrapper : startedPlugins) + { + for (PluginDependency pluginDependency : pluginWrapper.getDescriptor().getDependencies()) + { + deps.add(pluginDependency.getPluginId()); + } + } + + return deps; + } + + public List getDisabledPlugins() + { + return this.externalPluginManager.getResolvedPlugins() + .stream() + .filter(not(this.externalPluginManager.getStartedPlugins()::contains)) + .map(PluginWrapper::getPluginId) + .collect(Collectors.toList()); + } + + public List getStartedPlugins() + { + return this.externalPluginManager.getStartedPlugins(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/Plugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/Plugin.java index fddb364e9e..d785f241b3 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/Plugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/Plugin.java @@ -24,23 +24,26 @@ */ package net.runelite.client.plugins; +import com.google.common.collect.ImmutableSet; import com.google.inject.Binder; import com.google.inject.Injector; import com.google.inject.Module; -import io.reactivex.Observable; -import io.reactivex.schedulers.Schedulers; -import java.lang.invoke.MethodHandles; -import java.util.Collection; +import io.reactivex.functions.Consumer; +import java.lang.reflect.Method; import java.util.Set; import lombok.AccessLevel; import lombok.Getter; -import net.runelite.client.eventbus.AccessorGenerator; +import lombok.Value; +import net.runelite.api.events.Event; import net.runelite.client.eventbus.EventBus; -import net.runelite.client.eventbus.Subscription; +import net.runelite.client.eventbus.EventScheduler; +import net.runelite.client.eventbus.Subscribe; +import org.pf4j.ExtensionPoint; -public abstract class Plugin implements Module +public abstract class Plugin implements Module, ExtensionPoint { - private Set annotatedSubscriptions = null; + private final Set annotatedSubscriptions = findSubscriptions(); + private final Object annotatedSubsLock = new Object(); @Getter(AccessLevel.PROTECTED) protected Injector injector; @@ -58,33 +61,53 @@ public abstract class Plugin implements Module { } + @SuppressWarnings("unchecked") final void addAnnotatedSubscriptions(EventBus eventBus) { - if (annotatedSubscriptions == null) - { - Observable.fromCallable(this::findSubscriptions) - .subscribeOn(Schedulers.computation()) - .observeOn(Schedulers.single()) - .subscribe(subs -> addSubs(eventBus, (annotatedSubscriptions = subs))); - } - else - { - addSubs(eventBus, annotatedSubscriptions); - } + annotatedSubscriptions.forEach(sub -> eventBus.subscribe(sub.type, annotatedSubsLock, sub.method, sub.takeUntil, sub.subscribe, sub.observe)); } final void removeAnnotatedSubscriptions(EventBus eventBus) { - eventBus.unregister(this); + eventBus.unregister(annotatedSubsLock); } private Set findSubscriptions() { - return AccessorGenerator.scanSubscribes(MethodHandles.lookup(), this); + ImmutableSet.Builder builder = ImmutableSet.builder(); + + for (Method method : this.getClass().getDeclaredMethods()) + { + Subscribe annotation = method.getAnnotation(Subscribe.class); + if (annotation == null) + { + continue; + } + + assert method.getParameterCount() == 1 : "Methods annotated with @Subscribe should have only one parameter"; + + Class type = method.getParameterTypes()[0]; + + assert Event.class.isAssignableFrom(type) : "Parameters of methods annotated with @Subscribe should implement net.runelite.api.events.Event"; + assert method.getReturnType() == void.class : "Methods annotated with @Subscribe should have a void return type"; + + method.setAccessible(true); + + Subscription sub = new Subscription(type.asSubclass(Event.class), event -> method.invoke(this, event), annotation.takeUntil(), annotation.subscribe(), annotation.observe()); + + builder.add(sub); + } + + return builder.build(); } - private void addSubs(EventBus eventBus, Collection subs) + @Value + private static class Subscription { - subs.forEach(s -> s.subscribe(eventBus, this)); + private final Class type; + private final Consumer method; + private final int takeUntil; + private final EventScheduler subscribe; + private final EventScheduler observe; } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/PluginType.java b/runelite-client/src/main/java/net/runelite/client/plugins/PluginType.java index b25fa53ed5..33a2aeade0 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/PluginType.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/PluginType.java @@ -13,7 +13,7 @@ public enum PluginType SKILLING("Skilling"), UTILITY("Utilities"), MISCELLANEOUS("Miscellaneous"), - EXTERNAL("External"), + EXTERNAL("Legacy External"), IMPORTANT("System"), MINIGAME("Minigame"), GAMEMODE("Gamemode"), @@ -26,4 +26,4 @@ public enum PluginType { return getName(); } -} \ No newline at end of file +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/chatboxperformance/ChatboxPerformancePlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/chatboxperformance/ChatboxPerformancePlugin.java index f2fdbfaa33..76520a191d 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/chatboxperformance/ChatboxPerformancePlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/chatboxperformance/ChatboxPerformancePlugin.java @@ -24,9 +24,7 @@ */ package net.runelite.client.plugins.chatboxperformance; -import com.google.inject.Provides; import javax.inject.Inject; -import javax.inject.Singleton; import net.runelite.api.Client; import net.runelite.api.GameState; import net.runelite.api.ScriptID; @@ -37,18 +35,14 @@ import net.runelite.api.widgets.WidgetPositionMode; import net.runelite.api.widgets.WidgetSizeMode; import net.runelite.api.widgets.WidgetType; import net.runelite.client.callback.ClientThread; -import net.runelite.client.config.ConfigManager; import net.runelite.client.eventbus.Subscribe; -import net.runelite.client.events.ConfigChanged; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; -import net.runelite.client.plugins.PluginType; @PluginDescriptor( name = "Chatbox performance", - type = PluginType.MISCELLANEOUS + hidden = true ) -@Singleton public class ChatboxPerformancePlugin extends Plugin { @Inject @@ -57,24 +51,6 @@ public class ChatboxPerformancePlugin extends Plugin @Inject private ClientThread clientThread; - @Inject - private ChatboxPerformanceConfig config; - - @Subscribe - public void onConfigChanged(ConfigChanged event) - { - if (event.getGroup().equals("chatboxperformance")) - { - fixDarkBackground(); - } - } - - @Provides - ChatboxPerformanceConfig getConfig(ConfigManager configManager) - { - return configManager.getConfig(ChatboxPerformanceConfig.class); - } - @Override public void startUp() { @@ -108,7 +84,7 @@ public class ChatboxPerformancePlugin extends Plugin private void fixDarkBackground() { - int currOpacity = 255; + int currOpacity = 256; int prevY = 0; Widget[] children = client.getWidget(WidgetInfo.CHATBOX_TRANSPARENT_BACKGROUND).getDynamicChildren(); Widget prev = null; @@ -132,10 +108,7 @@ public class ChatboxPerformancePlugin extends Plugin } prevY = w.getRelativeY(); - if (config.transparentChatBox()) - { - currOpacity -= 3; - } + currOpacity -= 3; // Rough number, can't get exactly the same as Jagex because of rounding prev = w; } if (prev != null) @@ -146,7 +119,7 @@ public class ChatboxPerformancePlugin extends Plugin private void fixWhiteLines(boolean upperLine) { - int currOpacity = 255; + int currOpacity = 256; int prevWidth = 0; Widget[] children = client.getWidget(WidgetInfo.CHATBOX_TRANSPARENT_LINES).getDynamicChildren(); Widget prev = null; @@ -184,4 +157,4 @@ public class ChatboxPerformancePlugin extends Plugin prev.setOpacity(currOpacity); } } -} +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPanel.java index 2903bdfcdb..c6ac91c0e8 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPanel.java @@ -1122,9 +1122,7 @@ class ConfigPanel extends PluginPanel if (event.getPlugin() == this.pluginConfig.getPlugin()) { SwingUtilities.invokeLater(() -> - { - pluginToggle.setSelected(event.isLoaded()); - }); + pluginToggle.setSelected(event.isLoaded())); } } } \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListPanel.java index 2e0bf4d49c..35d7b30053 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListPanel.java @@ -72,6 +72,8 @@ import net.runelite.client.config.RuneLiteConfig; import net.runelite.client.eventbus.EventBus; import net.runelite.client.eventbus.Subscribe; import net.runelite.client.events.ConfigChanged; +import net.runelite.client.events.ExternalPluginChanged; +import net.runelite.client.events.ExternalPluginsLoaded; import net.runelite.client.events.PluginChanged; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; @@ -183,6 +185,14 @@ public class PluginListPanel extends PluginPanel } }); + eventBus.subscribe(ExternalPluginsLoaded.class, this, ignored -> { + eventBus.subscribe(ExternalPluginChanged.class, this, ev -> { + SwingUtilities.invokeLater(this::rebuildPluginList); + }); + + SwingUtilities.invokeLater(this::rebuildPluginList); + }); + muxer = new MultiplexingPluginPanel(this); searchBar = new IconTextField(); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/info/InfoConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/info/InfoConfig.java deleted file mode 100644 index d36967a555..0000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/info/InfoConfig.java +++ /dev/null @@ -1,87 +0,0 @@ -package net.runelite.client.plugins.info; - -import net.runelite.client.config.Config; -import net.runelite.client.config.ConfigGroup; -import net.runelite.client.config.ConfigItem; - -@ConfigGroup("info") -public interface InfoConfig extends Config -{ - @ConfigItem( - keyName = "showGithub", - name = "Show the OpenOSRS Github", - description = "Configures if you want to show the OpenOSRS Github or not.", - position = 0 - ) - default boolean showGithub() - { - return true; - } - - @ConfigItem( - keyName = "showLauncher", - name = "Show the Launcher download", - description = "Configures if you want to show the OpenOSRS Launcher download or not.", - position = 1 - ) - default boolean showLauncher() - { - return true; - } - - @ConfigItem( - keyName = "showLogDir", - name = "Show Log Directory", - description = "Configures if you want to show the Log Directory or not.", - position = 2 - ) - default boolean showLogDir() - { - return true; - } - - @ConfigItem( - keyName = "showRuneliteDir", - name = "Show Runelite Directory", - description = "Configures if you want to show the Runelite directory or not.", - position = 3 - ) - default boolean showRuneliteDir() - { - return true; - } - - @ConfigItem( - keyName = "showPluginsDir", - name = "Show Plugins Directory", - description = "Configures if you want to show the Plugins Directory or not.", - position = 4 - ) - default boolean showPluginsDir() - { - return true; - } - - @ConfigItem( - keyName = "showScreenshotsDir", - name = "Show Screenshots Directory", - description = "Configures if you want to show the Screenshots Directory or not.", - position = 5 - ) - default boolean showScreenshotsDir() - { - return true; - } - - @ConfigItem( - keyName = "showPhysicalDir", - name = "Show Physical Locations", - description = "Configures if you want to show the Physical Directory Locations or not.", - position = 6 - ) - default boolean showPhysicalDir() - { - return true; - } - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/info/InfoPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/info/InfoPanel.java index 1fa4377e32..dc090ff573 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/info/InfoPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/info/InfoPanel.java @@ -143,57 +143,36 @@ class InfoPanel extends PluginPanel actionsContainer.add(buildLinkPanel(GITHUB_ICON, "License info", "for distribution", "https://github.com/open-osrs/runelite/blob/master/LICENSE")); actionsContainer.add(buildLinkPanel(PATREON_ICON, "Patreon to support", "the OpenOSRS Devs", RuneLiteProperties.getPatreonLink())); actionsContainer.add(buildLinkPanel(DISCORD_ICON, "Talk to us on our", "Discord Server", "https://discord.gg/OpenOSRS")); - if (plugin.isShowGithub()) - { - actionsContainer.add(buildLinkPanel(GITHUB_ICON, "OpenOSRS Github", "", "https://github.com/open-osrs")); - } - if (plugin.isShowLauncher()) - { - actionsContainer.add(buildLinkPanel(IMPORT_ICON, "Launcher Download", "for the latest launcher", "https://github.com/open-osrs/launcher/releases")); - } - if (plugin.isShowRuneliteDir()) - { - actionsContainer.add(buildLinkPanel(FOLDER_ICON, "Open Runelite Directory", "for your .properties file", RUNELITE_DIR)); - } - if (plugin.isShowLogDir()) - { - actionsContainer.add(buildLinkPanel(FOLDER_ICON, "Open Logs Directory", "for bug reports", LOGS_DIR)); - } - if (plugin.isShowPluginsDir()) - { - actionsContainer.add(buildLinkPanel(FOLDER_ICON, "Open Plugins Directory", "for external plugins", PLUGINS_DIR)); - } - if (plugin.isShowScreenshotsDir()) - { - actionsContainer.add(buildLinkPanel(FOLDER_ICON, "Open Screenshots Directory", "for your screenshots", SCREENSHOT_DIR)); - } + actionsContainer.add(buildLinkPanel(GITHUB_ICON, "OpenOSRS Github", "", "https://github.com/open-osrs")); + actionsContainer.add(buildLinkPanel(IMPORT_ICON, "Launcher Download", "for the latest launcher", "https://github.com/open-osrs/launcher/releases")); + actionsContainer.add(buildLinkPanel(FOLDER_ICON, "Open Runelite Directory", "for your .properties file", RUNELITE_DIR)); + actionsContainer.add(buildLinkPanel(FOLDER_ICON, "Open Logs Directory", "for bug reports", LOGS_DIR)); + actionsContainer.add(buildLinkPanel(FOLDER_ICON, "Open Plugins Directory", "for external plugins", PLUGINS_DIR)); + actionsContainer.add(buildLinkPanel(FOLDER_ICON, "Open Screenshots Directory", "for your screenshots", SCREENSHOT_DIR)); - if (plugin.isShowPhysicalDir()) - { - JPanel pathPanel = new JPanel(); - pathPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - pathPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); - pathPanel.setLayout(new GridLayout(0, 1)); + JPanel pathPanel = new JPanel(); + pathPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); + pathPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); + pathPanel.setLayout(new GridLayout(0, 1)); - JLabel rldirectory = new JLabel(htmlLabel("Runelite Directory: ", RUNELITE_DIRECTORY)); - rldirectory.setFont(smallFont); + JLabel rldirectory = new JLabel(htmlLabel("Runelite Directory: ", RUNELITE_DIRECTORY)); + rldirectory.setFont(smallFont); - JLabel logdirectory = new JLabel(htmlLabel("Log Directory: ", LOG_DIRECTORY)); - logdirectory.setFont(smallFont); + JLabel logdirectory = new JLabel(htmlLabel("Log Directory: ", LOG_DIRECTORY)); + logdirectory.setFont(smallFont); - JLabel pluginsdirectory = new JLabel(htmlLabel("Plugins Directory: ", PLUGINS_DIRECTORY)); - pluginsdirectory.setFont(smallFont); + JLabel pluginsdirectory = new JLabel(htmlLabel("Plugins Directory: ", PLUGINS_DIRECTORY)); + pluginsdirectory.setFont(smallFont); - JLabel screenshotsdirectory = new JLabel(htmlLabel("Screenshot Directory: ", SCREENSHOT_DIRECTORY)); - screenshotsdirectory.setFont(smallFont); + JLabel screenshotsdirectory = new JLabel(htmlLabel("Screenshot Directory: ", SCREENSHOT_DIRECTORY)); + screenshotsdirectory.setFont(smallFont); - pathPanel.add(rldirectory); - pathPanel.add(logdirectory); - pathPanel.add(pluginsdirectory); - pathPanel.add(screenshotsdirectory); + pathPanel.add(rldirectory); + pathPanel.add(logdirectory); + pathPanel.add(pluginsdirectory); + pathPanel.add(screenshotsdirectory); - add(pathPanel, BorderLayout.SOUTH); - } + add(pathPanel, BorderLayout.SOUTH); add(versionPanel, BorderLayout.NORTH); add(actionsContainer, BorderLayout.CENTER); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/info/InfoPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/info/InfoPlugin.java index 6523490be4..3c1780ac2e 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/info/InfoPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/info/InfoPlugin.java @@ -24,13 +24,9 @@ */ package net.runelite.client.plugins.info; -import com.google.inject.Provides; import java.awt.image.BufferedImage; import javax.inject.Inject; import javax.inject.Singleton; -import lombok.AccessLevel; -import lombok.Getter; -import net.runelite.client.config.ConfigManager; import net.runelite.client.eventbus.Subscribe; import net.runelite.client.events.ConfigChanged; import net.runelite.client.plugins.Plugin; @@ -50,35 +46,11 @@ import net.runelite.client.util.ImageUtil; @Singleton public class InfoPlugin extends Plugin { - @Getter(AccessLevel.PACKAGE) - private boolean showLogDir; - @Getter(AccessLevel.PACKAGE) - private boolean showRuneliteDir; - @Getter(AccessLevel.PACKAGE) - private boolean showPluginsDir; - @Getter(AccessLevel.PACKAGE) - private boolean showScreenshotsDir; - @Getter(AccessLevel.PACKAGE) - private boolean showGithub; - @Getter(AccessLevel.PACKAGE) - private boolean showLauncher; - @Getter(AccessLevel.PACKAGE) - private boolean showPhysicalDir; - @Inject private ClientToolbar clientToolbar; - @Inject - private InfoConfig config; - private NavigationButton navButton; - @Provides - InfoConfig provideConfig(ConfigManager configManager) - { - return configManager.getConfig(InfoConfig.class); - } - @Subscribe private void onConfigChanged(ConfigChanged event) { @@ -86,15 +58,11 @@ public class InfoPlugin extends Plugin { return; } - - updateConfig(); } @Override protected void startUp() { - updateConfig(); - InfoPanel panel = injector.getInstance(InfoPanel.class); final BufferedImage icon = ImageUtil.getResourceStreamFromClass(getClass(), "info_icon.png"); @@ -114,15 +82,4 @@ public class InfoPlugin extends Plugin { clientToolbar.removeNavigation(navButton); } - - private void updateConfig() - { - this.showGithub = config.showGithub(); - this.showLauncher = config.showLauncher(); - this.showLogDir = config.showLogDir(); - this.showRuneliteDir = config.showRuneliteDir(); - this.showPluginsDir = config.showPluginsDir(); - this.showScreenshotsDir = config.showScreenshotsDir(); - this.showPhysicalDir = config.showPhysicalDir(); - } } \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/openosrs/OpenOSRSPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/openosrs/OpenOSRSPlugin.java index ab291c7161..69558520b2 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/openosrs/OpenOSRSPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/openosrs/OpenOSRSPlugin.java @@ -27,42 +27,29 @@ package net.runelite.client.plugins.openosrs; import java.awt.event.KeyEvent; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import java.awt.image.BufferedImage; import javax.inject.Inject; import javax.inject.Singleton; import lombok.extern.slf4j.Slf4j; -import net.runelite.api.AnimationID; -import net.runelite.api.ChatMessageType; import net.runelite.api.Client; -import net.runelite.api.GameObject; -import static net.runelite.api.ObjectID.CANNON_BASE; -import net.runelite.api.Player; -import net.runelite.api.Projectile; -import static net.runelite.api.ProjectileID.CANNONBALL; -import static net.runelite.api.ProjectileID.GRANITE_CANNONBALL; import static net.runelite.api.ScriptID.BANK_PIN_OP; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.CannonChanged; -import net.runelite.api.events.CannonPlaced; -import net.runelite.api.events.ChatMessage; -import net.runelite.api.events.GameObjectSpawned; -import net.runelite.api.events.GameTick; -import net.runelite.api.events.ProjectileSpawned; import net.runelite.api.events.ScriptCallbackEvent; import net.runelite.api.widgets.WidgetID; import static net.runelite.api.widgets.WidgetInfo.*; import net.runelite.client.callback.ClientThread; import net.runelite.client.config.Keybind; import net.runelite.client.config.OpenOSRSConfig; -import net.runelite.client.eventbus.EventBus; import net.runelite.client.eventbus.Subscribe; import net.runelite.client.events.ConfigChanged; import net.runelite.client.input.KeyListener; import net.runelite.client.input.KeyManager; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.plugins.openosrs.externals.PluginManagerPanel; +import net.runelite.client.ui.ClientToolbar; +import net.runelite.client.ui.NavigationButton; import net.runelite.client.util.HotkeyListener; +import net.runelite.client.util.ImageUtil; @PluginDescriptor( loadWhenOutdated = true, // prevent users from disabling @@ -88,21 +75,10 @@ public class OpenOSRSPlugin extends Plugin private ClientThread clientThread; @Inject - private EventBus eventBus; + private ClientToolbar clientToolbar; - private static final Pattern NUMBER_PATTERN = Pattern.compile("([0-9]+)"); - private static final int MAX_CBALLS = 30; - private int cballsLeft; - private WorldPoint cannonPosition; - private GameObject cannon; - private boolean cannonPlaced; - private boolean skipProjectileCheckThisTick; + private NavigationButton navButton; - private int entered = -1; - private int enterIdx; - private boolean expectInput; - private boolean detach; - private Keybind keybind; private final HotkeyListener hotkeyListener = new HotkeyListener(() -> this.keybind) { @Override @@ -113,10 +89,27 @@ public class OpenOSRSPlugin extends Plugin client.setOculusOrbNormalSpeed(detach ? 36 : 12); } }; + private int entered = -1; + private int enterIdx; + private boolean expectInput; + private boolean detach; + private Keybind keybind; @Override protected void startUp() { + PluginManagerPanel panel = injector.getInstance(PluginManagerPanel.class); + + final BufferedImage icon = ImageUtil.getResourceStreamFromClass(getClass(), "externalmanager_icon.png"); + + navButton = NavigationButton.builder() + .tooltip("External Plugin Manager") + .icon(icon) + .priority(1) + .panel(panel) + .build(); + clientToolbar.addNavigation(navButton); + entered = -1; enterIdx = 0; expectInput = false; @@ -127,6 +120,8 @@ public class OpenOSRSPlugin extends Plugin @Override protected void shutDown() { + clientToolbar.removeNavigation(navButton); + entered = 0; enterIdx = 0; expectInput = false; @@ -189,136 +184,6 @@ public class OpenOSRSPlugin extends Plugin } } - @Subscribe - private void onChatMessage(ChatMessage event) - { - if (event.getType() != ChatMessageType.SPAM && event.getType() != ChatMessageType.GAMEMESSAGE) - { - return; - } - - if (event.getMessage().equals("You add the furnace.")) - { - cballsLeft = 0; - eventBus.post(CannonPlaced.class, new CannonPlaced(true, cannonPosition, cannon)); - eventBus.post(CannonChanged.class, new CannonChanged(null, cballsLeft)); - cannonPlaced = true; - } - - if (event.getMessage().contains("You pick up the cannon") - || event.getMessage().contains("Your cannon has decayed. Speak to Nulodion to get a new one!")) - { - cballsLeft = 0; - eventBus.post(CannonPlaced.class, new CannonPlaced(false, null, null)); - eventBus.post(CannonChanged.class, new CannonChanged(null, cballsLeft)); - cannonPlaced = false; - } - - if (event.getMessage().startsWith("You load the cannon with")) - { - Matcher m = NUMBER_PATTERN.matcher(event.getMessage()); - if (m.find()) - { - // The cannon will usually refill to MAX_CBALLS, but if the - // player didn't have enough cannonballs in their inventory, - // it could fill up less than that. Filling the cannon to - // cballsLeft + amt is not always accurate though because our - // counter doesn't decrease if the player has been too far away - // from the cannon due to the projectiels not being in memory, - // so our counter can be higher than it is supposed to be. - int amt = Integer.parseInt(m.group()); - if (cballsLeft + amt >= MAX_CBALLS) - { - skipProjectileCheckThisTick = true; - cballsLeft = MAX_CBALLS; - } - else - { - cballsLeft += amt; - } - } - else if (event.getMessage().equals("You load the cannon with one cannonball.")) - { - if (cballsLeft + 1 >= MAX_CBALLS) - { - skipProjectileCheckThisTick = true; - cballsLeft = MAX_CBALLS; - } - else - { - cballsLeft++; - } - } - - eventBus.post(CannonChanged.class, new CannonChanged(null, cballsLeft)); - } - - if (event.getMessage().contains("Your cannon is out of ammo!")) - { - skipProjectileCheckThisTick = true; - - // If the player was out of range of the cannon, some cannonballs - // may have been used without the client knowing, so having this - // extra check is a good idea. - cballsLeft = 0; - - eventBus.post(CannonChanged.class, new CannonChanged(null, cballsLeft)); - } - - if (event.getMessage().startsWith("You unload your cannon and receive Cannonball") - || event.getMessage().startsWith("You unload your cannon and receive Granite cannonball")) - { - skipProjectileCheckThisTick = true; - - cballsLeft = 0; - - eventBus.post(CannonChanged.class, new CannonChanged(null, cballsLeft)); - } - } - - @Subscribe - private void onGameTick(GameTick event) - { - skipProjectileCheckThisTick = false; - } - - @Subscribe - private void onGameObjectSpawned(GameObjectSpawned event) - { - final GameObject gameObject = event.getGameObject(); - - final Player localPlayer = client.getLocalPlayer(); - if (gameObject.getId() == CANNON_BASE && !cannonPlaced && - localPlayer != null && localPlayer.getWorldLocation().distanceTo(gameObject.getWorldLocation()) <= 2 && - localPlayer.getAnimation() == AnimationID.BURYING_BONES) - { - cannonPosition = gameObject.getWorldLocation(); - cannon = gameObject; - } - } - - @Subscribe - private void onProjectileSpawned(ProjectileSpawned event) - { - if (!cannonPlaced) - { - return; - } - - final Projectile projectile = event.getProjectile(); - - if ((projectile.getId() == CANNONBALL || projectile.getId() == GRANITE_CANNONBALL) && cannonPosition != null) - { - final WorldPoint projectileLoc = WorldPoint.fromLocal(client, projectile.getX1(), projectile.getY1(), client.getPlane()); - - if (projectileLoc.equals(cannonPosition) && !skipProjectileCheckThisTick) - { - cballsLeft--; - eventBus.post(CannonChanged.class, new CannonChanged(projectile.getId(), cballsLeft)); - } - } - } - private void handleKey(char c) { if (client.getWidget(WidgetID.BANK_PIN_GROUP_ID, BANK_PIN_INSTRUCTION_TEXT.getChildId()) == null @@ -397,4 +262,4 @@ public class OpenOSRSPlugin extends Plugin { } } -} +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/openosrs/externals/ExternalBox.java b/runelite-client/src/main/java/net/runelite/client/plugins/openosrs/externals/ExternalBox.java new file mode 100644 index 0000000000..51c9e94b90 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/openosrs/externals/ExternalBox.java @@ -0,0 +1,97 @@ +package net.runelite.client.plugins.openosrs.externals; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.net.URL; +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextArea; +import javax.swing.border.CompoundBorder; +import javax.swing.border.EmptyBorder; +import javax.swing.text.DefaultCaret; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.FontManager; +import org.pf4j.update.PluginInfo; + +public class ExternalBox extends JPanel +{ + private static final Font normalFont = FontManager.getRunescapeFont(); + private static final Font smallFont = FontManager.getRunescapeSmallFont(); + + PluginInfo pluginInfo; + JLabel install = new JLabel(); + JMultilineLabel description = new JMultilineLabel(); + + ExternalBox(String name, URL url) + { + this(name, url.toString().replace("https://raw.githubusercontent.com/", "").replace("/master/", "")); + } + + ExternalBox(PluginInfo pluginInfo) + { + this(pluginInfo.name, pluginInfo.description); + } + + ExternalBox(String name, String desc) + { + setLayout(new BorderLayout()); + setBackground(ColorScheme.DARKER_GRAY_COLOR); + + JPanel titleWrapper = new JPanel(new BorderLayout()); + titleWrapper.setBackground(ColorScheme.DARKER_GRAY_COLOR); + titleWrapper.setBorder(new CompoundBorder( + BorderFactory.createMatteBorder(0, 0, 1, 0, ColorScheme.DARK_GRAY_COLOR), + BorderFactory.createLineBorder(ColorScheme.DARKER_GRAY_COLOR) + )); + + JLabel title = new JLabel(); + title.setText(name); + title.setFont(normalFont); + title.setBorder(null); + title.setBackground(ColorScheme.DARKER_GRAY_COLOR); + title.setPreferredSize(new Dimension(0, 24)); + title.setForeground(Color.WHITE); + title.setBorder(new EmptyBorder(0, 8, 0, 0)); + + JPanel titleActions = new JPanel(new BorderLayout(3, 0)); + titleActions.setBorder(new EmptyBorder(0, 0, 0, 8)); + titleActions.setBackground(ColorScheme.DARKER_GRAY_COLOR); + + titleActions.add(install, BorderLayout.EAST); + + titleWrapper.add(title, BorderLayout.CENTER); + titleWrapper.add(titleActions, BorderLayout.EAST); + + description.setText(desc); + description.setFont(smallFont); + description.setDisabledTextColor(Color.WHITE); + description.setBackground(ColorScheme.DARKER_GRAY_COLOR); + + add(titleWrapper, BorderLayout.NORTH); + add(description, BorderLayout.CENTER); + } + + public static class JMultilineLabel extends JTextArea + { + private static final long serialVersionUID = 1L; + + public JMultilineLabel() + { + super(); + setEditable(false); + setCursor(null); + setOpaque(false); + setFocusable(false); + setWrapStyleWord(true); + setLineWrap(true); + setBorder(new EmptyBorder(0, 8, 0, 8)); + setAlignmentY(JLabel.CENTER_ALIGNMENT); + + DefaultCaret caret = (DefaultCaret) getCaret(); + caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE); + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/openosrs/externals/PluginManagerPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/openosrs/externals/PluginManagerPanel.java new file mode 100644 index 0000000000..1e801ec641 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/openosrs/externals/PluginManagerPanel.java @@ -0,0 +1,831 @@ +package net.runelite.client.plugins.openosrs.externals; + +import com.google.gson.JsonSyntaxException; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.image.BufferedImage; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import javax.inject.Inject; +import javax.swing.BoxLayout; +import javax.swing.ImageIcon; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JScrollBar; +import javax.swing.JScrollPane; +import javax.swing.JTextField; +import javax.swing.JToggleButton; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; +import javax.swing.border.EmptyBorder; +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.eventbus.EventBus; +import net.runelite.client.events.ExternalPluginChanged; +import net.runelite.client.events.ExternalPluginsLoaded; +import net.runelite.client.plugins.ExternalPluginManager; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.DynamicGridLayout; +import net.runelite.client.ui.FontManager; +import net.runelite.client.ui.PluginPanel; +import net.runelite.client.ui.components.IconTextField; +import net.runelite.client.ui.components.shadowlabel.JShadowedLabel; +import net.runelite.client.util.DeferredDocumentChangedListener; +import net.runelite.client.util.ImageUtil; +import net.runelite.client.util.SwingUtil; +import org.apache.commons.text.similarity.JaroWinklerDistance; +import org.pf4j.update.PluginInfo; +import org.pf4j.update.UpdateManager; +import org.pf4j.update.UpdateRepository; +import org.pf4j.update.VerifyException; + +@Slf4j +public class PluginManagerPanel extends PluginPanel +{ + private static final JaroWinklerDistance DISTANCE = new JaroWinklerDistance(); + + private static final ImageIcon SECTION_EXPAND_ICON; + private static final ImageIcon SECTION_EXPAND_ICON_HOVER; + private static final ImageIcon SECTION_RETRACT_ICON; + private static final ImageIcon SECTION_RETRACT_ICON_HOVER; + private static final ImageIcon ADD_ICON; + private static final ImageIcon ADD_HOVER_ICON; + private static final ImageIcon DELETE_ICON; + private static final ImageIcon DELETE_HOVER_ICON; + private static final ImageIcon DELETE_ICON_GRAY; + private static final ImageIcon DELETE_HOVER_ICON_GRAY; + + static + { + final BufferedImage backIcon = ImageUtil.getResourceStreamFromClass(PluginManagerPanel.class, "config_back_icon.png"); + final BufferedImage orangeBackIcon = ImageUtil.fillImage(backIcon, ColorScheme.BRAND_BLUE); + + final BufferedImage sectionRetractIcon = ImageUtil.rotateImage(orangeBackIcon, Math.PI * 1.5); + SECTION_RETRACT_ICON = new ImageIcon(sectionRetractIcon); + SECTION_RETRACT_ICON_HOVER = new ImageIcon(ImageUtil.alphaOffset(sectionRetractIcon, -100)); + + final BufferedImage sectionExpandIcon = ImageUtil.rotateImage(orangeBackIcon, Math.PI); + SECTION_EXPAND_ICON = new ImageIcon(sectionExpandIcon); + SECTION_EXPAND_ICON_HOVER = new ImageIcon(ImageUtil.alphaOffset(sectionExpandIcon, -100)); + + final BufferedImage addIcon = + ImageUtil.recolorImage( + ImageUtil.getResourceStreamFromClass(PluginManagerPanel.class, "add_icon.png"), ColorScheme.BRAND_BLUE + ); + ADD_ICON = new ImageIcon(addIcon); + ADD_HOVER_ICON = new ImageIcon(ImageUtil.alphaOffset(addIcon, 0.53f)); + + final BufferedImage deleteImg = + ImageUtil.recolorImage( + ImageUtil.resizeCanvas( + ImageUtil.getResourceStreamFromClass(PluginManagerPanel.class, "delete_icon.png"), 14, 14 + ), ColorScheme.BRAND_BLUE + ); + DELETE_ICON = new ImageIcon(deleteImg); + DELETE_HOVER_ICON = new ImageIcon(ImageUtil.alphaOffset(deleteImg, 0.53f)); + + DELETE_ICON_GRAY = new ImageIcon(ImageUtil.grayscaleImage(deleteImg)); + DELETE_HOVER_ICON_GRAY = new ImageIcon(ImageUtil.alphaOffset(ImageUtil.grayscaleImage(deleteImg), 0.53f)); + } + + private final ExternalPluginManager externalPluginManager; + private final UpdateManager updateManager; + private final ScheduledExecutorService executor; + private final IconTextField searchBar = new IconTextField(); + private final List installedPluginsList = new ArrayList<>(); + private final List availablePluginsList = new ArrayList<>(); + private final JPanel repositoriesPanel = new JPanel(); + private final JPanel installedPluginsPanel = new JPanel(new GridBagLayout()); + private final JPanel availablePluginsPanel = new JPanel(new GridBagLayout()); + private String filterMode = "Available plugins (All)"; + private int scrollBarPosition; + private JScrollBar scrollbar; + private Set deps; + + @Inject + private PluginManagerPanel(ExternalPluginManager externalPluginManager, EventBus eventBus, ScheduledExecutorService executor) + { + super(false); + + this.externalPluginManager = externalPluginManager; + this.updateManager = externalPluginManager.getUpdateManager(); + this.executor = executor; + + eventBus.subscribe(ExternalPluginsLoaded.class, "loading-externals", (e) -> { + eventBus.unregister("loading-externals"); + eventBus.subscribe(ExternalPluginChanged.class, this, this::onExternalPluginChanged); + reloadPlugins(); + }); + + DeferredDocumentChangedListener listener = new DeferredDocumentChangedListener(); + listener.addChangeListener(e -> + onSearchBarChanged()); + + searchBar.setIcon(IconTextField.Icon.SEARCH); + searchBar.setPreferredSize(new Dimension(PluginPanel.PANEL_WIDTH - 20, 30)); + searchBar.setBackground(ColorScheme.DARKER_GRAY_COLOR); + searchBar.setHoverBackgroundColor(ColorScheme.DARK_GRAY_HOVER_COLOR); + searchBar.getDocument().addDocumentListener(listener); + + buildPanel(); + } + + private static boolean mismatchesSearchTerms(String search, PluginInfo pluginInfo) + { + final String[] searchTerms = search.toLowerCase().split(" "); + final String[] pluginTerms = (pluginInfo.name + " " + pluginInfo.description).toLowerCase().split("[/\\s]"); + for (String term : searchTerms) + { + if (Arrays.stream(pluginTerms).noneMatch((t) -> t.contains(term) || + DISTANCE.apply(t, term) > 0.9)) + { + return true; + } + } + return false; + } + + private JPanel addSection(String name, JPanel sectionContent) + { + final JPanel section = new JPanel(); + section.setLayout(new BoxLayout(section, BoxLayout.Y_AXIS)); + + JPanel item = new JPanel(); + item.setLayout(new BorderLayout()); + + JLabel headerLabel = new JLabel(name); + headerLabel.setFont(FontManager.getRunescapeFont()); + headerLabel.setForeground(ColorScheme.BRAND_BLUE); + + final JToggleButton collapse = new JToggleButton(SECTION_EXPAND_ICON); + + SwingUtil.removeButtonDecorations(collapse); + collapse.setRolloverIcon(SECTION_EXPAND_ICON_HOVER); + collapse.setSelectedIcon(SECTION_RETRACT_ICON); + collapse.setRolloverSelectedIcon(SECTION_RETRACT_ICON_HOVER); + collapse.setToolTipText("Retract"); + collapse.setPreferredSize(new Dimension(20, 20)); + collapse.setFont(collapse.getFont().deriveFont(16.0f)); + collapse.setBorder(null); + collapse.setMargin(new Insets(0, 0, 0, 0)); + headerLabel.setBorder(new EmptyBorder(0, 6, 0, 0)); + + item.add(collapse, BorderLayout.WEST); + item.add(headerLabel, BorderLayout.CENTER); + + final JPanel sectionContents = new JPanel(); + sectionContents.setLayout(new DynamicGridLayout(0, 1, 0, 5)); + sectionContents.setBorder(new EmptyBorder(6, 5, 0, 0)); + section.add(item, BorderLayout.NORTH); + section.add(sectionContents, BorderLayout.SOUTH); + + sectionContents.add(sectionContent); + + final MouseAdapter adapter = new MouseAdapter() + { + @Override + public void mouseClicked(MouseEvent e) + { + toggleSection(collapse, sectionContents); + } + }; + collapse.addActionListener(e -> toggleSection(collapse, sectionContents)); + headerLabel.addMouseListener(adapter); + + return section; + } + + private void toggleSection(JToggleButton button, JPanel contents) + { + boolean newState = !contents.isVisible(); + button.setSelected(newState); + contents.setVisible(newState); + button.setToolTipText(newState ? "Retract" : "Expand"); + SwingUtilities.invokeLater(() -> + { + contents.revalidate(); + contents.repaint(); + }); + } + + private JPanel filterPanel() + { + JPanel filterPanel = new JPanel(); + filterPanel.setLayout(new BorderLayout(0, 5)); + filterPanel.setBorder(new EmptyBorder(0, 10, 10, 10)); + + JRadioButton repositories = new JRadioButton("Repositories"); + repositories.setSelected(filterMode.equals("Repositories")); + JRadioButton plugins = new JRadioButton("Plugins"); + plugins.setSelected(filterMode.contains("plugins")); + + JRadioButton available = new JRadioButton("Available"); + available.setSelected(filterMode.contains("Available")); + JRadioButton installed = new JRadioButton("Installed"); + installed.setSelected(filterMode.contains("Installed")); + + List updateRepositories = externalPluginManager.getRepositories(); + List authors = new ArrayList<>(); + JRadioButton allPlugins = new JRadioButton("All"); + allPlugins.setSelected(filterMode.contains("All")); + + authors.add(allPlugins); + for (UpdateRepository repository : updateRepositories) + { + JRadioButton author = new JRadioButton(repository.getId()); + author.setSelected(filterMode.contains(repository.getId())); + + author.addActionListener(ev -> { + filterMode = filterMode.contains("Installed") ? "Installed plugins (" + repository.getId() + ")" : "Available plugins (" + repository.getId() + ")"; + onSearchBarChanged(); + buildPanel(); + }); + + authors.add(author); + } + + repositories.addActionListener(ev -> { + filterMode = "Repositories"; + buildPanel(); + }); + + plugins.addActionListener(ev -> { + filterMode = "Available plugins (All)"; + onSearchBarChanged(); + buildPanel(); + }); + + available.addActionListener(ev -> { + filterMode = "Available plugins (All)"; + onSearchBarChanged(); + buildPanel(); + }); + + installed.addActionListener(ev -> { + filterMode = "Installed plugins (All)"; + onSearchBarChanged(); + buildPanel(); + }); + + allPlugins.addActionListener(ev -> { + filterMode = filterMode.contains("Installed") ? "Installed plugins (All)" : "Available plugins (All)"; + onSearchBarChanged(); + buildPanel(); + }); + + RadioButtonPanel mainRadioPanel = new RadioButtonPanel("Show", repositories, plugins); + RadioButtonPanel pluginRadioPanel = new RadioButtonPanel("Plugins", available, installed); + RadioButtonPanel authorRadioPanel = new RadioButtonPanel("Author", authors.toArray(new JRadioButton[0])); + + filterPanel.add(mainRadioPanel, BorderLayout.NORTH); + + if (!filterMode.equals("Repositories")) + { + filterPanel.add(pluginRadioPanel, BorderLayout.CENTER); + } + if (!filterMode.equals("Repositories") && updateRepositories.size() > 1) + { + filterPanel.add(authorRadioPanel, BorderLayout.SOUTH); + } + + return filterPanel; + } + + private void buildPanel() + { + removeAll(); + + setLayout(new BorderLayout(0, 10)); + setBackground(ColorScheme.DARK_GRAY_COLOR); + + add(titleBar(), BorderLayout.NORTH); + add(wrapContainer(getContentPanels()), BorderLayout.CENTER); + + revalidate(); + repaint(); + } + + private JLabel titleLabel(String text) + { + JLabel title = new JShadowedLabel(); + + title.setFont(FontManager.getRunescapeSmallFont()); + title.setForeground(Color.WHITE); + title.setHorizontalAlignment(SwingConstants.CENTER); + title.setText("" + text + ""); + + return title; + } + + private JPanel titleBar() + { + JPanel titlePanel = new JPanel(new BorderLayout()); + titlePanel.setBorder(new EmptyBorder(10, 10, 10, 10)); + + JLabel title = new JLabel(); + JLabel addRepo = new JLabel(ADD_ICON); + + title.setText("External Plugin Manager"); + title.setForeground(Color.WHITE); + + addRepo.setToolTipText("Add new repository"); + addRepo.addMouseListener(new MouseAdapter() + { + @Override + public void mousePressed(MouseEvent mouseEvent) + { + JTextField owner = new JTextField(); + JTextField name = new JTextField(); + Object[] message = { + "Repository owner:", owner, + "Repository name:", name + }; + + int option = JOptionPane.showConfirmDialog(null, message, "Add repository", JOptionPane.OK_CANCEL_OPTION); + if (option != JOptionPane.OK_OPTION || owner.getText().equals("") || name.getText().equals("")) + { + return; + } + + if (ExternalPluginManager.testRepository(owner.getText(), name.getText())) + { + JOptionPane.showMessageDialog(null, "This doesn't appear to be a valid repository.", "Error!", JOptionPane.ERROR_MESSAGE); + return; + } + + externalPluginManager.addRepository(owner.getText(), name.getText()); + + repositories(); + reloadPlugins(); + buildPanel(); + } + + @Override + public void mouseEntered(MouseEvent mouseEvent) + { + addRepo.setIcon(ADD_HOVER_ICON); + } + + @Override + public void mouseExited(MouseEvent mouseEvent) + { + addRepo.setIcon(ADD_ICON); + } + }); + + titlePanel.add(title, BorderLayout.WEST); + titlePanel.add(addRepo, BorderLayout.EAST); + + return titlePanel; + } + + // Wrap the panel inside a scroll pane + private JScrollPane wrapContainer(final JPanel container) + { + final JPanel wrapped = new JPanel(new BorderLayout()); + wrapped.add(container, BorderLayout.NORTH); + + final JScrollPane scroller = new JScrollPane(wrapped); + scroller.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + scroller.getVerticalScrollBar().setPreferredSize(new Dimension(8, 0)); + + this.scrollbar = scroller.getVerticalScrollBar(); + + return scroller; + } + + private void onExternalPluginChanged(ExternalPluginChanged externalPluginChanged) + { + String pluginId = externalPluginChanged.getPluginId(); + Optional externalBox; + + if (externalPluginChanged.isAdded()) + { + externalBox = Arrays.stream( + availablePluginsPanel.getComponents() + ).filter(extBox -> + extBox instanceof ExternalBox && ((ExternalBox) extBox).pluginInfo.id.equals(pluginId) + ).findFirst(); + } + else + { + externalBox = Arrays.stream( + installedPluginsPanel.getComponents() + ).filter(extBox -> + extBox instanceof ExternalBox && ((ExternalBox) extBox).pluginInfo.id.equals(pluginId) + ).findFirst(); + } + + if (externalBox.isEmpty()) + { + log.info("EXTERNALBOX IS EMPTY: {}", pluginId); + return; + } + + ExternalBox extBox = (ExternalBox) externalBox.get(); + deps = externalPluginManager.getDependencies(); + + try + { + SwingUtil.syncExec(() -> + { + if (externalPluginChanged.isAdded()) + { + availablePluginsPanel.remove(externalBox.get()); + availablePluginsList.remove(extBox.pluginInfo); + + installedPluginsList.add(extBox.pluginInfo); + installedPluginsList.sort(Comparator.naturalOrder()); + + installedPlugins(); + + pluginInstallButton(extBox.install, extBox.pluginInfo, true, deps.contains(extBox.pluginInfo.id)); + } + else + { + installedPluginsPanel.remove(externalBox.get()); + installedPluginsList.remove(extBox.pluginInfo); + + availablePluginsList.add(extBox.pluginInfo); + availablePluginsList.sort(Comparator.naturalOrder()); + + availablePlugins(); + + pluginInstallButton(extBox.install, extBox.pluginInfo, false, false); + } + }); + } + catch (InvocationTargetException | InterruptedException e) + { + e.printStackTrace(); + } + } + + private void reloadPlugins() + { + fetchPlugins(); + + try + { + SwingUtil.syncExec(() -> { + this.installedPlugins(); + this.availablePlugins(); + + resetScrollValue(); + }); + + } + catch (InvocationTargetException | InterruptedException e) + { + e.printStackTrace(); + } + + resetScrollValue(); + } + + private void onSearchBarChanged() + { + if (filterMode.contains("Installed plugins")) + { + installedPlugins(); + } + else if (filterMode.contains("Available plugins")) + { + availablePlugins(); + } + } + + private JPanel getContentPanels() + { + JPanel contentPanel = new JPanel(); + contentPanel.setLayout(new GridBagLayout()); + GridBagConstraints c = new GridBagConstraints(); + + c.fill = GridBagConstraints.HORIZONTAL; + c.weightx = 1; + c.gridx = 0; + c.gridy = 0; + c.insets = new Insets(5, 0, 5, 0); + + contentPanel.add(addSection("Filter", filterPanel()), c); + + if (filterMode.equals("Repositories")) + { + c.gridy++; + contentPanel.add(repositoriesPanel(), c); + } + else if (filterMode.contains("Installed plugins")) + { + c.gridy++; + contentPanel.add(installedPluginsPanel(), c); + } + else if (filterMode.contains("Available plugins")) + { + c.gridy++; + contentPanel.add(availablePluginsPanel(), c); + } + + return contentPanel; + } + + private JPanel repositoriesPanel() + { + JPanel installedRepositoriesPanel = new JPanel(); + installedRepositoriesPanel.setLayout(new BorderLayout(0, 5)); + installedRepositoriesPanel.setBorder(new EmptyBorder(0, 10, 10, 10)); + installedRepositoriesPanel.add(titleLabel("Repositories"), BorderLayout.NORTH); + installedRepositoriesPanel.add(repositoriesPanel, BorderLayout.CENTER); + + repositories(); + + return installedRepositoriesPanel; + } + + private JPanel installedPluginsPanel() + { + JPanel installedPluginsContainer = new JPanel(); + installedPluginsContainer.setLayout(new BorderLayout(0, 5)); + installedPluginsContainer.setBorder(new EmptyBorder(0, 10, 10, 10)); + installedPluginsContainer.add(titleLabel(filterMode.replace(" (All)", "")), BorderLayout.NORTH); + installedPluginsContainer.add(searchBar, BorderLayout.CENTER); + installedPluginsContainer.add(installedPluginsPanel, BorderLayout.SOUTH); + + return installedPluginsContainer; + } + + private JPanel availablePluginsPanel() + { + JPanel availablePluginsContainer = new JPanel(); + availablePluginsContainer.setLayout(new BorderLayout(0, 5)); + availablePluginsContainer.setBorder(new EmptyBorder(0, 10, 10, 10)); + availablePluginsContainer.add(titleLabel(filterMode.replace(" (All)", "")), BorderLayout.NORTH); + availablePluginsContainer.add(searchBar, BorderLayout.CENTER); + availablePluginsContainer.add(availablePluginsPanel, BorderLayout.SOUTH); + + return availablePluginsContainer; + } + + private void repositories() + { + repositoriesPanel.removeAll(); + repositoriesPanel.setLayout(new GridBagLayout()); + GridBagConstraints c = new GridBagConstraints(); + + for (UpdateRepository repository : externalPluginManager.getRepositories()) + { + String name = repository.getId(); + ExternalBox repositoryBox = new ExternalBox(name, repository.getUrl()); + + c.fill = GridBagConstraints.HORIZONTAL; + c.weightx = 1.0; + c.gridy += 1; + c.insets = new Insets(5, 0, 0, 0); + + repositoriesPanel.add(repositoryBox, c); + + if (name.equals("OpenOSRS")) + { + repositoryBox.install.setVisible(false); + continue; + } + + repositoryBox.install.setIcon(DELETE_ICON); + repositoryBox.install.setToolTipText("Remove"); + repositoryBox.install.addMouseListener(new MouseAdapter() + { + @Override + public void mousePressed(MouseEvent e) + { + externalPluginManager.removeRepository(name); + + repositories(); + reloadPlugins(); + } + + @Override + public void mouseEntered(MouseEvent e) + { + repositoryBox.install.setIcon(DELETE_HOVER_ICON); + } + + @Override + public void mouseExited(MouseEvent e) + { + repositoryBox.install.setIcon(DELETE_ICON); + } + }); + } + } + + private void fetchPlugins() + { + List availablePlugins = null; + List plugins = null; + List disabledPlugins = externalPluginManager.getDisabledPlugins(); + + try + { + availablePlugins = updateManager.getAvailablePlugins(); + plugins = updateManager.getPlugins(); + } + catch (JsonSyntaxException ex) + { + log.error(String.valueOf(ex)); + } + + if (availablePlugins == null || plugins == null) + { + JOptionPane.showMessageDialog(null, "The external plugin list could not be loaded.", "Error", JOptionPane.ERROR_MESSAGE); + return; + } + + availablePluginsList.clear(); + installedPluginsList.clear(); + deps = externalPluginManager.getDependencies(); + + for (PluginInfo pluginInfo : plugins) + { + if (availablePlugins.contains(pluginInfo) || disabledPlugins.contains(pluginInfo.id)) + { + availablePluginsList.add(pluginInfo); + } + else + { + installedPluginsList.add(pluginInfo); + } + } + } + + private void installedPlugins() + { + JPanel panel = new JPanel(); + panel.setLayout(new GridBagLayout()); + GridBagConstraints c = new GridBagConstraints(); + + installedPluginsPanel.removeAll(); + String search = searchBar.getText(); + + for (PluginInfo pluginInfo : installedPluginsList) + { + + if ((!search.equals("") && mismatchesSearchTerms(search, pluginInfo)) || + (!filterMode.contains("All") && !filterMode.contains(pluginInfo.getRepositoryId()))) + { + continue; + } + + ExternalBox pluginBox = new ExternalBox(pluginInfo); + pluginBox.pluginInfo = pluginInfo; + + c.fill = GridBagConstraints.HORIZONTAL; + c.weightx = 1.0; + c.gridy += 1; + c.insets = new Insets(5, 0, 0, 0); + + pluginInstallButton(pluginBox.install, pluginInfo, true, deps.contains(pluginInfo.id)); + installedPluginsPanel.add(pluginBox, c); + } + + if (installedPluginsPanel.getComponents().length < 1) + { + installedPluginsPanel.add(titleLabel("No plugins found")); + } + } + + private void availablePlugins() + { + JPanel panel = new JPanel(); + panel.setLayout(new GridBagLayout()); + GridBagConstraints c = new GridBagConstraints(); + + availablePluginsPanel.removeAll(); + String search = searchBar.getText(); + + for (PluginInfo pluginInfo : availablePluginsList) + { + if ((!search.equals("") && mismatchesSearchTerms(search, pluginInfo)) || + (!filterMode.contains("All") && !filterMode.contains(pluginInfo.getRepositoryId()))) + { + continue; + } + + ExternalBox pluginBox = new ExternalBox(pluginInfo); + pluginBox.pluginInfo = pluginInfo; + + c.fill = GridBagConstraints.HORIZONTAL; + c.weightx = 1.0; + c.gridy += 1; + c.insets = new Insets(5, 0, 0, 0); + + pluginInstallButton(pluginBox.install, pluginInfo, false, false); + availablePluginsPanel.add(pluginBox, c); + } + + if (availablePluginsPanel.getComponents().length < 1) + { + availablePluginsPanel.add(titleLabel("No plugins found")); + } + } + + private void pluginInstallButton(JLabel install, PluginInfo pluginInfo, boolean installed, boolean hideAction) + { + install.setIcon(installed ? hideAction ? DELETE_ICON_GRAY : DELETE_ICON : ADD_ICON); + if (!hideAction) + { + install.setToolTipText(installed ? "Uninstall" : "Install"); + } + install.addMouseListener(new MouseAdapter() + { + @Override + public void mousePressed(MouseEvent e) + { + saveScrollValue(); + + if (installed) + { + if (hideAction) + { + JOptionPane.showMessageDialog(null, "This plugin can't be uninstalled because one or more other plugins have a dependency on it.", "Error!", JOptionPane.ERROR_MESSAGE); + } + else + { + install.setIcon(null); + install.setText("Uninstalling"); + executor.submit(() -> externalPluginManager.uninstall(pluginInfo.id)); + } + } + else + { + install.setIcon(null); + install.setText("Installing"); + executor.submit(() -> installPlugin(pluginInfo)); + } + } + + @Override + public void mouseEntered(MouseEvent e) + { + if (install.getText().toLowerCase().contains("installing")) + { + return; + } + + install.setIcon(installed ? hideAction ? DELETE_HOVER_ICON_GRAY : DELETE_HOVER_ICON : ADD_HOVER_ICON); + } + + @Override + public void mouseExited(MouseEvent e) + { + if (install.getText().toLowerCase().contains("installing")) + { + return; + } + + install.setIcon(installed ? hideAction ? DELETE_ICON_GRAY : DELETE_ICON : ADD_ICON); + } + }); + } + + private void installPlugin(PluginInfo pluginInfo) + { + try + { + externalPluginManager.install(pluginInfo.id); + } + catch (VerifyException ex) + { + try + { + SwingUtil.syncExec(() -> + JOptionPane.showMessageDialog(null, pluginInfo.name + " could not be installed, the hash could not be verified.", "Error!", JOptionPane.ERROR_MESSAGE)); + } + catch (InvocationTargetException | InterruptedException ignored) + { + } + } + } + + private void saveScrollValue() + { + scrollBarPosition = scrollbar.getValue(); + } + + private void resetScrollValue() + { + scrollbar.setValue(scrollBarPosition); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/openosrs/externals/RadioButtonPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/openosrs/externals/RadioButtonPanel.java new file mode 100644 index 0000000000..27c4ac3b97 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/openosrs/externals/RadioButtonPanel.java @@ -0,0 +1,56 @@ +package net.runelite.client.plugins.openosrs.externals; + +import java.awt.FlowLayout; +import java.awt.GridLayout; +import javax.swing.BorderFactory; +import javax.swing.ButtonGroup; +import javax.swing.JPanel; +import javax.swing.JRadioButton; + +public class RadioButtonPanel extends JPanel +{ + public static final Orientation VERTICAL = Orientation.VERTICAL; + public static final Orientation HORIZONTAL = Orientation.HORIZONTAL; + + private ButtonGroup buttonGroup = new ButtonGroup(); + + public RadioButtonPanel(String title, JRadioButton... buttons) + { + this(VERTICAL, title, buttons); + } + + public RadioButtonPanel(Orientation orientation, String title, JRadioButton... buttons) + { + if (orientation == VERTICAL) + { + this.setLayout(new GridLayout(buttons.length, 1)); + } + else + { + this.setLayout(new FlowLayout(FlowLayout.LEADING)); + } + + for (JRadioButton button : buttons) + { + buttonGroup.add(button); + this.add(button); + } + + if (title != null) + { + this.setBorder(BorderFactory.createTitledBorder( + BorderFactory.createEtchedBorder(), title)); + } + } + + public void clearSelection() + { + buttonGroup.clearSelection(); + } + + private enum Orientation + { + VERTICAL, + HORIZONTAL + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/util/DeferredDocumentChangedListener.java b/runelite-client/src/main/java/net/runelite/client/util/DeferredDocumentChangedListener.java new file mode 100644 index 0000000000..f8f321a1a6 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/util/DeferredDocumentChangedListener.java @@ -0,0 +1,58 @@ +package net.runelite.client.util; + +import java.util.ArrayList; +import java.util.List; +import javax.swing.Timer; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +public class DeferredDocumentChangedListener implements DocumentListener +{ + private final Timer timer; + private final List listeners; + + public DeferredDocumentChangedListener() + { + listeners = new ArrayList<>(25); + timer = new Timer(200, e -> fireStateChanged()); + timer.setRepeats(false); + } + + public void addChangeListener(ChangeListener listener) + { + listeners.add(listener); + } + + private void fireStateChanged() + { + if (!listeners.isEmpty()) + { + ChangeEvent evt = new ChangeEvent(this); + for (ChangeListener listener : listeners) + { + listener.stateChanged(evt); + } + } + } + + @Override + public void insertUpdate(DocumentEvent e) + { + timer.restart(); + } + + @Override + public void removeUpdate(DocumentEvent e) + { + timer.restart(); + } + + @Override + public void changedUpdate(DocumentEvent e) + { + timer.restart(); + } + +} diff --git a/runelite-client/src/main/java/net/runelite/client/util/SwingUtil.java b/runelite-client/src/main/java/net/runelite/client/util/SwingUtil.java index ac257aa0da..5f1974c02b 100644 --- a/runelite-client/src/main/java/net/runelite/client/util/SwingUtil.java +++ b/runelite-client/src/main/java/net/runelite/client/util/SwingUtil.java @@ -29,6 +29,7 @@ import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; +import java.awt.EventQueue; import java.awt.Font; import java.awt.Frame; import java.awt.Image; @@ -40,6 +41,7 @@ import java.awt.event.MouseEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.image.BufferedImage; +import java.lang.reflect.InvocationTargetException; import java.util.Enumeration; import java.util.concurrent.Callable; import java.util.function.BiConsumer; @@ -472,4 +474,16 @@ public class SwingUtil { button.addItemListener(l -> button.setToolTipText(button.isSelected() ? on : off)); } + + public static void syncExec(final Runnable r) throws InvocationTargetException, InterruptedException + { + if (EventQueue.isDispatchThread()) + { + r.run(); + } + else + { + EventQueue.invokeAndWait(r); + } + } } diff --git a/runelite-client/src/main/resources/open.osrs.properties b/runelite-client/src/main/resources/open.osrs.properties index 946d2db797..5d933b5010 100644 --- a/runelite-client/src/main/resources/open.osrs.properties +++ b/runelite-client/src/main/resources/open.osrs.properties @@ -12,3 +12,4 @@ runelite.wiki.troubleshooting.link=https://github.com/open-osrs/runelite/wiki/Tr runelite.wiki.building.link=https://github.com/open-osrs/runelite/wiki/Building-with-IntelliJ-IDEA runelite.dnschange.link=https://1.1.1.1/dns/ launcher.version=@launcher.version@ +plugin.path=@plugin.path@