From 4148b5e4a849b03616364b766dbfda181a015f1e Mon Sep 17 00:00:00 2001 From: Max Weber Date: Fri, 22 Nov 2019 09:14:12 -0700 Subject: [PATCH 1/7] config: Refactor config panel into separate panels for each logical view --- .../client/plugins/config/ConfigPanel.java | 388 ++++-------------- .../client/plugins/config/ConfigPlugin.java | 39 +- .../plugins/config/FixedWidthPanel.java | 39 ++ .../client/plugins/config/HotkeyButton.java | 2 +- .../config/PluginConfigurationDescriptor.java | 69 ++++ .../client/plugins/config/PluginListItem.java | 213 +++------- .../plugins/config/PluginListPanel.java | 356 ++++++++++++++++ .../plugins/config/PluginToggleButton.java | 62 +++ .../client/ui/MultiplexingPluginPanel.java | 134 ++++++ .../net/runelite/client/ui/PluginPanel.java | 6 +- .../net/runelite/client/util/SwingUtil.java | 16 + 11 files changed, 844 insertions(+), 480 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/config/FixedWidthPanel.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/config/PluginConfigurationDescriptor.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListPanel.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/config/PluginToggleButton.java create mode 100644 runelite-client/src/main/java/net/runelite/client/ui/MultiplexingPluginPanel.java 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 bd12d73470..4ed399b78a 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 @@ -36,12 +36,7 @@ import java.awt.event.ItemEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.concurrent.ScheduledExecutorService; -import java.util.stream.Collectors; +import javax.inject.Inject; import javax.swing.BorderFactory; import javax.swing.ImageIcon; import javax.swing.JButton; @@ -61,66 +56,53 @@ import javax.swing.SpinnerNumberModel; import javax.swing.SwingUtilities; import javax.swing.border.EmptyBorder; import javax.swing.event.ChangeListener; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; import javax.swing.text.JTextComponent; import lombok.extern.slf4j.Slf4j; -import net.runelite.client.config.ChatColorConfig; -import net.runelite.client.config.Config; import net.runelite.client.config.ConfigDescriptor; -import net.runelite.client.config.ConfigGroup; import net.runelite.client.config.ConfigItem; import net.runelite.client.config.ConfigItemDescriptor; import net.runelite.client.config.ConfigManager; import net.runelite.client.config.Keybind; import net.runelite.client.config.ModifierlessKeybind; import net.runelite.client.config.Range; -import net.runelite.client.config.RuneLiteConfig; -import net.runelite.client.plugins.Plugin; -import net.runelite.client.plugins.PluginDescriptor; -import net.runelite.client.plugins.PluginInstantiationException; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.PluginChanged; import net.runelite.client.plugins.PluginManager; import net.runelite.client.ui.ColorScheme; import net.runelite.client.ui.DynamicGridLayout; import net.runelite.client.ui.PluginPanel; import net.runelite.client.ui.components.ComboBoxListRenderer; -import net.runelite.client.ui.components.IconButton; -import net.runelite.client.ui.components.IconTextField; import net.runelite.client.ui.components.colorpicker.ColorPickerManager; import net.runelite.client.ui.components.colorpicker.RuneliteColorPicker; import net.runelite.client.util.ColorUtil; import net.runelite.client.util.ImageUtil; +import net.runelite.client.util.SwingUtil; import net.runelite.client.util.Text; @Slf4j -public class ConfigPanel extends PluginPanel +class ConfigPanel extends PluginPanel { private static final int SPINNER_FIELD_WIDTH = 6; - private static final int SCROLLBAR_WIDTH = 17; - private static final int OFFSET = 6; private static final ImageIcon BACK_ICON; private static final ImageIcon BACK_ICON_HOVER; - private static final String RUNELITE_GROUP_NAME = RuneLiteConfig.class.getAnnotation(ConfigGroup.class).value(); - private static final String PINNED_PLUGINS_CONFIG_KEY = "pinnedPlugins"; - private static final String RUNELITE_PLUGIN = "RuneLite"; - private static final String CHAT_COLOR_PLUGIN = "Chat Color"; + private final FixedWidthPanel mainPanel; + private final JLabel title; + private final PluginToggleButton pluginToggle; - private final PluginManager pluginManager; - private final ConfigManager configManager; - private final ScheduledExecutorService executorService; - private final RuneLiteConfig runeLiteConfig; - private final ChatColorConfig chatColorConfig; - private final ColorPickerManager colorPickerManager; - private final List pluginList = new ArrayList<>(); + @Inject + private PluginListPanel pluginList; - private final IconTextField searchBar = new IconTextField(); - private final JPanel topPanel; - private final JPanel mainPanel; - private final JScrollPane scrollPane; + @Inject + private ConfigManager configManager; - private boolean showingPluginList = true; - private int scrollBarPosition = 0; + @Inject + private PluginManager pluginManager; + + @Inject + private ColorPickerManager colorPickerManager; + + private PluginConfigurationDescriptor pluginConfig = null; static { @@ -129,48 +111,16 @@ public class ConfigPanel extends PluginPanel BACK_ICON_HOVER = new ImageIcon(ImageUtil.alphaOffset(backIcon, -100)); } - ConfigPanel(PluginManager pluginManager, ConfigManager configManager, ScheduledExecutorService executorService, - RuneLiteConfig runeLiteConfig, ChatColorConfig chatColorConfig, ColorPickerManager colorPickerManager) + public ConfigPanel() { super(false); - this.pluginManager = pluginManager; - this.configManager = configManager; - this.executorService = executorService; - this.runeLiteConfig = runeLiteConfig; - this.chatColorConfig = chatColorConfig; - this.colorPickerManager = colorPickerManager; - - 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(new DocumentListener() - { - @Override - public void insertUpdate(DocumentEvent e) - { - onSearchBarChanged(); - } - - @Override - public void removeUpdate(DocumentEvent e) - { - onSearchBarChanged(); - } - - @Override - public void changedUpdate(DocumentEvent e) - { - onSearchBarChanged(); - } - }); setLayout(new BorderLayout()); setBackground(ColorScheme.DARK_GRAY_COLOR); - topPanel = new JPanel(); + JPanel topPanel = new JPanel(); topPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); - topPanel.setLayout(new BorderLayout(0, OFFSET)); + topPanel.setLayout(new BorderLayout(0, BORDER_OFFSET)); add(topPanel, BorderLayout.NORTH); mainPanel = new FixedWidthPanel(); @@ -182,139 +132,66 @@ public class ConfigPanel extends PluginPanel northPanel.setLayout(new BorderLayout()); northPanel.add(mainPanel, BorderLayout.NORTH); - scrollPane = new JScrollPane(northPanel); + JScrollPane scrollPane = new JScrollPane(northPanel); scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); add(scrollPane, BorderLayout.CENTER); - initializePluginList(); - refreshPluginList(); - } - - private void initializePluginList() - { - final List pinnedPlugins = getPinnedPluginNames(); - - // populate pluginList with all non-hidden plugins - pluginManager.getPlugins().stream() - .filter(plugin -> !plugin.getClass().getAnnotation(PluginDescriptor.class).hidden()) - .forEach(plugin -> - { - final PluginDescriptor descriptor = plugin.getClass().getAnnotation(PluginDescriptor.class); - final Config config = pluginManager.getPluginConfigProxy(plugin); - final ConfigDescriptor configDescriptor = config == null ? null : configManager.getConfigDescriptor(config); - - final PluginListItem listItem = new PluginListItem(this, plugin, descriptor, config, configDescriptor); - listItem.setPinned(pinnedPlugins.contains(listItem.getName())); - pluginList.add(listItem); - }); - - // add special entries for core client configurations - final PluginListItem runeLite = new PluginListItem(this, runeLiteConfig, - configManager.getConfigDescriptor(runeLiteConfig), - RUNELITE_PLUGIN, "RuneLite client settings", "client"); - runeLite.setPinned(pinnedPlugins.contains(RUNELITE_PLUGIN)); - pluginList.add(runeLite); - - final PluginListItem chatColor = new PluginListItem(this, chatColorConfig, - configManager.getConfigDescriptor(chatColorConfig), - CHAT_COLOR_PLUGIN, "Recolor chat text", "colour", "messages"); - chatColor.setPinned(pinnedPlugins.contains(CHAT_COLOR_PLUGIN)); - pluginList.add(chatColor); - - pluginList.sort(Comparator.comparing(PluginListItem::getName)); - } - - void refreshPluginList() - { - // update enabled / disabled status of all items - pluginList.forEach(listItem -> - { - final Plugin plugin = listItem.getPlugin(); - if (plugin != null) - { - listItem.setPluginEnabled(pluginManager.isPluginEnabled(plugin)); - } - }); - - if (showingPluginList) - { - openConfigList(); - } - } - - void openConfigList() - { - if (showingPluginList) - { - scrollBarPosition = scrollPane.getVerticalScrollBar().getValue(); - } - - showingPluginList = true; - - topPanel.removeAll(); - mainPanel.removeAll(); - topPanel.add(searchBar, BorderLayout.CENTER); - - onSearchBarChanged(); - searchBar.requestFocusInWindow(); - validate(); - scrollPane.getVerticalScrollBar().setValue(scrollBarPosition); - } - - private void onSearchBarChanged() - { - final String text = searchBar.getText(); - - pluginList.forEach(mainPanel::remove); - - showMatchingPlugins(true, text); - showMatchingPlugins(false, text); - - revalidate(); - } - - private void showMatchingPlugins(boolean pinned, String text) - { - if (text.isEmpty()) - { - pluginList.stream().filter(item -> pinned == item.isPinned()).forEach(mainPanel::add); - return; - } - - final String[] searchTerms = text.toLowerCase().split(" "); - pluginList.forEach(listItem -> - { - if (pinned == listItem.isPinned() && listItem.matchesSearchTerms(searchTerms)) - { - mainPanel.add(listItem); - } - }); - } - - void openGroupConfigPanel(PluginListItem listItem, Config config, ConfigDescriptor cd) - { - showingPluginList = false; - - scrollBarPosition = scrollPane.getVerticalScrollBar().getValue(); - topPanel.removeAll(); - mainPanel.removeAll(); - - final IconButton topPanelBackButton = new IconButton(BACK_ICON, BACK_ICON_HOVER); + JButton topPanelBackButton = new JButton(BACK_ICON); + topPanelBackButton.setRolloverIcon(BACK_ICON_HOVER); + SwingUtil.removeButtonDecorations(topPanelBackButton); topPanelBackButton.setPreferredSize(new Dimension(22, 0)); topPanelBackButton.setBorder(new EmptyBorder(0, 0, 0, 5)); - topPanelBackButton.addActionListener(e -> openConfigList()); + topPanelBackButton.addActionListener(e -> pluginList.getMuxer().popState()); topPanelBackButton.setToolTipText("Back"); topPanel.add(topPanelBackButton, BorderLayout.WEST); - topPanel.add(listItem.getConfigToggleButton(), BorderLayout.EAST); - - String name = listItem.getName(); - JLabel title = new JLabel(name); + pluginToggle = new PluginToggleButton(); + topPanel.add(pluginToggle, BorderLayout.EAST); + title = new JLabel(); title.setForeground(Color.WHITE); - title.setToolTipText("" + name + ":
" + listItem.getDescription() + ""); - PluginListItem.addLabelPopupMenu(title, PluginListItem.wikiLinkMenuItem(listItem.getName())); - topPanel.add(title); + topPanel.add(title); + } + + void init(PluginConfigurationDescriptor pluginConfig) + { + assert this.pluginConfig == null; + this.pluginConfig = pluginConfig; + + String name = pluginConfig.getName(); + title.setText(name); + title.setForeground(Color.WHITE); + title.setToolTipText("" + name + ":
" + pluginConfig.getDescription() + ""); + PluginListItem.addLabelPopupMenu(title, pluginConfig.createSupportMenuItem()); + + if (pluginConfig.getPlugin() != null) + { + pluginToggle.setSelected(pluginManager.isPluginEnabled(pluginConfig.getPlugin())); + pluginToggle.addItemListener(i -> + { + if (pluginToggle.isSelected()) + { + pluginList.startPlugin(pluginConfig.getPlugin()); + } + else + { + pluginList.stopPlugin(pluginConfig.getPlugin()); + } + }); + } + else + { + pluginToggle.setVisible(false); + } + + rebuild(); + } + + private void rebuild() + { + mainPanel.removeAll(); + + ConfigDescriptor cd = pluginConfig.getConfigDescriptor(); for (ConfigItemDescriptor cid : cd.getItems()) { if (cid.getItem().hidden()) @@ -325,7 +202,7 @@ public class ConfigPanel extends PluginPanel JPanel item = new JPanel(); item.setLayout(new BorderLayout()); item.setMinimumSize(new Dimension(PANEL_WIDTH, 0)); - name = cid.getItem().name(); + String name = cid.getItem().name(); JLabel configEntryName = new JLabel(name); configEntryName.setForeground(Color.WHITE); configEntryName.setToolTipText("" + name + ":
" + cid.getItem().description() + ""); @@ -336,7 +213,7 @@ public class ConfigPanel extends PluginPanel JCheckBox checkbox = new JCheckBox(); checkbox.setBackground(ColorScheme.LIGHT_GRAY_COLOR); checkbox.setSelected(Boolean.parseBoolean(configManager.getConfiguration(cd.getGroup().value(), cid.getItem().keyName()))); - checkbox.addActionListener(ae -> changeConfiguration(listItem, config, checkbox, cd, cid)); + checkbox.addActionListener(ae -> changeConfiguration(checkbox, cd, cid)); item.add(checkbox, BorderLayout.EAST); } @@ -361,7 +238,7 @@ public class ConfigPanel extends PluginPanel Component editor = spinner.getEditor(); JFormattedTextField spinnerTextField = ((JSpinner.DefaultEditor) editor).getTextField(); spinnerTextField.setColumns(SPINNER_FIELD_WIDTH); - spinner.addChangeListener(ce -> changeConfiguration(listItem, config, spinner, cd, cid)); + spinner.addChangeListener(ce -> changeConfiguration(spinner, cd, cid)); item.add(spinner, BorderLayout.EAST); } @@ -390,7 +267,7 @@ public class ConfigPanel extends PluginPanel @Override public void focusLost(FocusEvent e) { - changeConfiguration(listItem, config, textField, cd, cid); + changeConfiguration(textField, cd, cid); } }); @@ -433,7 +310,7 @@ public class ConfigPanel extends PluginPanel colorPickerBtn.setBackground(c); colorPickerBtn.setText(ColorUtil.toHexColor(c).toUpperCase()); }); - colorPicker.setOnClose(c -> changeConfiguration(listItem, config, colorPicker, cd, cid)); + colorPicker.setOnClose(c -> changeConfiguration(colorPicker, cd, cid)); colorPicker.setVisible(true); } }); @@ -499,7 +376,7 @@ public class ConfigPanel extends PluginPanel { if (e.getStateChange() == ItemEvent.SELECTED) { - changeConfiguration(listItem, config, box, cd, cid); + changeConfiguration(box, cd, cid); box.setToolTipText(Text.titleCase((Enum) box.getSelectedItem())); } }); @@ -519,7 +396,7 @@ public class ConfigPanel extends PluginPanel @Override public void focusLost(FocusEvent e) { - changeConfiguration(listItem, config, button, cd, cid); + changeConfiguration(button, cd, cid); } }); @@ -538,23 +415,21 @@ public class ConfigPanel extends PluginPanel if (result == JOptionPane.YES_OPTION) { - configManager.setDefaultConfiguration(config, true); + configManager.setDefaultConfiguration(pluginConfig.getConfig(), true); - // Reload configuration panel - openGroupConfigPanel(listItem, config, cd); + rebuild(); } }); mainPanel.add(resetButton); JButton backButton = new JButton("Back"); - backButton.addActionListener(e -> openConfigList()); + backButton.addActionListener(e -> pluginList.getMuxer().popState()); mainPanel.add(backButton); revalidate(); - scrollPane.getVerticalScrollBar().setValue(0); } - private void changeConfiguration(PluginListItem listItem, Config config, Component component, ConfigDescriptor cd, ConfigItemDescriptor cid) + private void changeConfiguration(Component component, ConfigDescriptor cd, ConfigItemDescriptor cid) { final ConfigItem configItem = cid.getItem(); @@ -566,7 +441,7 @@ public class ConfigPanel extends PluginPanel if (result != JOptionPane.YES_OPTION) { - openGroupConfigPanel(listItem, config, cd); + rebuild(); return; } } @@ -603,102 +478,21 @@ public class ConfigPanel extends PluginPanel } } - void startPlugin(Plugin plugin, PluginListItem listItem) - { - executorService.submit(() -> - { - pluginManager.setPluginEnabled(plugin, true); - - try - { - pluginManager.startPlugin(plugin); - } - catch (PluginInstantiationException ex) - { - log.warn("Error when starting plugin {}", plugin.getClass().getSimpleName(), ex); - } - - listItem.setPluginEnabled(true); - }); - } - - void stopPlugin(Plugin plugin, PluginListItem listItem) - { - executorService.submit(() -> - { - pluginManager.setPluginEnabled(plugin, false); - - try - { - pluginManager.stopPlugin(plugin); - } - catch (PluginInstantiationException ex) - { - log.warn("Error when stopping plugin {}", plugin.getClass().getSimpleName(), ex); - } - - listItem.setPluginEnabled(false); - }); - } - - private List getPinnedPluginNames() - { - final String config = configManager.getConfiguration(RUNELITE_GROUP_NAME, PINNED_PLUGINS_CONFIG_KEY); - - if (config == null) - { - return Collections.emptyList(); - } - - return Text.fromCSV(config); - } - - void savePinnedPlugins() - { - final String value = pluginList.stream() - .filter(PluginListItem::isPinned) - .map(PluginListItem::getName) - .collect(Collectors.joining(",")); - - configManager.setConfiguration(RUNELITE_GROUP_NAME, PINNED_PLUGINS_CONFIG_KEY, value); - } - - void openConfigurationPanel(String configGroup) - { - for (PluginListItem pluginListItem : pluginList) - { - if (pluginListItem.getName().equals(configGroup)) - { - openGroupConfigPanel(pluginListItem, pluginListItem.getConfig(), pluginListItem.getConfigDescriptor()); - break; - } - } - } - - @Override - public void onActivate() - { - super.onActivate(); - - if (searchBar.getParent() != null) - { - searchBar.requestFocusInWindow(); - } - } - @Override public Dimension getPreferredSize() { return new Dimension(PANEL_WIDTH + SCROLLBAR_WIDTH, super.getPreferredSize().height); } - private class FixedWidthPanel extends JPanel + @Subscribe + public void onPluginChanged(PluginChanged event) { - @Override - public Dimension getPreferredSize() + if (event.getPlugin() == this.pluginConfig.getPlugin()) { - return new Dimension(PANEL_WIDTH, super.getPreferredSize().height); + SwingUtilities.invokeLater(() -> + { + pluginToggle.setSelected(event.isLoaded()); + }); } - } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPlugin.java index f17542447b..3024dfc786 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPlugin.java @@ -25,8 +25,8 @@ package net.runelite.client.plugins.config; import java.awt.image.BufferedImage; -import java.util.concurrent.ScheduledExecutorService; import javax.inject.Inject; +import javax.inject.Provider; import javax.swing.SwingUtilities; import net.runelite.api.MenuAction; import net.runelite.client.config.ChatColorConfig; @@ -34,13 +34,10 @@ import net.runelite.client.config.ConfigManager; import net.runelite.client.config.RuneLiteConfig; import net.runelite.client.eventbus.Subscribe; import net.runelite.client.events.OverlayMenuClicked; -import net.runelite.client.events.PluginChanged; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; -import net.runelite.client.plugins.PluginManager; import net.runelite.client.ui.ClientToolbar; import net.runelite.client.ui.NavigationButton; -import net.runelite.client.ui.components.colorpicker.ColorPickerManager; import net.runelite.client.ui.overlay.Overlay; import net.runelite.client.ui.overlay.OverlayMenuEntry; import net.runelite.client.util.ImageUtil; @@ -55,31 +52,35 @@ public class ConfigPlugin extends Plugin @Inject private ClientToolbar clientToolbar; + @Inject + private Provider pluginListPanelProvider; + @Inject private ConfigManager configManager; - @Inject - private PluginManager pluginManager; - - @Inject - private ScheduledExecutorService executorService; - @Inject private RuneLiteConfig runeLiteConfig; @Inject private ChatColorConfig chatColorConfig; - @Inject - private ColorPickerManager colorPickerManager; + private PluginListPanel pluginListPanel; - private ConfigPanel configPanel; private NavigationButton navButton; @Override protected void startUp() throws Exception { - configPanel = new ConfigPanel(pluginManager, configManager, executorService, runeLiteConfig, chatColorConfig, colorPickerManager); + pluginListPanel = pluginListPanelProvider.get(); + pluginListPanel.addFakePlugin(new PluginConfigurationDescriptor( + "RuneLite", "RuneLite client settings", new String[]{"client"}, + null, runeLiteConfig, configManager.getConfigDescriptor(runeLiteConfig) + ), + new PluginConfigurationDescriptor( + "Chat Color", "Recolor chat text", new String[]{"colour", "messages"}, + null, chatColorConfig, configManager.getConfigDescriptor(chatColorConfig) + )); + pluginListPanel.rebuildPluginList(); final BufferedImage icon = ImageUtil.getResourceStreamFromClass(getClass(), "config_icon.png"); @@ -87,7 +88,7 @@ public class ConfigPlugin extends Plugin .tooltip("Configuration") .icon(icon) .priority(0) - .panel(configPanel) + .panel(pluginListPanel.getMuxer()) .build(); clientToolbar.addNavigation(navButton); @@ -99,12 +100,6 @@ public class ConfigPlugin extends Plugin clientToolbar.removeNavigation(navButton); } - @Subscribe - public void onPluginChanged(PluginChanged event) - { - SwingUtilities.invokeLater(configPanel::refreshPluginList); - } - @Subscribe public void onOverlayMenuClicked(OverlayMenuClicked overlayMenuClicked) { @@ -126,7 +121,7 @@ public class ConfigPlugin extends Plugin { navButton.getOnSelect().run(); } - configPanel.openConfigurationPanel(descriptor.name()); + pluginListPanel.openConfigurationPanel(descriptor.name()); }); } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/FixedWidthPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/FixedWidthPanel.java new file mode 100644 index 0000000000..0a6373a201 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/FixedWidthPanel.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2017, Adam + * 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.plugins.config; + +import java.awt.Dimension; +import javax.swing.JPanel; +import net.runelite.client.ui.PluginPanel; + +class FixedWidthPanel extends JPanel +{ + @Override + public Dimension getPreferredSize() + { + return new Dimension(PluginPanel.PANEL_WIDTH, super.getPreferredSize().height); + } + +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/HotkeyButton.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/HotkeyButton.java index 585ade5ea6..17b23a5785 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/config/HotkeyButton.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/HotkeyButton.java @@ -31,7 +31,7 @@ import lombok.Getter; import net.runelite.client.config.Keybind; import net.runelite.client.config.ModifierlessKeybind; -public class HotkeyButton extends JButton +class HotkeyButton extends JButton { @Getter private Keybind value; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginConfigurationDescriptor.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginConfigurationDescriptor.java new file mode 100644 index 0000000000..69932e1d61 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginConfigurationDescriptor.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2019 Abex + * 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.plugins.config; + +import javax.annotation.Nullable; +import javax.swing.JMenuItem; +import lombok.Value; +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigDescriptor; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.util.LinkBrowser; + +@Value +class PluginConfigurationDescriptor +{ + private final String name; + private final String description; + private final String[] tags; + + // Can be null if its not an actual plugin (RuneLite / ChatColors) + @Nullable + private final Plugin plugin; + + // Can be null if it has no more configuration than the on/off toggle + @Nullable + private final Config config; + + @Nullable + private final ConfigDescriptor configDescriptor; + + boolean hasConfigurables() + { + return configDescriptor != null && !configDescriptor.getItems().stream().allMatch(item -> item.getItem().hidden()); + } + + /** + * Creates a menu item for linking to a support page for the plugin + * + * @return A {@link JMenuItem} which opens the plugin's wiki page URL in the browser when clicked + */ + JMenuItem createSupportMenuItem() + { + final JMenuItem menuItem = new JMenuItem("Wiki"); + menuItem.addActionListener(e -> LinkBrowser.browse("https://github.com/runelite/runelite/wiki/" + name.replace(' ', '-'))); + return menuItem; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListItem.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListItem.java index 69dec09365..340e0982ba 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListItem.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListItem.java @@ -38,91 +38,49 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; -import javax.annotation.Nullable; import javax.swing.ImageIcon; +import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; +import javax.swing.JToggleButton; import javax.swing.SwingUtilities; import javax.swing.border.EmptyBorder; -import lombok.AccessLevel; import lombok.Getter; -import net.runelite.client.config.Config; -import net.runelite.client.config.ConfigDescriptor; -import net.runelite.client.plugins.Plugin; -import net.runelite.client.plugins.PluginDescriptor; import net.runelite.client.ui.ColorScheme; import net.runelite.client.ui.PluginPanel; -import net.runelite.client.ui.components.IconButton; import net.runelite.client.util.ImageUtil; -import net.runelite.client.util.LinkBrowser; +import net.runelite.client.util.SwingUtil; import org.apache.commons.text.similarity.JaroWinklerDistance; class PluginListItem extends JPanel { private static final JaroWinklerDistance DISTANCE = new JaroWinklerDistance(); - private static final String RUNELITE_WIKI_FORMAT = "https://github.com/runelite/runelite/wiki/%s"; private static final ImageIcon CONFIG_ICON; private static final ImageIcon CONFIG_ICON_HOVER; - private static final ImageIcon ON_SWITCHER; - private static final ImageIcon OFF_SWITCHER; private static final ImageIcon ON_STAR; private static final ImageIcon OFF_STAR; - private final ConfigPanel configPanel; + private final PluginListPanel pluginListPanel; @Getter - @Nullable - private final Plugin plugin; - - @Nullable - @Getter(AccessLevel.PACKAGE) - private final Config config; - - @Nullable - @Getter(AccessLevel.PACKAGE) - private final ConfigDescriptor configDescriptor; - - @Getter - private final String name; - - @Getter - private final String description; - - @Getter - private final IconButton configToggleButton; + private final PluginConfigurationDescriptor pluginConfig; private final List keywords = new ArrayList<>(); - private final IconButton pinButton = new IconButton(OFF_STAR); - private final IconButton configButton = new IconButton(CONFIG_ICON, CONFIG_ICON_HOVER); - private final IconButton toggleButton; - - private boolean isPluginEnabled = false; - - @Getter - private boolean isPinned = false; + private final JToggleButton pinButton; + private final JToggleButton onOffToggle; static { BufferedImage configIcon = ImageUtil.getResourceStreamFromClass(ConfigPanel.class, "config_edit_icon.png"); - BufferedImage onSwitcher = ImageUtil.getResourceStreamFromClass(ConfigPanel.class, "switcher_on.png"); BufferedImage onStar = ImageUtil.getResourceStreamFromClass(ConfigPanel.class, "star_on.png"); CONFIG_ICON = new ImageIcon(configIcon); - ON_SWITCHER = new ImageIcon(onSwitcher); ON_STAR = new ImageIcon(onStar); CONFIG_ICON_HOVER = new ImageIcon(ImageUtil.grayscaleOffset(configIcon, -100)); - BufferedImage offSwitcherImage = ImageUtil.flipImage( - ImageUtil.grayscaleOffset( - ImageUtil.grayscaleImage(onSwitcher), - 0.61f - ), - true, - false - ); - OFF_SWITCHER = new ImageIcon(offSwitcherImage); + BufferedImage offStar = ImageUtil.grayscaleOffset( ImageUtil.grayscaleImage(onStar), 0.77f @@ -130,76 +88,54 @@ class PluginListItem extends JPanel OFF_STAR = new ImageIcon(offStar); } - /** - * Creates a new {@code PluginListItem} for a plugin. - *

- * Note that {@code config} and {@code configDescriptor} can be {@code null} - * if there is no configuration associated with the plugin. - */ - PluginListItem(ConfigPanel configPanel, Plugin plugin, PluginDescriptor descriptor, - @Nullable Config config, @Nullable ConfigDescriptor configDescriptor) + PluginListItem(PluginListPanel pluginListPanel, PluginConfigurationDescriptor pluginConfig) { - this(configPanel, plugin, config, configDescriptor, - descriptor.name(), descriptor.description(), descriptor.tags()); - } + this.pluginListPanel = pluginListPanel; + this.pluginConfig = pluginConfig; - /** - * Creates a new {@code PluginListItem} for a core configuration. - */ - PluginListItem(ConfigPanel configPanel, Config config, ConfigDescriptor configDescriptor, - String name, String description, String... tags) - { - this(configPanel, null, config, configDescriptor, name, description, tags); - } - - private PluginListItem(ConfigPanel configPanel, @Nullable Plugin plugin, @Nullable Config config, - @Nullable ConfigDescriptor configDescriptor, String name, String description, String... tags) - { - this.configPanel = configPanel; - this.plugin = plugin; - this.config = config; - this.configDescriptor = configDescriptor; - this.name = name; - this.description = description; - Collections.addAll(keywords, name.toLowerCase().split(" ")); - Collections.addAll(keywords, description.toLowerCase().split(" ")); - Collections.addAll(keywords, tags); + Collections.addAll(keywords, pluginConfig.getName().toLowerCase().split(" ")); + Collections.addAll(keywords, pluginConfig.getDescription().toLowerCase().split(" ")); + Collections.addAll(keywords, pluginConfig.getTags()); final List popupMenuItems = new ArrayList<>(); setLayout(new BorderLayout(3, 0)); setPreferredSize(new Dimension(PluginPanel.PANEL_WIDTH, 20)); - JLabel nameLabel = new JLabel(name); + JLabel nameLabel = new JLabel(pluginConfig.getName()); nameLabel.setForeground(Color.WHITE); - if (!description.isEmpty()) + if (!pluginConfig.getDescription().isEmpty()) { - nameLabel.setToolTipText("" + name + ":
" + description + ""); + nameLabel.setToolTipText("" + pluginConfig.getName() + ":
" + pluginConfig.getDescription() + ""); } - + pinButton = new JToggleButton(OFF_STAR); + pinButton.setSelectedIcon(ON_STAR); + SwingUtil.removeButtonDecorations(pinButton); + SwingUtil.addModalTooltip(pinButton, "Unpin plugin", "Pin plugin"); pinButton.setPreferredSize(new Dimension(21, 0)); add(pinButton, BorderLayout.LINE_START); pinButton.addActionListener(e -> { - setPinned(!isPinned); - configPanel.savePinnedPlugins(); - configPanel.openConfigList(); + pluginListPanel.savePinnedPlugins(); + pluginListPanel.refresh(); }); final JPanel buttonPanel = new JPanel(); buttonPanel.setLayout(new GridLayout(1, 2)); add(buttonPanel, BorderLayout.LINE_END); - configButton.setPreferredSize(new Dimension(25, 0)); - configButton.setVisible(false); - buttonPanel.add(configButton); - - // add a listener to configButton only if there are config items to show - if (config != null && !configDescriptor.getItems().stream().allMatch(item -> item.getItem().hidden())) + if (pluginConfig.hasConfigurables()) { + JButton configButton = new JButton(CONFIG_ICON); + configButton.setRolloverIcon(CONFIG_ICON_HOVER); + SwingUtil.removeButtonDecorations(configButton); + configButton.setPreferredSize(new Dimension(25, 0)); + configButton.setVisible(false); + buttonPanel.add(configButton); + configButton.addActionListener(e -> { configButton.setIcon(CONFIG_ICON); @@ -214,72 +150,50 @@ class PluginListItem extends JPanel popupMenuItems.add(configMenuItem); } - popupMenuItems.add(wikiLinkMenuItem(name)); + popupMenuItems.add(pluginConfig.createSupportMenuItem()); addLabelPopupMenu(nameLabel, popupMenuItems); add(nameLabel, BorderLayout.CENTER); - toggleButton = createToggleButton(); - buttonPanel.add(toggleButton); - - configToggleButton = createToggleButton(); - } - - private void attachToggleButtonListener(IconButton button) - { - // no need for a listener if there is no plugin to enable / disable - if (plugin == null) + onOffToggle = new PluginToggleButton(); + buttonPanel.add(onOffToggle); + if (pluginConfig.getPlugin() != null) { - button.setVisible(false); - return; + onOffToggle.addItemListener(i -> + { + if (onOffToggle.isSelected()) + { + pluginListPanel.startPlugin(pluginConfig.getPlugin()); + } + else + { + pluginListPanel.stopPlugin(pluginConfig.getPlugin()); + } + }); } - - button.addActionListener(e -> + else { - if (isPluginEnabled) - { - configPanel.stopPlugin(plugin, PluginListItem.this); - } - else - { - configPanel.startPlugin(plugin, PluginListItem.this); - } - - setPluginEnabled(!isPluginEnabled); - updateToggleButton(button); - }); + onOffToggle.setVisible(false); + } } - private IconButton createToggleButton() + boolean isPinned() { - final IconButton button = new IconButton(OFF_SWITCHER); - button.setPreferredSize(new Dimension(25, 0)); - updateToggleButton(button); - attachToggleButtonListener(button); - return button; - } - - void setPluginEnabled(boolean enabled) - { - isPluginEnabled = enabled; - updateToggleButton(toggleButton); - updateToggleButton(configToggleButton); + return pinButton.isSelected(); } void setPinned(boolean pinned) { - isPinned = pinned; - pinButton.setIcon(pinned ? ON_STAR : OFF_STAR); - pinButton.setToolTipText(pinned ? "Unpin plugin" : "Pin plugin"); + pinButton.setSelected(pinned); } - private void updateToggleButton(IconButton button) + void setPluginEnabled(boolean enabled) { - button.setIcon(isPluginEnabled ? ON_SWITCHER : OFF_SWITCHER); - button.setToolTipText(isPluginEnabled ? "Disable plugin" : "Enable plugin"); + onOffToggle.setSelected(enabled); } /** * Checks if all the search terms in the given list matches at least one keyword. + * * @return true if all search terms matches at least one keyword, or false if otherwise. */ boolean matchesSearchTerms(String[] searchTerms) @@ -297,7 +211,7 @@ class PluginListItem extends JPanel private void openGroupConfigPanel() { - configPanel.openGroupConfigPanel(PluginListItem.this, config, configDescriptor); + pluginListPanel.openConfigurationPanel(pluginConfig); } /** @@ -360,19 +274,4 @@ class PluginListItem extends JPanel } }); } - - /** - * Creates a menu item for linking to a wiki page which, when clicked, opens a link to the plugin's wiki page for - * the passed plugin name. - * - * @param pluginName The name of the plugin which should be linked to - * @return A {@link JMenuItem} which opens the plugin's wiki page URL in the browser when clicked - */ - static JMenuItem wikiLinkMenuItem(final String pluginName) - { - final JMenuItem menuItem = new JMenuItem("Wiki"); - final String sanitizedName = pluginName.replace(' ', '-'); - menuItem.addActionListener(e -> LinkBrowser.browse(String.format(RUNELITE_WIKI_FORMAT, sanitizedName))); - return menuItem; - } } 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 new file mode 100644 index 0000000000..abf9ab3bdb --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListPanel.java @@ -0,0 +1,356 @@ +/* + * Copyright (c) 2017, Adam + * 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.plugins.config; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Dimension; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.ScheduledExecutorService; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.inject.Inject; +import javax.inject.Provider; +import javax.inject.Singleton; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.ScrollPaneConstants; +import javax.swing.SwingUtilities; +import javax.swing.border.EmptyBorder; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigDescriptor; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.config.RuneLiteConfig; +import net.runelite.client.eventbus.EventBus; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.PluginChanged; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.plugins.PluginInstantiationException; +import net.runelite.client.plugins.PluginManager; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.DynamicGridLayout; +import net.runelite.client.ui.MultiplexingPluginPanel; +import net.runelite.client.ui.PluginPanel; +import net.runelite.client.ui.components.IconTextField; +import net.runelite.client.util.Text; + +@Slf4j +@Singleton +class PluginListPanel extends PluginPanel +{ + private static final String RUNELITE_GROUP_NAME = RuneLiteConfig.class.getAnnotation(ConfigGroup.class).value(); + private static final String PINNED_PLUGINS_CONFIG_KEY = "pinnedPlugins"; + + private final ConfigManager configManager; + private final PluginManager pluginManager; + private final ScheduledExecutorService executorService; + private final Provider configPanelProvider; + private final List fakePlugins = new ArrayList<>(); + + @Getter + private final MultiplexingPluginPanel muxer; + private final IconTextField searchBar; + private final JScrollPane scrollPane; + private final FixedWidthPanel mainPanel; + private List pluginList; + + @Inject + public PluginListPanel( + ConfigManager configManager, + PluginManager pluginManager, + ScheduledExecutorService executorService, + EventBus eventBus, + Provider configPanelProvider) + { + super(false); + + this.configManager = configManager; + this.pluginManager = pluginManager; + this.executorService = executorService; + this.configPanelProvider = configPanelProvider; + + muxer = new MultiplexingPluginPanel(this) + { + @Override + protected void onAdd(PluginPanel p) + { + eventBus.register(p); + } + + @Override + protected void onRemove(PluginPanel p) + { + eventBus.unregister(p); + } + }; + + searchBar = new IconTextField(); + 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(new DocumentListener() + { + @Override + public void insertUpdate(DocumentEvent e) + { + onSearchBarChanged(); + } + + @Override + public void removeUpdate(DocumentEvent e) + { + onSearchBarChanged(); + } + + @Override + public void changedUpdate(DocumentEvent e) + { + onSearchBarChanged(); + } + }); + + setLayout(new BorderLayout()); + setBackground(ColorScheme.DARK_GRAY_COLOR); + + JPanel topPanel = new JPanel(); + topPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); + topPanel.setLayout(new BorderLayout(0, BORDER_OFFSET)); + topPanel.add(searchBar, BorderLayout.CENTER); + add(topPanel, BorderLayout.NORTH); + + mainPanel = new FixedWidthPanel(); + mainPanel.setBorder(new EmptyBorder(8, 10, 10, 10)); + mainPanel.setLayout(new DynamicGridLayout(0, 1, 0, 5)); + mainPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + + JPanel northPanel = new FixedWidthPanel(); + northPanel.setLayout(new BorderLayout()); + northPanel.add(mainPanel, BorderLayout.NORTH); + + scrollPane = new JScrollPane(northPanel); + scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + add(scrollPane, BorderLayout.CENTER); + } + + void rebuildPluginList() + { + final List pinnedPlugins = getPinnedPluginNames(); + + // populate pluginList with all non-hidden plugins + pluginList = Stream.concat( + fakePlugins.stream(), + pluginManager.getPlugins().stream() + .filter(plugin -> !plugin.getClass().getAnnotation(PluginDescriptor.class).hidden()) + .map(plugin -> + { + PluginDescriptor descriptor = plugin.getClass().getAnnotation(PluginDescriptor.class); + Config config = pluginManager.getPluginConfigProxy(plugin); + ConfigDescriptor configDescriptor = config == null ? null : configManager.getConfigDescriptor(config); + + return new PluginConfigurationDescriptor( + descriptor.name(), + descriptor.description(), + descriptor.tags(), + plugin, + config, + configDescriptor); + }) + ).map(desc -> + { + PluginListItem listItem = new PluginListItem(this, desc); + listItem.setPinned(pinnedPlugins.contains(desc.getName())); + return listItem; + }).collect(Collectors.toList()); + + pluginList.sort(Comparator.comparing(p -> p.getPluginConfig().getName())); + mainPanel.removeAll(); + refresh(); + } + + void addFakePlugin(PluginConfigurationDescriptor... descriptor) + { + Collections.addAll(fakePlugins, descriptor); + } + + void refresh() + { + // update enabled / disabled status of all items + pluginList.forEach(listItem -> + { + final Plugin plugin = listItem.getPluginConfig().getPlugin(); + if (plugin != null) + { + listItem.setPluginEnabled(pluginManager.isPluginEnabled(plugin)); + } + }); + + int scrollBarPosition = scrollPane.getVerticalScrollBar().getValue(); + + onSearchBarChanged(); + searchBar.requestFocusInWindow(); + validate(); + + scrollPane.getVerticalScrollBar().setValue(scrollBarPosition); + } + + private void onSearchBarChanged() + { + final String text = searchBar.getText(); + + pluginList.forEach(mainPanel::remove); + + showMatchingPlugins(true, text); + showMatchingPlugins(false, text); + + revalidate(); + } + + private void showMatchingPlugins(boolean pinned, String text) + { + if (text.isEmpty()) + { + pluginList.stream().filter(item -> pinned == item.isPinned()).forEach(mainPanel::add); + return; + } + + final String[] searchTerms = text.toLowerCase().split(" "); + pluginList.forEach(listItem -> + { + if (pinned == listItem.isPinned() && listItem.matchesSearchTerms(searchTerms)) + { + mainPanel.add(listItem); + } + }); + } + + void openConfigurationPanel(String configGroup) + { + for (PluginListItem pluginListItem : pluginList) + { + if (pluginListItem.getPluginConfig().getName().equals(configGroup)) + { + openConfigurationPanel(pluginListItem.getPluginConfig()); + break; + } + } + } + + void openConfigurationPanel(PluginConfigurationDescriptor plugin) + { + ConfigPanel panel = configPanelProvider.get(); + panel.init(plugin); + muxer.pushState(panel); + } + + void startPlugin(Plugin plugin) + { + executorService.submit(() -> + { + pluginManager.setPluginEnabled(plugin, true); + + try + { + pluginManager.startPlugin(plugin); + } + catch (PluginInstantiationException ex) + { + log.warn("Error when starting plugin {}", plugin.getClass().getSimpleName(), ex); + } + }); + } + + void stopPlugin(Plugin plugin) + { + executorService.submit(() -> + { + pluginManager.setPluginEnabled(plugin, false); + + try + { + pluginManager.stopPlugin(plugin); + } + catch (PluginInstantiationException ex) + { + log.warn("Error when stopping plugin {}", plugin.getClass().getSimpleName(), ex); + } + }); + } + + private List getPinnedPluginNames() + { + final String config = configManager.getConfiguration(RUNELITE_GROUP_NAME, PINNED_PLUGINS_CONFIG_KEY); + + if (config == null) + { + return Collections.emptyList(); + } + + return Text.fromCSV(config); + } + + void savePinnedPlugins() + { + final String value = pluginList.stream() + .filter(PluginListItem::isPinned) + .map(p -> p.getPluginConfig().getName()) + .collect(Collectors.joining(",")); + + configManager.setConfiguration(RUNELITE_GROUP_NAME, PINNED_PLUGINS_CONFIG_KEY, value); + } + + @Subscribe + public void onPluginChanged(PluginChanged event) + { + SwingUtilities.invokeLater(this::refresh); + } + + @Override + public Dimension getPreferredSize() + { + return new Dimension(PANEL_WIDTH + SCROLLBAR_WIDTH, super.getPreferredSize().height); + } + + @Override + public void onActivate() + { + super.onActivate(); + + if (searchBar.getParent() != null) + { + searchBar.requestFocusInWindow(); + } + } + +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginToggleButton.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginToggleButton.java new file mode 100644 index 0000000000..0fb22dfbe0 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginToggleButton.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2019 Abex + * 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.plugins.config; + +import java.awt.Dimension; +import java.awt.image.BufferedImage; +import javax.swing.ImageIcon; +import javax.swing.JToggleButton; +import net.runelite.client.util.ImageUtil; +import net.runelite.client.util.SwingUtil; + +class PluginToggleButton extends JToggleButton +{ + private static final ImageIcon ON_SWITCHER; + private static final ImageIcon OFF_SWITCHER; + + static + { + BufferedImage onSwitcher = ImageUtil.getResourceStreamFromClass(ConfigPanel.class, "switcher_on.png"); + ON_SWITCHER = new ImageIcon(onSwitcher); + OFF_SWITCHER = new ImageIcon(ImageUtil.flipImage( + ImageUtil.grayscaleOffset( + ImageUtil.grayscaleImage(onSwitcher), + 0.61f + ), + true, + false + )); + } + + public PluginToggleButton() + { + super(OFF_SWITCHER); + setSelectedIcon(ON_SWITCHER); + SwingUtil.removeButtonDecorations(this); + setPreferredSize(new Dimension(25, 0)); + SwingUtil.addModalTooltip(this, "Disable plugin", "Enable plugin"); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/ui/MultiplexingPluginPanel.java b/runelite-client/src/main/java/net/runelite/client/ui/MultiplexingPluginPanel.java new file mode 100644 index 0000000000..6454058a4c --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/ui/MultiplexingPluginPanel.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2019 Abex + * 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.ui; + +import java.awt.CardLayout; + +public class MultiplexingPluginPanel extends PluginPanel +{ + private final CardLayout layout; + private boolean active = false; + private PluginPanel current; + + public MultiplexingPluginPanel(PluginPanel root) + { + super(false); + + layout = new CardLayout(); + setLayout(layout); + pushState(root); + } + + public void destroy() + { + for (int i = getComponentCount() - 1; i > 0; i--) + { + onRemove((PluginPanel) getComponent(i)); + remove(i); + } + } + + public void pushState(PluginPanel subpanel) + { + int index = -1; + for (int i = getComponentCount() - 1; i >= 0; i--) + { + if (getComponent(i) == subpanel) + { + index = i; + break; + } + } + + if (active) + { + current.onDeactivate(); + subpanel.onActivate(); + } + current = subpanel; + + String name = System.identityHashCode(subpanel) + ""; + + if (index != -1) + { + for (int i = getComponentCount() - 1; i > index; i--) + { + popState(); + } + } + else + { + add(subpanel, name); + onAdd(subpanel); + } + + layout.show(this, name); + revalidate(); + } + + public void popState() + { + int count = getComponentCount(); + if (count <= 1) + { + assert false : "Cannot pop last component"; + return; + } + + PluginPanel subpanel = (PluginPanel) getComponent(count - 2); + if (active) + { + current.onDeactivate(); + subpanel.onActivate(); + current = subpanel; + } + layout.show(this, System.identityHashCode(subpanel) + ""); + onRemove((PluginPanel) getComponent(count - 1)); + remove(count - 1); + revalidate(); + } + + protected void onAdd(PluginPanel p) + { + } + + protected void onRemove(PluginPanel p) + { + } + + @Override + public void onActivate() + { + active = true; + current.onActivate(); + } + + @Override + public void onDeactivate() + { + active = false; + current.onDeactivate(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/ui/PluginPanel.java b/runelite-client/src/main/java/net/runelite/client/ui/PluginPanel.java index 73e9a92f8b..f5678f9f8d 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/PluginPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/PluginPanel.java @@ -36,9 +36,9 @@ import lombok.Getter; public abstract class PluginPanel extends JPanel { public static final int PANEL_WIDTH = 225; - private static final int SCROLLBAR_WIDTH = 17; - private static final int OFFSET = 6; - private static final EmptyBorder BORDER_PADDING = new EmptyBorder(OFFSET, OFFSET, OFFSET, OFFSET); + public static final int SCROLLBAR_WIDTH = 17; + public static final int BORDER_OFFSET = 6; + private static final EmptyBorder BORDER_PADDING = new EmptyBorder(BORDER_OFFSET, BORDER_OFFSET, BORDER_OFFSET, BORDER_OFFSET); private static final Dimension OUTER_PREFERRED_SIZE = new Dimension(PluginPanel.PANEL_WIDTH + SCROLLBAR_WIDTH, 0); @Getter(AccessLevel.PROTECTED) 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 39fc738f1f..f4e12c9020 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.Color; import java.awt.Font; import java.awt.Frame; import java.awt.Image; +import java.awt.Insets; import java.awt.SystemTray; import java.awt.TrayIcon; import java.awt.event.MouseAdapter; @@ -41,6 +42,7 @@ import java.util.concurrent.Callable; import java.util.function.BiConsumer; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import javax.swing.AbstractButton; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JFrame; @@ -277,4 +279,18 @@ public class SwingUtil navigationButton.setOnSelect(button::doClick); return button; } + + public static void removeButtonDecorations(AbstractButton button) + { + button.setBorderPainted(false); + button.setContentAreaFilled(false); + button.setFocusPainted(false); + button.setMargin(new Insets(0, 0, 0, 0)); + button.setOpaque(false); + } + + public static void addModalTooltip(AbstractButton button, String on, String off) + { + button.addItemListener(l -> button.setToolTipText(button.isSelected() ? on : off)); + } } From 808e85fdf828e23744dd4d246483d2f691c90971 Mon Sep 17 00:00:00 2001 From: Max Weber Date: Tue, 26 Nov 2019 21:10:20 -0700 Subject: [PATCH 2/7] runelite-client: Remove IconButton Most of this class is defaults, which can just be a normal method and a listener for hover support, which is part of the base class anyway. --- .../kourendlibrary/KourendLibraryPanel.java | 24 ++----- .../timetracking/clocks/ClockPanel.java | 37 ++++------ .../timetracking/clocks/ClockTabPanel.java | 7 +- .../timetracking/clocks/StopwatchPanel.java | 11 ++- .../timetracking/clocks/TimerPanel.java | 7 +- .../client/ui/components/IconButton.java | 71 ------------------- 6 files changed, 38 insertions(+), 119 deletions(-) delete mode 100644 runelite-client/src/main/java/net/runelite/client/ui/components/IconButton.java diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/kourendlibrary/KourendLibraryPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/kourendlibrary/KourendLibraryPanel.java index f65f0c7a63..8480328c47 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/kourendlibrary/KourendLibraryPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/kourendlibrary/KourendLibraryPanel.java @@ -29,8 +29,6 @@ import com.google.inject.Inject; import java.awt.BorderLayout; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import java.util.Comparator; import java.util.HashMap; @@ -52,7 +50,7 @@ import net.runelite.client.util.ImageUtil; class KourendLibraryPanel extends PluginPanel { private static final ImageIcon RESET_ICON; - private static final ImageIcon RESET_CLICK_ICON; + private static final ImageIcon RESET_HOVER_ICON; private final KourendLibraryConfig config; private final Library library; @@ -63,7 +61,7 @@ class KourendLibraryPanel extends PluginPanel { final BufferedImage resetIcon = ImageUtil.getResourceStreamFromClass(KourendLibraryPanel.class, "/util/reset.png"); RESET_ICON = new ImageIcon(resetIcon); - RESET_CLICK_ICON = new ImageIcon(ImageUtil.alphaOffset(resetIcon, -100)); + RESET_HOVER_ICON = new ImageIcon(ImageUtil.alphaOffset(resetIcon, -100)); } @Inject @@ -100,21 +98,11 @@ class KourendLibraryPanel extends PluginPanel }); JButton reset = new JButton("Reset", RESET_ICON); - reset.addMouseListener(new MouseAdapter() + reset.setRolloverIcon(RESET_HOVER_ICON); + reset.addActionListener(ev -> { - @Override - public void mousePressed(MouseEvent mouseEvent) - { - reset.setIcon(RESET_CLICK_ICON); - library.reset(); - update(); - } - - @Override - public void mouseReleased(MouseEvent mouseEvent) - { - reset.setIcon(RESET_ICON); - } + library.reset(); + update(); }); add(reset, BorderLayout.NORTH); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/ClockPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/ClockPanel.java index 98db274ee2..612a5a0918 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/ClockPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/ClockPanel.java @@ -30,12 +30,12 @@ import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; import java.time.Duration; import java.time.format.DateTimeParseException; import javax.swing.BorderFactory; +import javax.swing.JButton; import javax.swing.JPanel; +import javax.swing.JToggleButton; import javax.swing.SwingConstants; import javax.swing.border.Border; import javax.swing.border.CompoundBorder; @@ -43,7 +43,7 @@ import javax.swing.border.EmptyBorder; import lombok.Getter; import net.runelite.client.ui.ColorScheme; import net.runelite.client.ui.components.FlatTextField; -import net.runelite.client.ui.components.IconButton; +import net.runelite.client.util.SwingUtil; abstract class ClockPanel extends JPanel { @@ -63,7 +63,7 @@ abstract class ClockPanel extends JPanel final JPanel rightActions; private final FlatTextField nameInput; - private final IconButton startPauseButton; + private final JToggleButton startPauseButton; private final FlatTextField displayInput; @Getter @@ -167,28 +167,17 @@ abstract class ClockPanel extends JPanel leftActions = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0)); leftActions.setBackground(ColorScheme.DARKER_GRAY_COLOR); - startPauseButton = new IconButton(ClockTabPanel.START_ICON); + startPauseButton = new JToggleButton(ClockTabPanel.START_ICON); + startPauseButton.setRolloverIcon(ClockTabPanel.START_ICON_HOVER); + startPauseButton.setSelectedIcon(ClockTabPanel.PAUSE_ICON); + startPauseButton.setRolloverSelectedIcon(ClockTabPanel.PAUSE_ICON_HOVER); + SwingUtil.removeButtonDecorations(startPauseButton); startPauseButton.setPreferredSize(new Dimension(16, 14)); updateActivityStatus(); - startPauseButton.addMouseListener(new MouseAdapter() - { - @Override - public void mouseEntered(MouseEvent e) - { - startPauseButton.setIcon(clock.isActive() ? ClockTabPanel.PAUSE_ICON_HOVER : ClockTabPanel.START_ICON_HOVER); - } - - @Override - public void mouseExited(MouseEvent e) - { - startPauseButton.setIcon(clock.isActive() ? ClockTabPanel.PAUSE_ICON : ClockTabPanel.START_ICON); - } - }); - startPauseButton.addActionListener(e -> { - if (clock.isActive()) + if (!startPauseButton.isSelected()) { clock.pause(); } @@ -201,7 +190,9 @@ abstract class ClockPanel extends JPanel clockManager.saveToConfig(); }); - IconButton resetButton = new IconButton(ClockTabPanel.RESET_ICON, ClockTabPanel.RESET_ICON_HOVER); + JButton resetButton = new JButton(ClockTabPanel.RESET_ICON); + resetButton.setRolloverIcon(ClockTabPanel.RESET_ICON_HOVER); + SwingUtil.removeButtonDecorations(resetButton); resetButton.setPreferredSize(new Dimension(16, 14)); resetButton.setToolTipText("Reset " + clockType); @@ -249,7 +240,7 @@ abstract class ClockPanel extends JPanel displayInput.setEditable(editable && !isActive); displayInput.getTextField().setForeground(isActive ? ACTIVE_CLOCK_COLOR : INACTIVE_CLOCK_COLOR); startPauseButton.setToolTipText(isActive ? "Pause " + clockType : "Start " + clockType); - startPauseButton.setIcon(isActive ? ClockTabPanel.PAUSE_ICON : ClockTabPanel.START_ICON); + startPauseButton.setSelected(isActive); if (editable && clock.getDisplayTime() == 0 && !isActive) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/ClockTabPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/ClockTabPanel.java index 79578a3b79..7eb2dfb9ca 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/ClockTabPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/ClockTabPanel.java @@ -32,6 +32,7 @@ import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.List; import javax.swing.ImageIcon; +import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.border.EmptyBorder; @@ -40,9 +41,9 @@ import net.runelite.client.plugins.timetracking.TimeTrackingPlugin; import net.runelite.client.ui.ColorScheme; import net.runelite.client.ui.DynamicGridLayout; import net.runelite.client.ui.FontManager; -import net.runelite.client.ui.components.IconButton; import net.runelite.client.ui.components.shadowlabel.JShadowedLabel; import net.runelite.client.util.ImageUtil; +import net.runelite.client.util.SwingUtil; public class ClockTabPanel extends TabContentPanel { @@ -150,7 +151,9 @@ public class ClockTabPanel extends TabContentPanel headerLabel.setFont(FontManager.getRunescapeSmallFont()); panel.add(headerLabel, BorderLayout.CENTER); - IconButton addButton = new IconButton(ADD_ICON, ADD_ICON_HOVER); + JButton addButton = new JButton(ADD_ICON); + addButton.setRolloverIcon(ADD_ICON_HOVER); + SwingUtil.removeButtonDecorations(addButton); addButton.setPreferredSize(new Dimension(14, 14)); addButton.setToolTipText("Add a " + type); addButton.addActionListener(actionListener); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/StopwatchPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/StopwatchPanel.java index 99bb8def81..0603907d54 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/StopwatchPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/StopwatchPanel.java @@ -30,13 +30,14 @@ import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.util.List; +import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.SwingConstants; import javax.swing.border.EmptyBorder; import net.runelite.client.ui.ColorScheme; import net.runelite.client.ui.FontManager; -import net.runelite.client.ui.components.IconButton; +import net.runelite.client.util.SwingUtil; class StopwatchPanel extends ClockPanel { @@ -57,7 +58,9 @@ class StopwatchPanel extends ClockPanel contentContainer.add(lapsContainer); - IconButton lapButton = new IconButton(ClockTabPanel.LAP_ICON, ClockTabPanel.LAP_ICON_HOVER); + JButton lapButton = new JButton(ClockTabPanel.LAP_ICON); + lapButton.setRolloverIcon(ClockTabPanel.LAP_ICON_HOVER); + SwingUtil.removeButtonDecorations(lapButton); lapButton.setPreferredSize(new Dimension(16, 14)); lapButton.setToolTipText("Add lap time"); @@ -70,7 +73,9 @@ class StopwatchPanel extends ClockPanel leftActions.add(lapButton); - IconButton deleteButton = new IconButton(ClockTabPanel.DELETE_ICON, ClockTabPanel.DELETE_ICON_HOVER); + JButton deleteButton = new JButton(ClockTabPanel.DELETE_ICON); + deleteButton.setRolloverIcon(ClockTabPanel.DELETE_ICON_HOVER); + SwingUtil.removeButtonDecorations(deleteButton); deleteButton.setPreferredSize(new Dimension(16, 14)); deleteButton.setToolTipText("Delete stopwatch"); deleteButton.addActionListener(e -> clockManager.removeStopwatch(stopwatch)); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/TimerPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/TimerPanel.java index 9f2bf047f8..c1fc578dff 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/TimerPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/TimerPanel.java @@ -25,7 +25,8 @@ package net.runelite.client.plugins.timetracking.clocks; import java.awt.Dimension; -import net.runelite.client.ui.components.IconButton; +import javax.swing.JButton; +import net.runelite.client.util.SwingUtil; class TimerPanel extends ClockPanel { @@ -33,7 +34,9 @@ class TimerPanel extends ClockPanel { super(clockManager, timer, "timer", true); - IconButton deleteButton = new IconButton(ClockTabPanel.DELETE_ICON, ClockTabPanel.DELETE_ICON_HOVER); + JButton deleteButton = new JButton(ClockTabPanel.DELETE_ICON); + SwingUtil.removeButtonDecorations(deleteButton); + deleteButton.setRolloverIcon(ClockTabPanel.DELETE_ICON_HOVER); deleteButton.setPreferredSize(new Dimension(16, 14)); deleteButton.setToolTipText("Delete timer"); deleteButton.addActionListener(e -> clockManager.removeTimer(timer)); diff --git a/runelite-client/src/main/java/net/runelite/client/ui/components/IconButton.java b/runelite-client/src/main/java/net/runelite/client/ui/components/IconButton.java deleted file mode 100644 index 1d54bf601c..0000000000 --- a/runelite-client/src/main/java/net/runelite/client/ui/components/IconButton.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Teo - * 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.ui.components; - -import java.awt.Insets; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import javax.swing.ImageIcon; -import javax.swing.JButton; - -/** - * A button that consists of an icon, without any background, borders, or margins. - */ -public class IconButton extends JButton -{ - public IconButton(ImageIcon icon) - { - this(icon, null); - } - - public IconButton(ImageIcon icon, ImageIcon hoverIcon) - { - setIcon(icon); - setBorderPainted(false); - setContentAreaFilled(false); - setFocusPainted(false); - setMargin(new Insets(0, 0, 0, 0)); - setOpaque(false); - setRolloverEnabled(false); - - if (hoverIcon != null) - { - addMouseListener(new MouseAdapter() - { - @Override - public void mouseEntered(MouseEvent e) - { - setIcon(hoverIcon); - } - - @Override - public void mouseExited(MouseEvent e) - { - setIcon(icon); - } - }); - } - } -} From dc2f4b6f590e65183cc3d11d6880bd0e6ce5bf17 Mon Sep 17 00:00:00 2001 From: Max Weber Date: Tue, 26 Nov 2019 22:27:45 -0700 Subject: [PATCH 3/7] ImageUtil: Rename methods to luminance, and work with non ARGB images --- .../client/plugins/config/PluginListItem.java | 4 +- .../plugins/config/PluginToggleButton.java | 2 +- .../screenmarkers/ui/ScreenMarkerPanel.java | 6 +-- .../timetracking/clocks/ClockTabPanel.java | 10 ++--- .../plugins/worldhopper/WorldTableHeader.java | 2 +- .../net/runelite/client/util/ImageUtil.java | 41 +++++++++++++------ .../runelite/client/util/ImageUtilTest.java | 34 +++++++-------- 7 files changed, 58 insertions(+), 41 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListItem.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListItem.java index 340e0982ba..d0a61f0b7d 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListItem.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListItem.java @@ -79,9 +79,9 @@ class PluginListItem extends JPanel BufferedImage onStar = ImageUtil.getResourceStreamFromClass(ConfigPanel.class, "star_on.png"); CONFIG_ICON = new ImageIcon(configIcon); ON_STAR = new ImageIcon(onStar); - CONFIG_ICON_HOVER = new ImageIcon(ImageUtil.grayscaleOffset(configIcon, -100)); + CONFIG_ICON_HOVER = new ImageIcon(ImageUtil.luminanceOffset(configIcon, -100)); - BufferedImage offStar = ImageUtil.grayscaleOffset( + BufferedImage offStar = ImageUtil.luminanceScale( ImageUtil.grayscaleImage(onStar), 0.77f ); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginToggleButton.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginToggleButton.java index 0fb22dfbe0..a2eb72719f 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginToggleButton.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginToggleButton.java @@ -42,7 +42,7 @@ class PluginToggleButton extends JToggleButton BufferedImage onSwitcher = ImageUtil.getResourceStreamFromClass(ConfigPanel.class, "switcher_on.png"); ON_SWITCHER = new ImageIcon(onSwitcher); OFF_SWITCHER = new ImageIcon(ImageUtil.flipImage( - ImageUtil.grayscaleOffset( + ImageUtil.luminanceScale( ImageUtil.grayscaleImage(onSwitcher), 0.61f ), diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/screenmarkers/ui/ScreenMarkerPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/screenmarkers/ui/ScreenMarkerPanel.java index e9d827202b..782697f396 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/screenmarkers/ui/ScreenMarkerPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/screenmarkers/ui/ScreenMarkerPanel.java @@ -108,7 +108,7 @@ class ScreenMarkerPanel extends JPanel static { final BufferedImage borderImg = ImageUtil.getResourceStreamFromClass(ScreenMarkerPlugin.class, "border_color_icon.png"); - final BufferedImage borderImgHover = ImageUtil.grayscaleOffset(borderImg, -150); + final BufferedImage borderImgHover = ImageUtil.luminanceOffset(borderImg, -150); BORDER_COLOR_ICON = new ImageIcon(borderImg); BORDER_COLOR_HOVER_ICON = new ImageIcon(borderImgHover); @@ -116,7 +116,7 @@ class ScreenMarkerPanel extends JPanel NO_BORDER_COLOR_HOVER_ICON = new ImageIcon(ImageUtil.alphaOffset(borderImgHover, -100)); final BufferedImage fillImg = ImageUtil.getResourceStreamFromClass(ScreenMarkerPlugin.class, "fill_color_icon.png"); - final BufferedImage fillImgHover = ImageUtil.grayscaleOffset(fillImg, -150); + final BufferedImage fillImgHover = ImageUtil.luminanceOffset(fillImg, -150); FILL_COLOR_ICON = new ImageIcon(fillImg); FILL_COLOR_HOVER_ICON = new ImageIcon(fillImgHover); @@ -124,7 +124,7 @@ class ScreenMarkerPanel extends JPanel NO_FILL_COLOR_HOVER_ICON = new ImageIcon(ImageUtil.alphaOffset(fillImgHover, -100)); final BufferedImage opacityImg = ImageUtil.getResourceStreamFromClass(ScreenMarkerPlugin.class, "opacity_icon.png"); - final BufferedImage opacityImgHover = ImageUtil.grayscaleOffset(opacityImg, -150); + final BufferedImage opacityImgHover = ImageUtil.luminanceOffset(opacityImg, -150); FULL_OPACITY_ICON = new ImageIcon(opacityImg); FULL_OPACITY_HOVER_ICON = new ImageIcon(opacityImgHover); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/ClockTabPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/ClockTabPanel.java index 7eb2dfb9ca..cd1b1a5a2e 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/ClockTabPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/timetracking/clocks/ClockTabPanel.java @@ -75,15 +75,15 @@ public class ClockTabPanel extends TabContentPanel BufferedImage addIcon = ImageUtil.getResourceStreamFromClass(TimeTrackingPlugin.class, "add_icon.png"); DELETE_ICON = new ImageIcon(deleteIcon); - DELETE_ICON_HOVER = new ImageIcon(ImageUtil.grayscaleOffset(deleteIcon, -80)); + DELETE_ICON_HOVER = new ImageIcon(ImageUtil.luminanceOffset(deleteIcon, -80)); LAP_ICON = new ImageIcon(lapIcon); - LAP_ICON_HOVER = new ImageIcon(ImageUtil.grayscaleOffset(lapIcon, -80)); + LAP_ICON_HOVER = new ImageIcon(ImageUtil.luminanceOffset(lapIcon, -80)); PAUSE_ICON = new ImageIcon(pauseIcon); - PAUSE_ICON_HOVER = new ImageIcon(ImageUtil.grayscaleOffset(pauseIcon, -80)); + PAUSE_ICON_HOVER = new ImageIcon(ImageUtil.luminanceOffset(pauseIcon, -80)); RESET_ICON = new ImageIcon(resetIcon); - RESET_ICON_HOVER = new ImageIcon(ImageUtil.grayscaleOffset(resetIcon, -80)); + RESET_ICON_HOVER = new ImageIcon(ImageUtil.luminanceOffset(resetIcon, -80)); START_ICON = new ImageIcon(startIcon); - START_ICON_HOVER = new ImageIcon(ImageUtil.grayscaleOffset(startIcon, -80)); + START_ICON_HOVER = new ImageIcon(ImageUtil.luminanceOffset(startIcon, -80)); ADD_ICON = new ImageIcon(addIcon); ADD_ICON_HOVER = new ImageIcon(ImageUtil.alphaOffset(addIcon, 0.53f)); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldTableHeader.java b/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldTableHeader.java index 2d7ef01406..6e3c094ef0 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldTableHeader.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldTableHeader.java @@ -56,7 +56,7 @@ class WorldTableHeader extends JPanel { final BufferedImage arrowDown = ImageUtil.getResourceStreamFromClass(WorldHopperPlugin.class, "arrow_down.png"); final BufferedImage arrowUp = ImageUtil.rotateImage(arrowDown, Math.PI); - final BufferedImage arrowUpFaded = ImageUtil.grayscaleOffset(arrowUp, -80); + final BufferedImage arrowUpFaded = ImageUtil.luminanceOffset(arrowUp, -80); ARROW_UP = new ImageIcon(arrowUpFaded); final BufferedImage highlightArrowDown = ImageUtil.fillImage(arrowDown, HIGHLIGHT_COLOR); diff --git a/runelite-client/src/main/java/net/runelite/client/util/ImageUtil.java b/runelite-client/src/main/java/net/runelite/client/util/ImageUtil.java index 47df1a4349..afe7054d43 100644 --- a/runelite-client/src/main/java/net/runelite/client/util/ImageUtil.java +++ b/runelite-client/src/main/java/net/runelite/client/util/ImageUtil.java @@ -70,23 +70,37 @@ public class ImageUtil return (BufferedImage) image; } - final BufferedImage out = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_ARGB); - final Graphics2D g2d = out.createGraphics(); + return toARGB(image); + } + + /** + * Creates an ARGB {@link BufferedImage} from an {@link Image}. + */ + public static BufferedImage toARGB(final Image image) + { + if (image instanceof BufferedImage && ((BufferedImage) image).getType() == BufferedImage.TYPE_INT_ARGB) + { + return (BufferedImage) image; + } + + BufferedImage out = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = out.createGraphics(); g2d.drawImage(image, 0, 0, null); g2d.dispose(); return out; } /** - * Offsets an image in the grayscale (darkens/brightens) by a given offset. + * Offsets an image's luminance by a given value. * - * @param image The image to be darkened or brightened. + * @param rawImg The image to be darkened or brightened. * @param offset A signed 8-bit integer value to brighten or darken the image with. * Values above 0 will brighten, and values below 0 will darken. * @return The given image with its brightness adjusted by the given offset. */ - public static BufferedImage grayscaleOffset(final BufferedImage image, final int offset) + public static BufferedImage luminanceOffset(final Image rawImg, final int offset) { + BufferedImage image = toARGB(rawImg); final float offsetFloat = (float) offset; final int numComponents = image.getColorModel().getNumComponents(); final float[] scales = new float[numComponents]; @@ -104,15 +118,16 @@ public class ImageUtil } /** - * Offsets an image in the grayscale (darkens/brightens) by a given percentage. + * Changes an images luminance by a scaling factor * - * @param image The image to be darkened or brightened. + * @param rawImg The image to be darkened or brightened. * @param percentage The ratio to darken or brighten the given image. * Values above 1 will brighten, and values below 1 will darken. * @return The given image with its brightness scaled by the given percentage. */ - public static BufferedImage grayscaleOffset(final BufferedImage image, final float percentage) + public static BufferedImage luminanceScale(final Image rawImg, final float percentage) { + BufferedImage image = toARGB(rawImg); final int numComponents = image.getColorModel().getNumComponents(); final float[] scales = new float[numComponents]; final float[] offsets = new float[numComponents]; @@ -131,14 +146,15 @@ public class ImageUtil /** * Offsets an image's alpha component by a given offset. * - * @param image The image to be made more or less transparent. + * @param rawImg The image to be made more or less transparent. * @param offset A signed 8-bit integer value to modify the image's alpha component with. * Values above 0 will increase transparency, and values below 0 will decrease * transparency. * @return The given image with its alpha component adjusted by the given offset. */ - public static BufferedImage alphaOffset(final BufferedImage image, final int offset) + public static BufferedImage alphaOffset(final Image rawImg, final int offset) { + BufferedImage image = toARGB(rawImg); final float offsetFloat = (float) offset; final int numComponents = image.getColorModel().getNumComponents(); final float[] scales = new float[numComponents]; @@ -153,14 +169,15 @@ public class ImageUtil /** * Offsets an image's alpha component by a given percentage. * - * @param image The image to be made more or less transparent. + * @param rawImg The image to be made more or less transparent. * @param percentage The ratio to modify the image's alpha component with. * Values above 1 will increase transparency, and values below 1 will decrease * transparency. * @return The given image with its alpha component scaled by the given percentage. */ - public static BufferedImage alphaOffset(final BufferedImage image, final float percentage) + public static BufferedImage alphaOffset(final Image rawImg, final float percentage) { + BufferedImage image = toARGB(rawImg); final int numComponents = image.getColorModel().getNumComponents(); final float[] scales = new float[numComponents]; final float[] offsets = new float[numComponents]; diff --git a/runelite-client/src/test/java/net/runelite/client/util/ImageUtilTest.java b/runelite-client/src/test/java/net/runelite/client/util/ImageUtilTest.java index 829e90368b..44dc88d778 100644 --- a/runelite-client/src/test/java/net/runelite/client/util/ImageUtilTest.java +++ b/runelite-client/src/test/java/net/runelite/client/util/ImageUtilTest.java @@ -81,25 +81,25 @@ public class ImageUtilTest public void grayscaleOffset() { // grayscaleOffset(BufferedImage image, int offset) - assertTrue(bufferedImagesEqual(oneByOne(BLACK), ImageUtil.grayscaleOffset(oneByOne(BLACK), -255))); - assertTrue(bufferedImagesEqual(oneByOne(new Color(50, 50, 50)), ImageUtil.grayscaleOffset(oneByOne(BLACK), 50))); - assertTrue(bufferedImagesEqual(oneByOne(GRAY), ImageUtil.grayscaleOffset(oneByOne(BLACK), 128))); - assertTrue(bufferedImagesEqual(oneByOne(BLACK), ImageUtil.grayscaleOffset(oneByOne(GRAY), -255))); - assertTrue(bufferedImagesEqual(oneByOne(WHITE), ImageUtil.grayscaleOffset(oneByOne(BLACK), 255))); - assertTrue(bufferedImagesEqual(oneByOne(new Color(200, 200, 200)), ImageUtil.grayscaleOffset(oneByOne(WHITE), -55))); - assertTrue(bufferedImagesEqual(oneByOne(WHITE), ImageUtil.grayscaleOffset(oneByOne(WHITE), 55))); + assertTrue(bufferedImagesEqual(oneByOne(BLACK), ImageUtil.luminanceOffset(oneByOne(BLACK), -255))); + assertTrue(bufferedImagesEqual(oneByOne(new Color(50, 50, 50)), ImageUtil.luminanceOffset(oneByOne(BLACK), 50))); + assertTrue(bufferedImagesEqual(oneByOne(GRAY), ImageUtil.luminanceOffset(oneByOne(BLACK), 128))); + assertTrue(bufferedImagesEqual(oneByOne(BLACK), ImageUtil.luminanceOffset(oneByOne(GRAY), -255))); + assertTrue(bufferedImagesEqual(oneByOne(WHITE), ImageUtil.luminanceOffset(oneByOne(BLACK), 255))); + assertTrue(bufferedImagesEqual(oneByOne(new Color(200, 200, 200)), ImageUtil.luminanceOffset(oneByOne(WHITE), -55))); + assertTrue(bufferedImagesEqual(oneByOne(WHITE), ImageUtil.luminanceOffset(oneByOne(WHITE), 55))); // grayscaleOffset(BufferedImage image, float percentage) - assertTrue(bufferedImagesEqual(oneByOne(BLACK), ImageUtil.grayscaleOffset(oneByOne(BLACK), 0f))); - assertTrue(bufferedImagesEqual(oneByOne(BLACK), ImageUtil.grayscaleOffset(oneByOne(BLACK), 1f))); - assertTrue(bufferedImagesEqual(oneByOne(BLACK), ImageUtil.grayscaleOffset(oneByOne(BLACK), 2f))); - assertTrue(bufferedImagesEqual(oneByOne(BLACK), ImageUtil.grayscaleOffset(oneByOne(GRAY), 0f))); - assertTrue(bufferedImagesEqual(oneByOne(GRAY), ImageUtil.grayscaleOffset(oneByOne(GRAY), 1f))); - assertTrue(bufferedImagesEqual(oneByOne(WHITE), ImageUtil.grayscaleOffset(oneByOne(GRAY), 2f))); - assertTrue(bufferedImagesEqual(oneByOne(BLACK), ImageUtil.grayscaleOffset(oneByOne(WHITE), 0f))); - assertTrue(bufferedImagesEqual(oneByOne(GRAY), ImageUtil.grayscaleOffset(oneByOne(WHITE), 0.503f))); // grayscaleOffset does Math.floor - assertTrue(bufferedImagesEqual(oneByOne(WHITE), ImageUtil.grayscaleOffset(oneByOne(WHITE), 1f))); - assertTrue(bufferedImagesEqual(oneByOne(WHITE), ImageUtil.grayscaleOffset(oneByOne(WHITE), 2f))); + assertTrue(bufferedImagesEqual(oneByOne(BLACK), ImageUtil.luminanceScale(oneByOne(BLACK), 0f))); + assertTrue(bufferedImagesEqual(oneByOne(BLACK), ImageUtil.luminanceScale(oneByOne(BLACK), 1f))); + assertTrue(bufferedImagesEqual(oneByOne(BLACK), ImageUtil.luminanceScale(oneByOne(BLACK), 2f))); + assertTrue(bufferedImagesEqual(oneByOne(BLACK), ImageUtil.luminanceScale(oneByOne(GRAY), 0f))); + assertTrue(bufferedImagesEqual(oneByOne(GRAY), ImageUtil.luminanceScale(oneByOne(GRAY), 1f))); + assertTrue(bufferedImagesEqual(oneByOne(WHITE), ImageUtil.luminanceScale(oneByOne(GRAY), 2f))); + assertTrue(bufferedImagesEqual(oneByOne(BLACK), ImageUtil.luminanceScale(oneByOne(WHITE), 0f))); + assertTrue(bufferedImagesEqual(oneByOne(GRAY), ImageUtil.luminanceScale(oneByOne(WHITE), 0.503f))); // grayscaleOffset does Math.floor + assertTrue(bufferedImagesEqual(oneByOne(WHITE), ImageUtil.luminanceScale(oneByOne(WHITE), 1f))); + assertTrue(bufferedImagesEqual(oneByOne(WHITE), ImageUtil.luminanceScale(oneByOne(WHITE), 2f))); } @Test From 754ea00789ff0f4891063f3be3730d4aff4c3008 Mon Sep 17 00:00:00 2001 From: Max Weber Date: Wed, 27 Nov 2019 00:24:55 -0700 Subject: [PATCH 4/7] DynamicGridLayout: Take the container's insets into account --- .../java/net/runelite/client/ui/DynamicGridLayout.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/ui/DynamicGridLayout.java b/runelite-client/src/main/java/net/runelite/client/ui/DynamicGridLayout.java index 8be06a706c..120decaf13 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/DynamicGridLayout.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/DynamicGridLayout.java @@ -100,8 +100,11 @@ public class DynamicGridLayout extends GridLayout // scaling factors final Dimension pd = preferredLayoutSize(parent); - final double sw = (1.0 * parent.getWidth()) / pd.width; - final double sh = (1.0 * parent.getHeight()) / pd.height; + final Insets parentInsets = parent.getInsets(); + int wborder = parentInsets.left + parentInsets.right; + int hborder = parentInsets.top + parentInsets.bottom; + final double sw = (1.0 * parent.getWidth() - wborder) / (pd.width - wborder); + final double sh = (1.0 * parent.getHeight() - hborder) / (pd.height - hborder); final int[] w = new int[ncols]; final int[] h = new int[nrows]; From 7ff327c57ef11dccd76cde8cb3cdbf1f020ffcb7 Mon Sep 17 00:00:00 2001 From: Max Weber Date: Sat, 30 Nov 2019 22:21:46 -0700 Subject: [PATCH 5/7] SplashScreen: Ceiling download total --- .../src/main/java/net/runelite/client/ui/SplashScreen.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/ui/SplashScreen.java b/runelite-client/src/main/java/net/runelite/client/ui/SplashScreen.java index 6e945cb2a4..93313665e0 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/SplashScreen.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/SplashScreen.java @@ -206,8 +206,9 @@ public class SplashScreen extends JFrame implements ActionListener String progress; if (mib) { - final double MiB = 1024 * 1042; - progress = String.format("%.1f / %.1f MiB", done / MiB, total / MiB); + final double MiB = 1024 * 1024; + final double CEIL = 1.d / 10.d; + progress = String.format("%.1f / %.1f MiB", done / MiB, (total / MiB) + CEIL); } else { From 5a703157772ce3069bd6d20fe5e539f0fe5ec11b Mon Sep 17 00:00:00 2001 From: Max Weber Date: Sat, 30 Nov 2019 22:23:44 -0700 Subject: [PATCH 6/7] SplashScreen: Allow use after our L&F has been installed --- .../net/runelite/client/ui/SplashScreen.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/runelite-client/src/main/java/net/runelite/client/ui/SplashScreen.java b/runelite-client/src/main/java/net/runelite/client/ui/SplashScreen.java index 93313665e0..59f0b13610 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/SplashScreen.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/SplashScreen.java @@ -45,6 +45,8 @@ import javax.swing.UIManager; import javax.swing.border.EmptyBorder; import javax.swing.plaf.basic.BasicProgressBarUI; import lombok.extern.slf4j.Slf4j; +import net.runelite.client.ui.skin.SubstanceRuneLiteLookAndFeel; +import org.pushingpixels.substance.internal.SubstanceSynapse; @Slf4j public class SplashScreen extends JFrame implements ActionListener @@ -151,6 +153,11 @@ public class SplashScreen extends JFrame implements ActionListener } } + public static boolean isOpen() + { + return INSTANCE != null; + } + public static void init() { try @@ -164,8 +171,16 @@ public class SplashScreen extends JFrame implements ActionListener try { - UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName()); + boolean hasLAF = UIManager.getLookAndFeel() instanceof SubstanceRuneLiteLookAndFeel; + if (!hasLAF) + { + UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName()); + } INSTANCE = new SplashScreen(); + if (hasLAF) + { + INSTANCE.getRootPane().putClientProperty(SubstanceSynapse.COLORIZATION_FACTOR, 1.0); + } } catch (Exception e) { From 43390c0c2892df22e01677fdaa51470110b252f5 Mon Sep 17 00:00:00 2001 From: Max Weber Date: Sat, 30 Nov 2019 22:25:37 -0700 Subject: [PATCH 7/7] runelite-client: Add External Plugin support --- .../java/net/runelite/client/RuneLite.java | 11 +- .../runelite/client/RuneLiteProperties.java | 9 + .../client/config/RuneLiteConfig.java | 4 +- .../client/events/ExternalPluginsChanged.java | 38 ++ .../ExternalPluginClassLoader.java | 41 ++ .../externalplugins/ExternalPluginClient.java | 137 +++++ .../ExternalPluginManager.java | 366 ++++++++++++ .../ExternalPluginManifest.java | 84 +++ .../client/plugins/PluginManager.java | 94 +-- .../client/plugins/config/ConfigPanel.java | 33 +- .../config/PluginConfigurationDescriptor.java | 29 +- .../client/plugins/config/PluginHubPanel.java | 559 ++++++++++++++++++ .../client/plugins/config/PluginListItem.java | 40 +- .../plugins/config/PluginListPanel.java | 42 +- .../net/runelite/client/rs/ClientLoader.java | 2 + .../runelite/client/ui/FatalErrorDialog.java | 2 +- .../{rs => util}/CountingInputStream.java | 6 +- .../{rs => util}/VerificationException.java | 2 +- .../externalplugins/externalplugins.crt | 19 + .../plugins/config/pluginhub_configure.png | Bin 0 -> 410 bytes .../client/plugins/config/pluginhub_help.png | Bin 0 -> 477 bytes .../plugins/config/pluginhub_missingicon.png | Bin 0 -> 764 bytes .../net/runelite/client/runelite.properties | 2 + 23 files changed, 1447 insertions(+), 73 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/events/ExternalPluginsChanged.java create mode 100644 runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginClassLoader.java create mode 100644 runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginClient.java create mode 100644 runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginManager.java create mode 100644 runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginManifest.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/config/PluginHubPanel.java rename runelite-client/src/main/java/net/runelite/client/{rs => util}/CountingInputStream.java (93%) rename runelite-client/src/main/java/net/runelite/client/{rs => util}/VerificationException.java (97%) create mode 100644 runelite-client/src/main/resources/net/runelite/client/externalplugins/externalplugins.crt create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/config/pluginhub_configure.png create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/config/pluginhub_help.png create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/config/pluginhub_missingicon.png 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 f70ba2dd63..cb1ffd5ee2 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLite.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLite.java @@ -57,6 +57,7 @@ import net.runelite.client.game.ItemManager; import net.runelite.client.game.LootManager; import net.runelite.client.game.chatbox.ChatboxPanelManager; import net.runelite.client.menus.MenuManager; +import net.runelite.client.externalplugins.ExternalPluginManager; import net.runelite.client.plugins.PluginManager; import net.runelite.client.rs.ClientLoader; import net.runelite.client.rs.ClientUpdateCheckMode; @@ -80,6 +81,7 @@ public class RuneLite { 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 PLUGINS_DIR = new File(RUNELITE_DIR, "plugins"); public static final File PROFILES_DIR = new File(RUNELITE_DIR, "profiles"); public static final File SCREENSHOT_DIR = new File(RUNELITE_DIR, "screenshots"); public static final File LOGS_DIR = new File(RUNELITE_DIR, "logs"); @@ -90,6 +92,9 @@ public class RuneLite @Inject private PluginManager pluginManager; + @Inject + private ExternalPluginManager externalPluginManager; + @Inject private EventBus eventBus; @@ -288,12 +293,13 @@ public class RuneLite // Load the plugins, but does not start them yet. // This will initialize configuration pluginManager.loadCorePlugins(); + externalPluginManager.loadExternalPlugins(); SplashScreen.stage(.70, null, "Finalizing configuration"); // Plugins have provided their config, so set default config // to main settings - pluginManager.loadDefaultPluginConfiguration(); + pluginManager.loadDefaultPluginConfiguration(null); // Start client session clientSessionManager.start(); @@ -309,6 +315,7 @@ public class RuneLite // Register event listeners eventBus.register(clientUI); eventBus.register(pluginManager); + eventBus.register(externalPluginManager); eventBus.register(overlayManager); eventBus.register(drawManager); eventBus.register(infoBoxManager); @@ -337,7 +344,7 @@ public class RuneLite } // Start plugins - pluginManager.startCorePlugins(); + pluginManager.startPlugins(); SplashScreen.stop(); 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 60355dee62..6408be3522 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java @@ -28,6 +28,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.Properties; import javax.annotation.Nullable; +import okhttp3.HttpUrl; public class RuneLiteProperties { @@ -45,6 +46,8 @@ public class RuneLiteProperties private static final String DNS_CHANGE_LINK = "runelite.dnschange.link"; private static final String JAV_CONFIG = "runelite.jav_config"; private static final String JAV_CONFIG_BACKUP = "runelite.jav_config_backup"; + private static final String PLUGINHUB_BASE = "runelite.pluginhub.url"; + private static final String PLUGINHUB_VERSION = "runelite.pluginhub.version"; private static final Properties properties = new Properties(); @@ -130,4 +133,10 @@ public class RuneLiteProperties { return properties.getProperty(JAV_CONFIG_BACKUP); } + + public static HttpUrl getPluginHubBase() + { + String version = System.getProperty(PLUGINHUB_VERSION, properties.getProperty(PLUGINHUB_VERSION)); + return HttpUrl.parse(properties.get(PLUGINHUB_BASE) + "/" + version); + } } \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/config/RuneLiteConfig.java b/runelite-client/src/main/java/net/runelite/client/config/RuneLiteConfig.java index 2c4cf3597a..e28bcdcbf1 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/RuneLiteConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/config/RuneLiteConfig.java @@ -28,9 +28,11 @@ import java.awt.Dimension; import net.runelite.api.Constants; import net.runelite.client.ui.ContainableFrame; -@ConfigGroup("runelite") +@ConfigGroup(RuneLiteConfig.GROUP_NAME) public interface RuneLiteConfig extends Config { + String GROUP_NAME = "runelite"; + @ConfigItem( keyName = "gameSize", name = "Game size", diff --git a/runelite-client/src/main/java/net/runelite/client/events/ExternalPluginsChanged.java b/runelite-client/src/main/java/net/runelite/client/events/ExternalPluginsChanged.java new file mode 100644 index 0000000000..f38490121e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/events/ExternalPluginsChanged.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019 Abex + * 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 java.util.List; +import lombok.Value; +import net.runelite.client.externalplugins.ExternalPluginManifest; + +/** + * Posted when an external plugin has been added, removed, or updated + */ +@Value +public class ExternalPluginsChanged +{ + private final List loadedManifest; +} diff --git a/runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginClassLoader.java b/runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginClassLoader.java new file mode 100644 index 0000000000..054a7779b5 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginClassLoader.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019 Abex + * 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.externalplugins; + +import java.net.URL; +import java.net.URLClassLoader; +import lombok.Getter; + +class ExternalPluginClassLoader extends URLClassLoader +{ + @Getter + private final ExternalPluginManifest manifest; + + ExternalPluginClassLoader(ExternalPluginManifest manifest, URL[] urls) + { + super(urls, ExternalPluginClassLoader.class.getClassLoader()); + this.manifest = manifest; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginClient.java b/runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginClient.java new file mode 100644 index 0000000000..cd023c8c3a --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginClient.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2019 Abex + * 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.externalplugins; + +import com.google.common.reflect.TypeToken; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.util.List; +import javax.imageio.ImageIO; +import javax.inject.Inject; +import net.runelite.client.RuneLiteProperties; +import net.runelite.http.api.RuneLiteAPI; +import net.runelite.client.util.VerificationException; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okio.BufferedSource; + +public class ExternalPluginClient +{ + private final OkHttpClient cachingClient; + + @Inject + public ExternalPluginClient(OkHttpClient cachingClient) + { + this.cachingClient = cachingClient; + } + + public List downloadManifest() throws IOException, VerificationException + { + HttpUrl manifest = RuneLiteProperties.getPluginHubBase() + .newBuilder() + .addPathSegments("manifest.js") + .build(); + try (Response res = cachingClient.newCall(new Request.Builder().url(manifest).build()).execute()) + { + if (res.code() != 200) + { + throw new IOException("Non-OK response code: " + res.code()); + } + + BufferedSource src = res.body().source(); + + byte[] signature = new byte[src.readInt()]; + src.readFully(signature); + + byte[] data = src.readByteArray(); + Signature s = Signature.getInstance("SHA256withRSA"); + s.initVerify(loadCertificate()); + s.update(data); + + if (!s.verify(signature)) + { + throw new VerificationException("Unable to verify external plugin manifest"); + } + + return RuneLiteAPI.GSON.fromJson(new String(data, StandardCharsets.UTF_8), + new TypeToken>() + { + }.getType()); + } + catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) + { + throw new RuntimeException(e); + } + } + + public BufferedImage downloadIcon(ExternalPluginManifest plugin) throws IOException + { + if (!plugin.hasIcon()) + { + return null; + } + + HttpUrl url = RuneLiteProperties.getPluginHubBase() + .newBuilder() + .addPathSegment(plugin.getInternalName()) + .addPathSegment(plugin.getCommit() + ".png") + .build(); + + try (Response res = cachingClient.newCall(new Request.Builder().url(url).build()).execute()) + { + byte[] bytes = res.body().bytes(); + // We don't stream so the lock doesn't block the edt trying to load something at the same time + synchronized (ImageIO.class) + { + return ImageIO.read(new ByteArrayInputStream(bytes)); + } + } + } + + private static Certificate loadCertificate() + { + try + { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + Certificate certificate = certFactory.generateCertificate(ExternalPluginClient.class.getResourceAsStream("externalplugins.crt")); + return certificate; + } + catch (CertificateException e) + { + throw new RuntimeException(e); + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginManager.java b/runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginManager.java new file mode 100644 index 0000000000..531f443cf9 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginManager.java @@ -0,0 +1,366 @@ +/* + * Copyright (c) 2019 Abex + * 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.externalplugins; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; +import com.google.common.hash.Hashing; +import com.google.common.hash.HashingInputStream; +import com.google.common.io.Files; +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Function; +import javax.inject.Inject; +import javax.inject.Singleton; +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.RuneLite; +import net.runelite.client.RuneLiteProperties; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.config.RuneLiteConfig; +import net.runelite.client.eventbus.EventBus; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.ExternalPluginsChanged; +import net.runelite.client.events.SessionClose; +import net.runelite.client.events.SessionOpen; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginInstantiationException; +import net.runelite.client.plugins.PluginManager; +import net.runelite.client.ui.SplashScreen; +import net.runelite.client.util.CountingInputStream; +import net.runelite.client.util.Text; +import net.runelite.client.util.VerificationException; +import net.runelite.http.api.RuneLiteAPI; +import okhttp3.HttpUrl; +import okhttp3.Request; +import okhttp3.Response; + +@Singleton +@Slf4j +public class ExternalPluginManager +{ + private static final String PLUGIN_LIST_KEY = "externalPlugins"; + private static Class[] builtinExternals = null; + + @Inject + private ConfigManager configManager; + + @Inject + private ExternalPluginClient externalPluginClient; + + @Inject + private PluginManager pluginManager; + + @Inject + private ScheduledExecutorService executor; + + @Inject + private EventBus eventBus; + + public void loadExternalPlugins() throws PluginInstantiationException + { + refreshPlugins(); + + if (builtinExternals != null) + { + // builtin external's don't actually have a manifest or a separate classloader... + pluginManager.loadPlugins(Lists.newArrayList(builtinExternals), null); + } + } + + @Subscribe + public void onSessionOpen(SessionOpen event) + { + executor.submit(this::refreshPlugins); + } + + @Subscribe + public void onSessionClose(SessionClose event) + { + executor.submit(this::refreshPlugins); + } + + private void refreshPlugins() + { + Multimap loadedExternalPlugins = HashMultimap.create(); + for (Plugin p : pluginManager.getPlugins()) + { + ExternalPluginManifest m = getExternalPluginManifest(p.getClass()); + if (m != null) + { + loadedExternalPlugins.put(m, p); + } + } + + List installedIDs = getInstalledExternalPlugins(); + if (installedIDs.isEmpty() && loadedExternalPlugins.isEmpty()) + { + return; + } + + boolean startup = SplashScreen.isOpen(); + try + { + double splashStart = startup ? .60 : 0; + double splashLength = startup ? .10 : 1; + if (!startup) + { + SplashScreen.init(); + } + + SplashScreen.stage(splashStart, null, "Downloading external plugins"); + Set externalPlugins = new HashSet<>(); + + RuneLite.PLUGINS_DIR.mkdirs(); + + List manifestList; + try + { + manifestList = externalPluginClient.downloadManifest(); + Map manifests = manifestList + .stream().collect(ImmutableMap.toImmutableMap(ExternalPluginManifest::getInternalName, Function.identity())); + + Set needsDownload = new HashSet<>(); + Set keep = new HashSet<>(); + + for (String name : installedIDs) + { + ExternalPluginManifest manifest = manifests.get(name); + if (manifest != null) + { + externalPlugins.add(manifest); + + if (!manifest.isValid()) + { + needsDownload.add(manifest); + } + else + { + keep.add(manifest.getJarFile()); + } + } + } + + // delete old plugins + File[] files = RuneLite.PLUGINS_DIR.listFiles(); + if (files != null) + { + for (File fi : files) + { + if (!keep.contains(fi)) + { + fi.delete(); + } + } + } + + int toDownload = needsDownload.stream().mapToInt(ExternalPluginManifest::getSize).sum(); + int downloaded = 0; + + for (ExternalPluginManifest manifest : needsDownload) + { + HttpUrl url = RuneLiteProperties.getPluginHubBase().newBuilder() + .addPathSegment(manifest.getInternalName()) + .addPathSegment(manifest.getCommit() + ".jar") + .build(); + + try (Response res = RuneLiteAPI.CLIENT.newCall(new Request.Builder().url(url).build()).execute()) + { + int fdownloaded = downloaded; + downloaded += manifest.getSize(); + HashingInputStream his = new HashingInputStream(Hashing.sha256(), + new CountingInputStream(res.body().byteStream(), i -> + SplashScreen.stage(splashStart + (splashLength * .2), splashStart + (splashLength * .8), + null, "Downloading " + manifest.getDisplayName(), + i + fdownloaded, toDownload, true))); + Files.asByteSink(manifest.getJarFile()).writeFrom(his); + if (!his.hash().toString().equals(manifest.getHash())) + { + throw new VerificationException("Plugin " + manifest.getInternalName() + " didn't match its hash"); + } + } + catch (IOException | VerificationException e) + { + externalPlugins.remove(manifest); + log.error("Unable to download external plugin \"{}\"", manifest.getInternalName(), e); + } + } + } + catch (IOException | VerificationException e) + { + log.error("Unable to download external plugins", e); + return; + } + + SplashScreen.stage(splashStart + (splashLength * .8), null, "Starting external plugins"); + + // TODO(abex): make sure the plugins get fully removed from the scheduler/eventbus/other managers (iterate and check classloader) + Set add = new HashSet<>(); + for (ExternalPluginManifest ex : externalPlugins) + { + if (loadedExternalPlugins.removeAll(ex).size() <= 0) + { + add.add(ex); + } + } + // list of loaded external plugins that aren't in the manifest + Collection remove = loadedExternalPlugins.values(); + + for (Plugin p : remove) + { + log.info("Stopping external plugin \"{}\"", p.getClass()); + try + { + pluginManager.stopPlugin(p); + } + catch (PluginInstantiationException e) + { + log.warn("Unable to stop external plugin \"{}\"", p.getClass().getName(), e); + } + pluginManager.remove(p); + } + + for (ExternalPluginManifest manifest : add) + { + // I think this can't happen, but just in case + if (!manifest.isValid()) + { + log.warn("Invalid plugin for validated manifest: {}", manifest); + continue; + } + + log.info("Loading external plugin \"{}\" version \"{}\" commit \"{}\"", manifest.getInternalName(), manifest.getVersion(), manifest.getCommit()); + + List newPlugins = null; + try + { + ClassLoader cl = new ExternalPluginClassLoader(manifest, new URL[]{manifest.getJarFile().toURI().toURL()}); + List> clazzes = new ArrayList<>(); + for (String className : manifest.getPlugins()) + { + clazzes.add(cl.loadClass(className)); + } + + newPlugins = pluginManager.loadPlugins(clazzes, null); + if (!startup) + { + pluginManager.loadDefaultPluginConfiguration(newPlugins); + + for (Plugin p : newPlugins) + { + pluginManager.startPlugin(p); + } + } + } + catch (Exception e) + { + log.warn("Unable to start or load external plugin \"{}\"", manifest.getInternalName(), e); + if (newPlugins != null) + { + for (Plugin p : newPlugins) + { + try + { + pluginManager.stopPlugin(p); + } + catch (Exception inner) + { + } + pluginManager.remove(p); + } + } + } + } + + if (!startup) + { + eventBus.post(new ExternalPluginsChanged(manifestList)); + } + } + finally + { + if (!startup) + { + SplashScreen.stop(); + } + } + } + + public List getInstalledExternalPlugins() + { + String externalPluginsStr = configManager.getConfiguration(RuneLiteConfig.GROUP_NAME, PLUGIN_LIST_KEY); + return Text.fromCSV(externalPluginsStr == null ? "" : externalPluginsStr); + } + + public void install(String key) + { + Set plugins = new HashSet<>(getInstalledExternalPlugins()); + if (plugins.add(key)) + { + configManager.setConfiguration(RuneLiteConfig.GROUP_NAME, PLUGIN_LIST_KEY, Text.toCSV(plugins)); + executor.submit(this::refreshPlugins); + } + } + + public void remove(String key) + { + Set plugins = new HashSet<>(getInstalledExternalPlugins()); + if (plugins.remove(key)) + { + configManager.setConfiguration(RuneLiteConfig.GROUP_NAME, PLUGIN_LIST_KEY, Text.toCSV(plugins)); + executor.submit(this::refreshPlugins); + } + } + + public void update() + { + executor.submit(this::refreshPlugins); + } + + public static ExternalPluginManifest getExternalPluginManifest(Class plugin) + { + ClassLoader cl = plugin.getClassLoader(); + if (cl instanceof ExternalPluginClassLoader) + { + ExternalPluginClassLoader ecl = (ExternalPluginClassLoader) cl; + return ecl.getManifest(); + } + return null; + } + + public static void loadBuiltin(Class... plugins) + { + builtinExternals = plugins; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginManifest.java b/runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginManifest.java new file mode 100644 index 0000000000..95b5fe5637 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginManifest.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2019 Abex + * 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.externalplugins; + +import com.google.common.hash.Hashing; +import com.google.common.io.Files; +import java.io.File; +import java.io.IOException; +import java.net.URL; +import lombok.Data; +import lombok.EqualsAndHashCode; +import net.runelite.client.RuneLite; + +@Data +public class ExternalPluginManifest +{ + private String internalName; + private String commit; + private String hash; + private int size; + private String[] plugins; + + private String displayName; + private String version; + private String author; + private String description; + private String[] tags; + @EqualsAndHashCode.Exclude + private URL support; + private boolean hasIcon; + + public boolean hasIcon() + { + return hasIcon; + } + + File getJarFile() + { + return new File(RuneLite.PLUGINS_DIR, internalName + commit + ".jar"); + } + + boolean isValid() + { + File file = getJarFile(); + + try + { + if (file.exists()) + { + String hash = Files.asByteSource(file).hash(Hashing.sha256()).toString(); + if (this.hash.equals(hash)) + { + return true; + } + } + } + catch (IOException e) + { + } + return false; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java index 25d379c998..fadd54ebc9 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java @@ -25,7 +25,6 @@ package net.runelite.client.plugins; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.graph.Graph; import com.google.common.graph.GraphBuilder; @@ -49,6 +48,7 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ScheduledExecutorService; +import java.util.function.BiConsumer; import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Named; @@ -61,7 +61,6 @@ import net.runelite.client.events.SessionClose; import net.runelite.client.events.SessionOpen; import net.runelite.client.RuneLite; import net.runelite.client.config.Config; -import net.runelite.client.config.ConfigGroup; import net.runelite.client.config.ConfigManager; import net.runelite.client.config.RuneLiteConfig; import net.runelite.client.eventbus.EventBus; @@ -90,8 +89,6 @@ public class PluginManager private final Provider sceneTileManager; private final List plugins = new CopyOnWriteArrayList<>(); private final List activePlugins = new CopyOnWriteArrayList<>(); - private final String runeliteGroupName = RuneLiteConfig.class - .getAnnotation(ConfigGroup.class).value(); @Setter boolean isOutdated; @@ -128,15 +125,22 @@ public class PluginManager private void refreshPlugins() { - loadDefaultPluginConfiguration(); + loadDefaultPluginConfiguration(null); getPlugins() .forEach(plugin -> executor.submit(() -> { try { - if (!startPlugin(plugin)) + if (isPluginEnabled(plugin) != activePlugins.contains(plugin)) { - stopPlugin(plugin); + if (activePlugins.contains(plugin)) + { + stopPlugin(plugin); + } + else + { + startPlugin(plugin); + } } } catch (PluginInstantiationException e) @@ -162,11 +166,15 @@ public class PluginManager return null; } - public List getPluginConfigProxies() + public List getPluginConfigProxies(Collection plugins) { List injectors = new ArrayList<>(); - injectors.add(RuneLite.getInjector()); - getPlugins().forEach(pl -> injectors.add(pl.getInjector())); + if (plugins == null) + { + injectors.add(RuneLite.getInjector()); + plugins = getPlugins(); + } + plugins.forEach(pl -> injectors.add(pl.getInjector())); List list = new ArrayList<>(); for (Injector injector : injectors) @@ -185,20 +193,15 @@ public class PluginManager return list; } - public void loadDefaultPluginConfiguration() + public void loadDefaultPluginConfiguration(Collection plugins) { - for (Object config : getPluginConfigProxies()) + for (Object config : getPluginConfigProxies(plugins)) { configManager.setDefaultConfiguration(config, false); } } - public void loadCorePlugins() throws IOException - { - plugins.addAll(scanAndInstantiate(getClass().getClassLoader(), PLUGIN_PACKAGE)); - } - - public void startCorePlugins() + public void startPlugins() { List scannedPlugins = new ArrayList<>(plugins); int loaded = 0; @@ -219,37 +222,41 @@ public class PluginManager } } - List scanAndInstantiate(ClassLoader classLoader, String packageName) throws IOException + public void loadCorePlugins() throws IOException, PluginInstantiationException { SplashScreen.stage(.59, null, "Loading Plugins"); + ClassPath classPath = ClassPath.from(getClass().getClassLoader()); + + List> plugins = classPath.getTopLevelClassesRecursive(PLUGIN_PACKAGE).stream() + .map(ClassInfo::load) + .collect(Collectors.toList()); + + loadPlugins(plugins, (loaded, total) -> + SplashScreen.stage(.60, .70, null, "Loading Plugins", loaded, total, false)); + } + + public List loadPlugins(List> plugins, BiConsumer onPluginLoaded) throws PluginInstantiationException + { MutableGraph> graph = GraphBuilder .directed() .build(); - List scannedPlugins = new ArrayList<>(); - ClassPath classPath = ClassPath.from(classLoader); - - ImmutableSet classes = packageName == null ? classPath.getAllClasses() - : classPath.getTopLevelClassesRecursive(packageName); - for (ClassInfo classInfo : classes) + for (Class clazz : plugins) { - Class clazz = classInfo.load(); PluginDescriptor pluginDescriptor = clazz.getAnnotation(PluginDescriptor.class); if (pluginDescriptor == null) { if (clazz.getSuperclass() == Plugin.class) { - log.warn("Class {} is a plugin, but has no plugin descriptor", - clazz); + log.warn("Class {} is a plugin, but has no plugin descriptor", clazz); } continue; } if (clazz.getSuperclass() != Plugin.class) { - log.warn("Class {} has plugin descriptor, but is not a plugin", - clazz); + log.warn("Class {} has plugin descriptor, but is not a plugin", clazz); continue; } @@ -280,20 +287,22 @@ public class PluginManager if (Graphs.hasCycle(graph)) { - throw new RuntimeException("Plugin dependency graph contains a cycle!"); + throw new PluginInstantiationException("Plugin dependency graph contains a cycle!"); } List> sortedPlugins = topologicalSort(graph); sortedPlugins = Lists.reverse(sortedPlugins); int loaded = 0; + List newPlugins = new ArrayList<>(); for (Class pluginClazz : sortedPlugins) { Plugin plugin; try { - plugin = instantiate(scannedPlugins, (Class) pluginClazz); - scannedPlugins.add(plugin); + plugin = instantiate(this.plugins, (Class) pluginClazz); + newPlugins.add(plugin); + this.plugins.add(plugin); } catch (PluginInstantiationException ex) { @@ -301,10 +310,13 @@ public class PluginManager } loaded++; - SplashScreen.stage(.60, .70, null, "Loading Plugins", loaded, sortedPlugins.size(), false); + if (onPluginLoaded != null) + { + onPluginLoaded.accept(loaded, sortedPlugins.size()); + } } - return scannedPlugins; + return newPlugins; } public synchronized boolean startPlugin(Plugin plugin) throws PluginInstantiationException @@ -355,13 +367,11 @@ public class PluginManager public synchronized boolean stopPlugin(Plugin plugin) throws PluginInstantiationException { - if (!activePlugins.contains(plugin) || isPluginEnabled(plugin)) + if (!activePlugins.remove(plugin)) { return false; } - activePlugins.remove(plugin); - try { unschedule(plugin); @@ -395,13 +405,13 @@ public class PluginManager public void setPluginEnabled(Plugin plugin, boolean enabled) { final String keyName = plugin.getClass().getSimpleName().toLowerCase(); - configManager.setConfiguration(runeliteGroupName, keyName, String.valueOf(enabled)); + configManager.setConfiguration(RuneLiteConfig.GROUP_NAME, keyName, String.valueOf(enabled)); } public boolean isPluginEnabled(Plugin plugin) { final String keyName = plugin.getClass().getSimpleName().toLowerCase(); - final String value = configManager.getConfiguration(runeliteGroupName, keyName); + final String value = configManager.getConfiguration(RuneLiteConfig.GROUP_NAME, keyName); if (value != null) { @@ -465,12 +475,12 @@ public class PluginManager return plugin; } - void add(Plugin plugin) + public void add(Plugin plugin) { plugins.add(plugin); } - void remove(Plugin plugin) + public void remove(Plugin plugin) { plugins.remove(plugin); } 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 4ed399b78a..01989d39d9 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 @@ -44,6 +44,7 @@ import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JFormattedTextField; import javax.swing.JLabel; +import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPasswordField; @@ -66,7 +67,10 @@ import net.runelite.client.config.Keybind; import net.runelite.client.config.ModifierlessKeybind; import net.runelite.client.config.Range; import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.ExternalPluginsChanged; import net.runelite.client.events.PluginChanged; +import net.runelite.client.externalplugins.ExternalPluginManager; +import net.runelite.client.externalplugins.ExternalPluginManifest; import net.runelite.client.plugins.PluginManager; import net.runelite.client.ui.ColorScheme; import net.runelite.client.ui.DynamicGridLayout; @@ -83,8 +87,8 @@ import net.runelite.client.util.Text; class ConfigPanel extends PluginPanel { private static final int SPINNER_FIELD_WIDTH = 6; - private static final ImageIcon BACK_ICON; - private static final ImageIcon BACK_ICON_HOVER; + static final ImageIcon BACK_ICON; + static final ImageIcon BACK_ICON_HOVER; private final FixedWidthPanel mainPanel; private final JLabel title; @@ -99,6 +103,9 @@ class ConfigPanel extends PluginPanel @Inject private PluginManager pluginManager; + @Inject + private ExternalPluginManager externalPluginManager; + @Inject private ColorPickerManager colorPickerManager; @@ -162,7 +169,16 @@ class ConfigPanel extends PluginPanel title.setText(name); title.setForeground(Color.WHITE); title.setToolTipText("" + name + ":
" + pluginConfig.getDescription() + ""); - PluginListItem.addLabelPopupMenu(title, pluginConfig.createSupportMenuItem()); + + ExternalPluginManifest mf = pluginConfig.getExternalPluginManifest(); + JMenuItem uninstallItem = null; + if (mf != null) + { + uninstallItem = new JMenuItem("Uninstall"); + uninstallItem.addActionListener(ev -> externalPluginManager.remove(mf.getInternalName())); + } + + PluginListItem.addLabelPopupMenu(title, pluginConfig.createSupportMenuItem(), uninstallItem); if (pluginConfig.getPlugin() != null) { @@ -495,4 +511,15 @@ class ConfigPanel extends PluginPanel }); } } + + @Subscribe + private void onExternalPluginsChanged(ExternalPluginsChanged ev) + { + if (pluginManager.getPlugins().stream() + .noneMatch(p -> p == this.pluginConfig.getPlugin())) + { + pluginList.getMuxer().popState(); + } + SwingUtilities.invokeLater(this::rebuild); + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginConfigurationDescriptor.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginConfigurationDescriptor.java index 69932e1d61..279991656b 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginConfigurationDescriptor.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginConfigurationDescriptor.java @@ -29,6 +29,8 @@ import javax.swing.JMenuItem; import lombok.Value; import net.runelite.client.config.Config; import net.runelite.client.config.ConfigDescriptor; +import net.runelite.client.externalplugins.ExternalPluginManager; +import net.runelite.client.externalplugins.ExternalPluginManifest; import net.runelite.client.plugins.Plugin; import net.runelite.client.util.LinkBrowser; @@ -60,10 +62,35 @@ class PluginConfigurationDescriptor * * @return A {@link JMenuItem} which opens the plugin's wiki page URL in the browser when clicked */ + @Nullable JMenuItem createSupportMenuItem() { - final JMenuItem menuItem = new JMenuItem("Wiki"); + ExternalPluginManifest mf = getExternalPluginManifest(); + if (mf != null) + { + if (mf.getSupport() == null) + { + return null; + } + + JMenuItem menuItem = new JMenuItem("Support"); + menuItem.addActionListener(e -> LinkBrowser.browse(mf.getSupport().toString())); + return menuItem; + } + + JMenuItem menuItem = new JMenuItem("Wiki"); menuItem.addActionListener(e -> LinkBrowser.browse("https://github.com/runelite/runelite/wiki/" + name.replace(' ', '-'))); return menuItem; } + + @Nullable + ExternalPluginManifest getExternalPluginManifest() + { + if (plugin == null) + { + return null; + } + + return ExternalPluginManager.getExternalPluginManifest(plugin.getClass()); + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginHubPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginHubPanel.java new file mode 100644 index 0000000000..09907493ff --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginHubPanel.java @@ -0,0 +1,559 @@ +/* + * Copyright (c) 2019 Abex + * 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.plugins.config; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Multimap; +import com.google.common.collect.Sets; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Function; +import java.util.function.ToDoubleFunction; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.swing.AbstractAction; +import javax.swing.BorderFactory; +import javax.swing.GroupLayout; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.KeyStroke; +import javax.swing.LayoutStyle; +import javax.swing.ScrollPaneConstants; +import javax.swing.SwingUtilities; +import javax.swing.border.EmptyBorder; +import javax.swing.border.LineBorder; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.config.Config; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.ExternalPluginsChanged; +import net.runelite.client.externalplugins.ExternalPluginClient; +import net.runelite.client.externalplugins.ExternalPluginManager; +import net.runelite.client.externalplugins.ExternalPluginManifest; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginManager; +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.util.ImageUtil; +import net.runelite.client.util.LinkBrowser; +import net.runelite.client.util.SwingUtil; +import net.runelite.client.util.VerificationException; +import org.apache.commons.text.similarity.JaroWinklerDistance; + +@Slf4j +@Singleton +class PluginHubPanel extends PluginPanel +{ + private static final ImageIcon MISSING_ICON; + private static final ImageIcon HELP_ICON; + private static final ImageIcon HELP_ICON_HOVER; + private static final ImageIcon CONFIGURE_ICON; + private static final ImageIcon CONFIGURE_ICON_HOVER; + private static final Pattern SPACES = Pattern.compile(" +"); + private static final JaroWinklerDistance DISTANCE = new JaroWinklerDistance(); + + static + { + BufferedImage missingIcon = ImageUtil.getResourceStreamFromClass(PluginHubPanel.class, "pluginhub_missingicon.png"); + MISSING_ICON = new ImageIcon(missingIcon); + + BufferedImage helpIcon = ImageUtil.getResourceStreamFromClass(PluginHubPanel.class, "pluginhub_help.png"); + HELP_ICON = new ImageIcon(helpIcon); + HELP_ICON_HOVER = new ImageIcon(ImageUtil.alphaOffset(helpIcon, -100)); + + BufferedImage configureIcon = ImageUtil.getResourceStreamFromClass(PluginHubPanel.class, "pluginhub_configure.png"); + CONFIGURE_ICON = new ImageIcon(configureIcon); + CONFIGURE_ICON_HOVER = new ImageIcon(ImageUtil.alphaOffset(configureIcon, -100)); + } + + private class PluginItem extends JPanel + { + private static final int HEIGHT = 70; + private static final int ICON_WIDTH = 48; + private static final int BOTTOM_LINE_HEIGHT = 16; + static final float MIN_FILTER_SCORE = .8f; + + private final ExternalPluginManifest manifest; + + @Getter + private final boolean installed; + + @Getter + private float filter; + + PluginItem(ExternalPluginManifest newManifest, Collection loadedPlugins, boolean installed) + { + ExternalPluginManifest loaded = null; + if (!loadedPlugins.isEmpty()) + { + loaded = ExternalPluginManager.getExternalPluginManifest(loadedPlugins.iterator().next().getClass()); + } + + manifest = newManifest == null ? loaded : newManifest; + this.installed = installed; + + setBackground(ColorScheme.DARKER_GRAY_COLOR); + setOpaque(true); + + GroupLayout layout = new GroupLayout(this); + setLayout(layout); + + JLabel pluginName = new JLabel(manifest.getDisplayName()); + pluginName.setFont(FontManager.getRunescapeBoldFont()); + pluginName.setToolTipText(manifest.getDisplayName()); + + JLabel author = new JLabel(manifest.getAuthor()); + author.setFont(FontManager.getRunescapeSmallFont()); + author.setToolTipText(manifest.getAuthor()); + + JLabel version = new JLabel(manifest.getVersion()); + version.setFont(FontManager.getRunescapeSmallFont()); + version.setToolTipText(manifest.getVersion()); + + JLabel description = new JLabel(manifest.getDescription()); + description.setToolTipText(manifest.getDescription()); + + JLabel icon = new JLabel(); + icon.setHorizontalAlignment(JLabel.CENTER); + icon.setIcon(MISSING_ICON); + if (manifest.hasIcon()) + { + executor.submit(() -> + { + try + { + BufferedImage img = externalPluginClient.downloadIcon(manifest); + + SwingUtilities.invokeLater(() -> + { + icon.setIcon(new ImageIcon(img)); + }); + } + catch (IOException e) + { + log.info("Cannot download icon for plugin \"{}\"", manifest.getInternalName(), e); + } + }); + } + + JButton help = new JButton(HELP_ICON); + help.setRolloverIcon(HELP_ICON_HOVER); + SwingUtil.removeButtonDecorations(help); + help.setToolTipText("Help"); + help.setBorder(null); + if (manifest.getSupport() == null) + { + help.setVisible(false); + } + else + { + help.addActionListener(ev -> LinkBrowser.browse(manifest.getSupport().toString())); + } + + JButton configure = new JButton(CONFIGURE_ICON); + configure.setRolloverIcon(CONFIGURE_ICON_HOVER); + SwingUtil.removeButtonDecorations(configure); + configure.setToolTipText("Configure"); + help.setBorder(null); + if (loaded != null) + { + String search = null; + if (loadedPlugins.size() > 1) + { + search = loaded.getInternalName(); + } + else + { + Plugin plugin = loadedPlugins.iterator().next(); + Config cfg = pluginManager.getPluginConfigProxy(plugin); + if (cfg == null) + { + search = loaded.getInternalName(); + } + else + { + configure.addActionListener(l -> pluginListPanel.openConfigurationPanel(plugin)); + } + } + + if (search != null) + { + final String javaIsABadLanguage = search; + configure.addActionListener(l -> pluginListPanel.openWithFilter(javaIsABadLanguage)); + } + } + else + { + configure.setVisible(false); + } + + boolean install = !installed; + boolean update = loaded != null && newManifest != null && !newManifest.equals(loaded); + boolean remove = !install && !update; + JButton addrm = new JButton(); + if (install) + { + addrm.setText("Install"); + addrm.setBackground(new Color(0x28BE28)); + addrm.addActionListener(l -> externalPluginManager.install(manifest.getInternalName())); + } + else if (remove) + { + addrm.setText("Remove"); + addrm.setBackground(new Color(0xBE2828)); + addrm.addActionListener(l -> externalPluginManager.remove(manifest.getInternalName())); + } + else + { + assert update; + addrm.setText("Update"); + addrm.setBackground(new Color(0x1F621F)); + addrm.addActionListener(l -> externalPluginManager.update()); + } + addrm.setBorder(new LineBorder(addrm.getBackground().darker())); + addrm.setFocusPainted(false); + + layout.setHorizontalGroup(layout.createSequentialGroup() + .addComponent(icon, ICON_WIDTH, ICON_WIDTH, ICON_WIDTH) + .addGap(5) + .addGroup(layout.createParallelGroup() + .addGroup(layout.createSequentialGroup() + .addComponent(pluginName, 0, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE) + .addComponent(author, 0, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE)) + .addComponent(description, 0, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addComponent(version, 0, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.PREFERRED_SIZE, 100) + .addComponent(help, 0, 24, 24) + .addComponent(configure, 0, 24, 24) + .addComponent(addrm, 0, 50, GroupLayout.PREFERRED_SIZE) + .addGap(5)))); + + layout.setVerticalGroup(layout.createParallelGroup() + .addComponent(icon, HEIGHT, HEIGHT, HEIGHT) + .addGroup(layout.createSequentialGroup() + .addGap(5) + .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) + .addComponent(pluginName) + .addComponent(author)) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE) + .addComponent(description) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE) + .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) + .addComponent(version, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT) + .addComponent(help, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT) + .addComponent(configure, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT) + .addComponent(addrm, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT)) + .addGap(5))); + } + + float setFilter(String[] filter) + { + ToDoubleFunction match = r -> Stream.of(filter) + .mapToDouble(l -> Math.pow(DISTANCE.apply(l, r), 2)) + .max() + .orElse(0.D); + + double sim = SPACES.splitAsStream(manifest.getDisplayName()).collect(Collectors.averagingDouble(match)) * 2; + + if (manifest.getTags() != null) + { + sim += Stream.of(manifest.getTags()).mapToDouble(match).sum(); + } + + return this.filter = (float) sim; + } + } + + private final PluginListPanel pluginListPanel; + private final ExternalPluginManager externalPluginManager; + private final PluginManager pluginManager; + private final ExternalPluginClient externalPluginClient; + private final ScheduledExecutorService executor; + + private final IconTextField searchBar; + private final JLabel refreshing; + private final JPanel mainPanel; + private List plugins = null; + + @Inject + PluginHubPanel( + PluginListPanel pluginListPanel, + ExternalPluginManager externalPluginManager, + PluginManager pluginManager, + ExternalPluginClient externalPluginClient, + ScheduledExecutorService executor) + { + super(false); + this.pluginListPanel = pluginListPanel; + this.externalPluginManager = externalPluginManager; + this.pluginManager = pluginManager; + this.externalPluginClient = externalPluginClient; + this.executor = executor; + + { + Object refresh = "this could just be a lambda, but no, it has to be abstracted"; + getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0), refresh); + getActionMap().put(refresh, new AbstractAction() + { + @Override + public void actionPerformed(ActionEvent e) + { + reloadPluginList(); + } + }); + } + + GroupLayout layout = new GroupLayout(this); + setLayout(layout); + setBackground(ColorScheme.DARK_GRAY_COLOR); + + searchBar = new IconTextField(); + searchBar.setIcon(IconTextField.Icon.SEARCH); + searchBar.setBackground(ColorScheme.DARKER_GRAY_COLOR); + searchBar.setHoverBackgroundColor(ColorScheme.DARK_GRAY_HOVER_COLOR); + searchBar.getDocument().addDocumentListener(new DocumentListener() + { + @Override + public void insertUpdate(DocumentEvent e) + { + filter(); + } + + @Override + public void removeUpdate(DocumentEvent e) + { + filter(); + } + + @Override + public void changedUpdate(DocumentEvent e) + { + filter(); + } + }); + + JLabel externalPluginWarning = new JLabel("External plugins are not supported by the RuneLite Developers." + + "They may cause bugs or instability."); + externalPluginWarning.setBackground(new Color(0xFFBB33)); + externalPluginWarning.setForeground(Color.BLACK); + externalPluginWarning.setBorder(new EmptyBorder(5, 5, 5, 2)); + externalPluginWarning.setOpaque(true); + + JLabel externalPluginWarning2 = new JLabel("Use at your own risk!"); + externalPluginWarning2.setHorizontalAlignment(JLabel.CENTER); + externalPluginWarning2.setFont(FontManager.getRunescapeBoldFont()); + externalPluginWarning2.setBackground(externalPluginWarning.getBackground()); + externalPluginWarning2.setForeground(externalPluginWarning.getForeground()); + externalPluginWarning2.setBorder(new EmptyBorder(0, 5, 5, 5)); + externalPluginWarning2.setOpaque(true); + + JButton backButton = new JButton(ConfigPanel.BACK_ICON); + backButton.setRolloverIcon(ConfigPanel.BACK_ICON_HOVER); + SwingUtil.removeButtonDecorations(backButton); + backButton.setToolTipText("Back"); + backButton.addActionListener(l -> pluginListPanel.getMuxer().popState()); + + mainPanel = new JPanel(); + mainPanel.setBorder(BorderFactory.createEmptyBorder(0, 7, 7, 7)); + mainPanel.setLayout(new DynamicGridLayout(0, 1, 0, 5)); + mainPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + + refreshing = new JLabel("Loading..."); + refreshing.setHorizontalAlignment(JLabel.CENTER); + + JPanel mainPanelWrapper = new JPanel(); + mainPanelWrapper.setLayout(new BorderLayout()); + mainPanelWrapper.add(mainPanel, BorderLayout.NORTH); + mainPanelWrapper.add(refreshing, BorderLayout.CENTER); + + JScrollPane scrollPane = new JScrollPane(); + scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + scrollPane.setPreferredSize(new Dimension(Short.MAX_VALUE, Short.MAX_VALUE)); + scrollPane.getViewport().setLayout(new BorderLayout()); + scrollPane.getViewport().add(mainPanelWrapper, BorderLayout.CENTER); + + layout.setVerticalGroup(layout.createSequentialGroup() + .addComponent(externalPluginWarning) + .addComponent(externalPluginWarning2) + .addGap(10) + .addGroup(layout.createParallelGroup() + .addComponent(backButton) + .addComponent(searchBar)) + .addGap(10) + .addComponent(scrollPane)); + + layout.setHorizontalGroup(layout.createParallelGroup() + .addComponent(externalPluginWarning, 0, Short.MAX_VALUE, Short.MAX_VALUE) + .addComponent(externalPluginWarning2, 0, Short.MAX_VALUE, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addComponent(backButton) + .addComponent(searchBar) + .addGap(10)) + .addComponent(scrollPane)); + + revalidate(); + + refreshing.setVisible(false); + reloadPluginList(); + } + + private void reloadPluginList() + { + if (refreshing.isVisible()) + { + return; + } + + refreshing.setVisible(true); + mainPanel.removeAll(); + + executor.submit(() -> + { + List manifest; + try + { + manifest = externalPluginClient.downloadManifest(); + } + catch (IOException | VerificationException e) + { + log.error("", e); + SwingUtilities.invokeLater(() -> + { + refreshing.setVisible(false); + mainPanel.add(new JLabel("Downloading the plugin manifest failed")); + + JButton retry = new JButton("Retry"); + retry.addActionListener(l -> reloadPluginList()); + mainPanel.add(retry); + }); + return; + } + + reloadPluginList(manifest); + }); + } + + private void reloadPluginList(List manifest) + { + Map manifests = manifest.stream() + .collect(ImmutableMap.toImmutableMap(ExternalPluginManifest::getInternalName, Function.identity())); + + Multimap loadedPlugins = HashMultimap.create(); + for (Plugin p : pluginManager.getPlugins()) + { + Class clazz = p.getClass(); + ExternalPluginManifest mf = ExternalPluginManager.getExternalPluginManifest(clazz); + if (mf != null) + { + loadedPlugins.put(mf.getInternalName(), p); + } + } + + Set installed = new HashSet<>(externalPluginManager.getInstalledExternalPlugins()); + + SwingUtilities.invokeLater(() -> + { + plugins = Sets.union(manifests.keySet(), loadedPlugins.keySet()) + .stream() + .map(id -> new PluginItem(manifests.get(id), loadedPlugins.get(id), installed.contains(id))) + .collect(Collectors.toList()); + + refreshing.setVisible(false); + filter(); + }); + } + + void filter() + { + if (refreshing.isVisible()) + { + return; + } + + mainPanel.removeAll(); + + Stream stream = plugins.stream(); + + String search = searchBar.getText(); + boolean isSearching = search != null && !search.trim().isEmpty(); + if (isSearching) + { + String[] searchArray = SPACES.split(search.toLowerCase()); + stream = stream + .filter(p -> p.setFilter(searchArray) > PluginItem.MIN_FILTER_SCORE) + .sorted(Comparator.comparing(PluginItem::getFilter)); + } + else + { + stream = stream + .sorted(Comparator.comparing(PluginItem::isInstalled)); + } + + stream.forEach(mainPanel::add); + mainPanel.revalidate(); + } + + @Override + public void onActivate() + { + revalidate(); + searchBar.setText(""); + reloadPluginList(); + searchBar.requestFocusInWindow(); + } + + @Subscribe + private void onExternalPluginsChanged(ExternalPluginsChanged ev) + { + SwingUtilities.invokeLater(() -> reloadPluginList(ev.getLoadedManifest())); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListItem.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListItem.java index d0a61f0b7d..9b1575acc3 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListItem.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListItem.java @@ -35,7 +35,6 @@ import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.List; import javax.swing.ImageIcon; @@ -48,6 +47,7 @@ import javax.swing.JToggleButton; import javax.swing.SwingUtilities; import javax.swing.border.EmptyBorder; import lombok.Getter; +import net.runelite.client.externalplugins.ExternalPluginManifest; import net.runelite.client.ui.ColorScheme; import net.runelite.client.ui.PluginPanel; import net.runelite.client.util.ImageUtil; @@ -96,6 +96,11 @@ class PluginListItem extends JPanel Collections.addAll(keywords, pluginConfig.getName().toLowerCase().split(" ")); Collections.addAll(keywords, pluginConfig.getDescription().toLowerCase().split(" ")); Collections.addAll(keywords, pluginConfig.getTags()); + ExternalPluginManifest mf = pluginConfig.getExternalPluginManifest(); + if (mf != null) + { + keywords.add(mf.getInternalName()); + } final List popupMenuItems = new ArrayList<>(); @@ -127,6 +132,7 @@ class PluginListItem extends JPanel buttonPanel.setLayout(new GridLayout(1, 2)); add(buttonPanel, BorderLayout.LINE_END); + JMenuItem configMenuItem = null; if (pluginConfig.hasConfigurables()) { JButton configButton = new JButton(CONFIG_ICON); @@ -145,13 +151,18 @@ class PluginListItem extends JPanel configButton.setVisible(true); configButton.setToolTipText("Edit plugin configuration"); - final JMenuItem configMenuItem = new JMenuItem("Configure"); + configMenuItem = new JMenuItem("Configure"); configMenuItem.addActionListener(e -> openGroupConfigPanel()); - popupMenuItems.add(configMenuItem); } - popupMenuItems.add(pluginConfig.createSupportMenuItem()); - addLabelPopupMenu(nameLabel, popupMenuItems); + JMenuItem uninstallItem = null; + if (mf != null) + { + uninstallItem = new JMenuItem("Uninstall"); + uninstallItem.addActionListener(ev -> pluginListPanel.getExternalPluginManager().remove(mf.getInternalName())); + } + + addLabelPopupMenu(nameLabel, configMenuItem, pluginConfig.createSupportMenuItem(), uninstallItem); add(nameLabel, BorderLayout.CENTER); onOffToggle = new PluginToggleButton(); @@ -214,18 +225,6 @@ class PluginListItem extends JPanel pluginListPanel.openConfigurationPanel(pluginConfig); } - /** - * Adds a mouseover effect to change the text of the passed label to {@link ColorScheme#BRAND_ORANGE} color, and - * adds the passed menu item to a popup menu shown when the label is clicked. - * - * @param label The label to attach the mouseover and click effects to - * @param menuItem The menu item to be shown when the label is clicked - */ - static void addLabelPopupMenu(final JLabel label, final JMenuItem menuItem) - { - addLabelPopupMenu(label, Collections.singletonList(menuItem)); - } - /** * Adds a mouseover effect to change the text of the passed label to {@link ColorScheme#BRAND_ORANGE} color, and * adds the passed menu items to a popup menu shown when the label is clicked. @@ -233,7 +232,7 @@ class PluginListItem extends JPanel * @param label The label to attach the mouseover and click effects to * @param menuItems The menu items to be shown when the label is clicked */ - static void addLabelPopupMenu(final JLabel label, final Collection menuItems) + static void addLabelPopupMenu(JLabel label, JMenuItem... menuItems) { final JPopupMenu menu = new JPopupMenu(); final Color labelForeground = label.getForeground(); @@ -241,6 +240,11 @@ class PluginListItem extends JPanel for (final JMenuItem menuItem : menuItems) { + if (menuItem == null) + { + continue; + } + // Some machines register mouseEntered through a popup menu, and do not register mouseExited when a popup // menu item is clicked, so reset the label's color when we click one of these options. menuItem.addActionListener(e -> label.setForeground(labelForeground)); 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 abf9ab3bdb..b7b08b780b 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 @@ -37,6 +37,7 @@ import java.util.stream.Stream; import javax.inject.Inject; import javax.inject.Provider; import javax.inject.Singleton; +import javax.swing.JButton; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.ScrollPaneConstants; @@ -53,7 +54,9 @@ import net.runelite.client.config.ConfigManager; import net.runelite.client.config.RuneLiteConfig; import net.runelite.client.eventbus.EventBus; import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.ExternalPluginsChanged; import net.runelite.client.events.PluginChanged; +import net.runelite.client.externalplugins.ExternalPluginManager; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; import net.runelite.client.plugins.PluginInstantiationException; @@ -78,8 +81,12 @@ class PluginListPanel extends PluginPanel private final Provider configPanelProvider; private final List fakePlugins = new ArrayList<>(); + @Getter + private final ExternalPluginManager externalPluginManager; + @Getter private final MultiplexingPluginPanel muxer; + private final IconTextField searchBar; private final JScrollPane scrollPane; private final FixedWidthPanel mainPanel; @@ -89,14 +96,17 @@ class PluginListPanel extends PluginPanel public PluginListPanel( ConfigManager configManager, PluginManager pluginManager, + ExternalPluginManager externalPluginManager, ScheduledExecutorService executorService, EventBus eventBus, - Provider configPanelProvider) + Provider configPanelProvider, + Provider pluginHubPanelProvider) { super(false); this.configManager = configManager; this.pluginManager = pluginManager; + this.externalPluginManager = externalPluginManager; this.executorService = executorService; this.configPanelProvider = configPanelProvider; @@ -155,9 +165,15 @@ class PluginListPanel extends PluginPanel mainPanel.setLayout(new DynamicGridLayout(0, 1, 0, 5)); mainPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + JButton externalPluginButton = new JButton("Plugin Hub"); + externalPluginButton.setBorder(new EmptyBorder(5, 5, 5, 5)); + externalPluginButton.setLayout(new BorderLayout(0, BORDER_OFFSET)); + externalPluginButton.addActionListener(l -> muxer.pushState(pluginHubPanelProvider.get())); + JPanel northPanel = new FixedWidthPanel(); northPanel.setLayout(new BorderLayout()); northPanel.add(mainPanel, BorderLayout.NORTH); + northPanel.add(externalPluginButton, BorderLayout.SOUTH); scrollPane = new JScrollPane(northPanel); scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); @@ -225,6 +241,13 @@ class PluginListPanel extends PluginPanel scrollPane.getVerticalScrollBar().setValue(scrollBarPosition); } + void openWithFilter(String filter) + { + searchBar.setText(filter); + onSearchBarChanged(); + muxer.pushState(this); + } + private void onSearchBarChanged() { final String text = searchBar.getText(); @@ -267,6 +290,18 @@ class PluginListPanel extends PluginPanel } } + void openConfigurationPanel(Plugin plugin) + { + for (PluginListItem pluginListItem : pluginList) + { + if (pluginListItem.getPluginConfig().getPlugin() == plugin) + { + openConfigurationPanel(pluginListItem.getPluginConfig()); + break; + } + } + } + void openConfigurationPanel(PluginConfigurationDescriptor plugin) { ConfigPanel panel = configPanelProvider.get(); @@ -353,4 +388,9 @@ class PluginListPanel extends PluginPanel } } + @Subscribe + private void onExternalPluginsChanged(ExternalPluginsChanged ev) + { + SwingUtilities.invokeLater(this::rebuildPluginList); + } } diff --git a/runelite-client/src/main/java/net/runelite/client/rs/ClientLoader.java b/runelite-client/src/main/java/net/runelite/client/rs/ClientLoader.java index 10c9f76bc8..58993f63b7 100644 --- a/runelite-client/src/main/java/net/runelite/client/rs/ClientLoader.java +++ b/runelite-client/src/main/java/net/runelite/client/rs/ClientLoader.java @@ -65,8 +65,10 @@ import static net.runelite.client.rs.ClientUpdateCheckMode.NONE; import static net.runelite.client.rs.ClientUpdateCheckMode.VANILLA; import net.runelite.client.ui.FatalErrorDialog; import net.runelite.client.ui.SplashScreen; +import net.runelite.client.util.CountingInputStream; import net.runelite.http.api.RuneLiteAPI; import net.runelite.http.api.worlds.World; +import net.runelite.client.util.VerificationException; import okhttp3.HttpUrl; import okhttp3.Request; import okhttp3.Response; diff --git a/runelite-client/src/main/java/net/runelite/client/ui/FatalErrorDialog.java b/runelite-client/src/main/java/net/runelite/client/ui/FatalErrorDialog.java index 34517868d8..131bbb7830 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/FatalErrorDialog.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/FatalErrorDialog.java @@ -53,7 +53,7 @@ import javax.swing.border.EmptyBorder; import lombok.extern.slf4j.Slf4j; import net.runelite.client.RuneLite; import net.runelite.client.RuneLiteProperties; -import net.runelite.client.rs.VerificationException; +import net.runelite.client.util.VerificationException; import net.runelite.client.util.LinkBrowser; @Slf4j diff --git a/runelite-client/src/main/java/net/runelite/client/rs/CountingInputStream.java b/runelite-client/src/main/java/net/runelite/client/util/CountingInputStream.java similarity index 93% rename from runelite-client/src/main/java/net/runelite/client/rs/CountingInputStream.java rename to runelite-client/src/main/java/net/runelite/client/util/CountingInputStream.java index 5f44362b1a..8543d65295 100644 --- a/runelite-client/src/main/java/net/runelite/client/rs/CountingInputStream.java +++ b/runelite-client/src/main/java/net/runelite/client/util/CountingInputStream.java @@ -22,18 +22,18 @@ * (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.rs; +package net.runelite.client.util; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.util.function.IntConsumer; -class CountingInputStream extends FilterInputStream +public class CountingInputStream extends FilterInputStream { private final IntConsumer changed; - CountingInputStream(InputStream in, IntConsumer changed) + public CountingInputStream(InputStream in, IntConsumer changed) { super(in); this.changed = changed; diff --git a/runelite-client/src/main/java/net/runelite/client/rs/VerificationException.java b/runelite-client/src/main/java/net/runelite/client/util/VerificationException.java similarity index 97% rename from runelite-client/src/main/java/net/runelite/client/rs/VerificationException.java rename to runelite-client/src/main/java/net/runelite/client/util/VerificationException.java index 4138a12fd3..2f6f1f5dee 100644 --- a/runelite-client/src/main/java/net/runelite/client/rs/VerificationException.java +++ b/runelite-client/src/main/java/net/runelite/client/util/VerificationException.java @@ -22,7 +22,7 @@ * (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.rs; +package net.runelite.client.util; public class VerificationException extends Exception { diff --git a/runelite-client/src/main/resources/net/runelite/client/externalplugins/externalplugins.crt b/runelite-client/src/main/resources/net/runelite/client/externalplugins/externalplugins.crt new file mode 100644 index 0000000000..2ba1550b51 --- /dev/null +++ b/runelite-client/src/main/resources/net/runelite/client/externalplugins/externalplugins.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDDDCCAfSgAwIBAgIJAK8uBanmNQZaMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV +BAMMEHJ1bmVsaXRlLXBsdWdpbnMwHhcNMTkxMjEyMjEwNzUxWhcNMjUxMjEwMjEw +NzUxWjAbMRkwFwYDVQQDDBBydW5lbGl0ZS1wbHVnaW5zMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEApu11OVANSU+pHaXRxB7fIZapucJ6BT46neicEixs +NVPuK/QRVjO/G8F++MXFD/tlZUOEDllDN8uaHBIVwxilqEVYL7oX65Esl7qqC1TZ +WGdjiMyYoK3CXWEWB4w+CdB31T7JG2HqH45ZsVs+U9OVWBkNkL5nNQNPOmZFd+3A +yCb9nGlO7SxduiHpwh3CV19jY47y8tevyo5qpaBuQeWtu3vbpeer0kbDarwD3xoF +yUMPRK518gxRUSmOpsSG5viQ731mKVCUUfIXz91d3s+kJYAjORHS4zJe9s+1dljp +oLYNLkaP6m3CmNtC84OxkmognvZTNMbiQ3GQm/BK4sdjPQIDAQABo1MwUTAdBgNV +HQ4EFgQUxrkiRXNd0OHPMkqgl9UgV1//OuQwHwYDVR0jBBgwFoAUxrkiRXNd0OHP +Mkqgl9UgV1//OuQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA +StPyblz3aqOM5z2KqHX1B7Z3Q8B58g55YSefpcfwWEc6LT4HCztszcZDteWpV3W2 +ERfemkGKgsDhQ0qkzIt7tS5eNN3PPj7RZZm7vl5HquQ1vC/33ri/Z3CEKzbW7knt +i1iEpx8E9DKb9J9DjdKwNxSomOyCOFUt9YoQJs80xc1mwPDd6aWR3xwvnEUimkm+ +Dbj7HMOXLeyN810wkeWcT8nC5GhxH3ZAmVExBHsaIOB876RntzshBehjY8s8JQhw +R+fT1e8EhYMM9ylYDk1KIWFWrAujjU04lS9tXZ5C2e7fr9R953XN6Y0PNM/taNTU +GzwGroJZI02V+1ADO14rRA== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/config/pluginhub_configure.png b/runelite-client/src/main/resources/net/runelite/client/plugins/config/pluginhub_configure.png new file mode 100644 index 0000000000000000000000000000000000000000..5cf5a192e56e9ff7870ea5e09fbd9ed83e127f2a GIT binary patch literal 410 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbMf&@~GAIf1 z3ua(sVrF4w=j7uT5R{No*U&fji;hpq?&zO1b@kd!yY?M8c=*)CtGDjld-3}1`>#L$ z8v6dM1L|$_ba4!kkn}ysF4Sbe;CfI#+2QED>dvkk_y78*8}+zopr0As?#R{#J2 literal 0 HcmV?d00001 diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/config/pluginhub_help.png b/runelite-client/src/main/resources/net/runelite/client/plugins/config/pluginhub_help.png new file mode 100644 index 0000000000000000000000000000000000000000..ffe3b6084021e9b5702f245a1524caa1152db101 GIT binary patch literal 477 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbMf_5hy{S0KG~2!dBDI_3eLZc`HE z7tFxO#=*nOCoUnQqNc8A>g(qp7#0~9pH^C4-_Y6JH*wOG*>mSDTd{imhV47|>^pw` z>h+tq?%aF!;{ErZf4NU?V*#2o)6>ND!zC z_2T7i?&%)04nKYVJNM!OF~7B;?kW#G_sY#%b4Byw(tS@{?&ixnMBh~}i7HqSdTi&e zm-l{fIqF9IPM_!;Joj_>r{nD<{K|{9nmYse+1!ph yRD`WR^~rJW#1`Z1(35_4yZ-;$VV(0~zHU;md{@Me*CjwlGI+ZBxvX zWm9uU|HLV?W-nd4e*3O{M~|O4cmCq#tJiNmd-3x1$L~LX|9#LmPnUs#F~ZZuF~sBe z+AA-E4jG8BKIrC(aJ}Pjr1l`Ixrlg($Q{SriQB&aoqtqhw_5Hj>&HJQ&$PZjv+U9m zhEHo={s?9KYI4ET@#g$qo48fKF5CC#y4F9cx)c2Q_`G`g;7{)J{-5xQJs11BYhPl< z;;$Dz@11i;nQzXq#$D;Xnm3kLT+681C;#o%+yP)^Ykkt+BJ^DM?EI}im~O{joc`zLj^57RuJ_{2?dz1(Ol$Jn zzJHOKEcH=t*Nh7h+mdY~B<|n%`nTqbM55Q3oh!~S`SDPvJ|K5dyZB1|uJDIZVK=&V zT1#&_Hhm0Wt2blX!$C^zrXCOCcpT~bk^SAYfoTS(Vukb zlzFvsZ?(QSzrL<^$;NwG)c)Gnd&~69mNSafM6Hm6!Fdr@EdROP`&?s%w>*gh#vy~J LtDnm{r-UW|