From 22bc73b9ab023fa11455d2ba4790d1bc9d6f070e Mon Sep 17 00:00:00 2001 From: takuyakanbr Date: Mon, 25 Jun 2018 00:31:57 +0800 Subject: [PATCH] config panel: change plugin search to use plugin name + tags --- .../client/plugins/config/ConfigPanel.java | 417 +++++++----------- .../client/plugins/config/ConfigPlugin.java | 2 +- .../client/plugins/config/FuzzySearch.java | 131 ------ .../client/plugins/config/PluginListItem.java | 248 +++++++++++ 4 files changed, 416 insertions(+), 382 deletions(-) delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/config/FuzzySearch.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListItem.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 2a5573f0ed..3e186e72c7 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 @@ -29,7 +29,6 @@ import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; -import java.awt.GridLayout; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.awt.event.ItemEvent; @@ -37,11 +36,10 @@ import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; -import java.awt.image.BufferedImage; import java.io.IOException; +import java.util.ArrayList; import java.util.Comparator; -import java.util.Map; -import java.util.TreeMap; +import java.util.List; import java.util.concurrent.ScheduledExecutorService; import javax.imageio.ImageIO; import javax.swing.BorderFactory; @@ -84,30 +82,32 @@ 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.IconTextField; -import net.runelite.client.util.SwingUtil; @Slf4j public class ConfigPanel extends PluginPanel { private static final int SPINNER_FIELD_WIDTH = 6; - - 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 SEARCH; + private static final String RUNELITE_PLUGIN = "RuneLite"; + private static final String CHAT_COLOR_PLUGIN = "Chat Color"; + + private final PluginManager pluginManager; + private final ConfigManager configManager; + private final ScheduledExecutorService executorService; + private final RuneLiteConfig runeLiteConfig; + private final ChatColorConfig chatColorConfig; + private final IconTextField searchBar = new IconTextField(); + private final List pluginList = new ArrayList<>(); + private DisplayMode currentMode = DisplayMode.PLUGIN_LIST; + private int scrollBarPosition = 0; + static { try { synchronized (ImageIO.class) { - BufferedImage configIcon = ImageIO.read(ConfigPanel.class.getResourceAsStream("config_edit_icon.png")); - CONFIG_ICON = new ImageIcon(configIcon); - CONFIG_ICON_HOVER = new ImageIcon(SwingUtil.grayscaleOffset(configIcon, -100)); - ON_SWITCHER = new ImageIcon(ImageIO.read(ConfigPanel.class.getResourceAsStream("switchers/on.png"))); - OFF_SWITCHER = new ImageIcon(ImageIO.read(ConfigPanel.class.getResourceAsStream("switchers/off.png"))); SEARCH = new ImageIcon(ImageIO.read(IconTextField.class.getResourceAsStream("search.png"))); } } @@ -117,16 +117,7 @@ public class ConfigPanel extends PluginPanel } } - private final PluginManager pluginManager; - private final ConfigManager configManager; - private final ScheduledExecutorService executorService; - private final RuneLiteConfig runeLiteConfig; - private final ChatColorConfig chatColorConfig; - private final IconTextField searchBar = new IconTextField(); - private Map children = new TreeMap<>(); - private int scrollBarPosition = 0; - - public ConfigPanel(PluginManager pluginManager, ConfigManager configManager, ScheduledExecutorService executorService, + ConfigPanel(PluginManager pluginManager, ConfigManager configManager, ScheduledExecutorService executorService, RuneLiteConfig runeLiteConfig, ChatColorConfig chatColorConfig) { super(); @@ -165,210 +156,55 @@ public class ConfigPanel extends PluginPanel setLayout(new DynamicGridLayout(0, 1, 0, 5)); setBackground(ColorScheme.DARK_GRAY_COLOR); - rebuildPluginList(); - openConfigList(); + initializePluginList(); + refreshPluginList(); } - final void rebuildPluginList() + private void initializePluginList() { - scrollBarPosition = getScrollPane().getVerticalScrollBar().getValue(); - Map newChildren = new TreeMap<>(); - + // populate pluginList with all non-hidden plugins pluginManager.getPlugins().stream() .filter(plugin -> !plugin.getClass().getAnnotation(PluginDescriptor.class).hidden()) - .sorted(Comparator.comparing(left -> left.getClass().getAnnotation(PluginDescriptor.class).name())) .forEach(plugin -> { - final Config pluginConfigProxy = pluginManager.getPluginConfigProxy(plugin); - final String pluginName = plugin.getClass().getAnnotation(PluginDescriptor.class).name(); + final Config config = pluginManager.getPluginConfigProxy(plugin); + final ConfigDescriptor configDescriptor = config == null ? null : configManager.getConfigDescriptor(config); - final JPanel groupPanel = buildGroupPanel(); - - JLabel name = new JLabel(pluginName); - name.setForeground(Color.WHITE); - - groupPanel.add(name, BorderLayout.CENTER); - - final JPanel buttonPanel = new JPanel(); - buttonPanel.setLayout(new GridLayout(1, 2)); - groupPanel.add(buttonPanel, BorderLayout.LINE_END); - - final JLabel editConfigButton = buildConfigButton(pluginConfigProxy); - buttonPanel.add(editConfigButton); - - final JLabel toggleButton = buildToggleButton(plugin); - toggleButton.setHorizontalAlignment(SwingConstants.RIGHT); - buttonPanel.add(toggleButton); - - newChildren.put(pluginName, groupPanel); + pluginList.add(new PluginListItem(this, plugin, config, configDescriptor)); }); - addCoreConfig(newChildren, "RuneLite", runeLiteConfig); - addCoreConfig(newChildren, "Chat Color", chatColorConfig); + // add special entries for core client configurations + pluginList.add(new PluginListItem(this, runeLiteConfig, configManager.getConfigDescriptor(runeLiteConfig), + RUNELITE_PLUGIN, "RuneLite client settings", "client")); + pluginList.add(new PluginListItem(this, chatColorConfig, configManager.getConfigDescriptor(chatColorConfig), + CHAT_COLOR_PLUGIN, "Recolor chat text", "colour", "messages")); - children = newChildren; - openConfigList(); + pluginList.sort(Comparator.comparing(PluginListItem::getName)); } - private void addCoreConfig(Map newChildren, String configName, Config config) + void refreshPluginList() { - final JPanel groupPanel = buildGroupPanel(); + scrollBarPosition = getScrollPane().getVerticalScrollBar().getValue(); - JLabel name = new JLabel(configName); - name.setForeground(Color.WHITE); - - groupPanel.add(name, BorderLayout.CENTER); - - final JPanel buttonPanel = new JPanel(); - buttonPanel.setLayout(new GridLayout(1, 2)); - groupPanel.add(buttonPanel, BorderLayout.LINE_END); - - final JLabel editConfigButton = buildConfigButton(config); - buttonPanel.add(editConfigButton); - - final JLabel toggleButton = buildToggleButton(null); - toggleButton.setVisible(false); - buttonPanel.add(toggleButton); - - newChildren.put(configName, groupPanel); - } - - private JPanel buildGroupPanel() - { - // Create base panel for the config button and enabled/disabled button - final JPanel groupPanel = new JPanel(); - groupPanel.setLayout(new BorderLayout(3, 0)); - groupPanel.setPreferredSize(new Dimension(PluginPanel.PANEL_WIDTH, 20)); - return groupPanel; - } - - private JLabel buildConfigButton(Config config) - { - // Create edit config button and disable it by default - final JLabel editConfigButton = new JLabel(CONFIG_ICON); - editConfigButton.setPreferredSize(new Dimension(25, 0)); - editConfigButton.setVisible(false); - - // If we have configuration proxy enable the button and add edit config listener - if (config != null) + // update enabled / disabled status of all items + pluginList.forEach(listItem -> { - final ConfigDescriptor configDescriptor = configManager.getConfigDescriptor(config); - final boolean configEmpty = configDescriptor.getItems().stream().allMatch(item -> item.getItem().hidden()); - - if (!configEmpty) + final Plugin plugin = listItem.getPlugin(); + if (plugin != null) { - editConfigButton.addMouseListener(new MouseAdapter() - { - @Override - public void mousePressed(MouseEvent mouseEvent) - { - editConfigButton.setIcon(CONFIG_ICON); - openGroupConfigPanel(config, configDescriptor, configManager); - } - - @Override - public void mouseEntered(MouseEvent e) - { - editConfigButton.setIcon(CONFIG_ICON_HOVER); - } - - @Override - public void mouseExited(MouseEvent e) - { - editConfigButton.setIcon(CONFIG_ICON); - } - }); - editConfigButton.setVisible(true); - editConfigButton.setToolTipText("Edit plugin configuration"); - } - } - - return editConfigButton; - } - - private JLabel buildToggleButton(Plugin plugin) - { - // Create enabling/disabling button - final JLabel toggleButton = new JLabel(ON_SWITCHER); - toggleButton.setPreferredSize(new Dimension(25, 0)); - - if (plugin == null) - { - toggleButton.setEnabled(false); - return toggleButton; - } - - highlightButton(toggleButton, pluginManager.isPluginEnabled(plugin)); - - toggleButton.addMouseListener(new MouseAdapter() - { - @Override - public void mousePressed(MouseEvent mouseEvent) - { - executorService.submit(() -> - { - final boolean enabled = pluginManager.isPluginEnabled(plugin); - pluginManager.setPluginEnabled(plugin, !enabled); - - try - { - if (enabled) - { - pluginManager.stopPlugin(plugin); - } - else - { - pluginManager.startPlugin(plugin); - } - } - catch (PluginInstantiationException ex) - { - log.warn("Error during starting/stopping plugin {}", plugin.getClass().getSimpleName(), ex); - } - - highlightButton(toggleButton, !enabled); - }); + listItem.setPluginEnabled(pluginManager.isPluginEnabled(plugin)); } }); - return toggleButton; - } - - private void highlightButton(JLabel button, boolean enabled) - { - button.setIcon(enabled ? ON_SWITCHER : OFF_SWITCHER); - button.setToolTipText(enabled ? "Disable plugin" : "Enable plugin"); - } - - private void onSearchBarChanged() - { - final String text = searchBar.getText(); - - children.values().forEach(this::remove); - - if (text.isEmpty()) + if (currentMode == DisplayMode.PLUGIN_LIST) { - children.values().forEach(this::add); - revalidate(); - return; - } - - FuzzySearch.findAndProcess(text, children.keySet(), (k) -> add(children.get(k))); - revalidate(); - } - - @Override - public void onActivate() - { - super.onActivate(); - if (searchBar.getParent() != null) - { - searchBar.requestFocusInWindow(); + openConfigList(); } } private void openConfigList() { + currentMode = DisplayMode.PLUGIN_LIST; removeAll(); JLabel title = new JLabel("Configuration", SwingConstants.LEFT); @@ -384,62 +220,35 @@ public class ConfigPanel extends PluginPanel scrollbar.getVerticalScrollBar().setValue(scrollBarPosition); } - private void changeConfiguration(Config config, JComponent component, ConfigDescriptor cd, ConfigItemDescriptor cid) + private void onSearchBarChanged() { - ConfigItem configItem = cid.getItem(); + final String text = searchBar.getText(); - if (!Strings.isNullOrEmpty(configItem.warning())) + pluginList.forEach(this::remove); + + if (text.isEmpty()) { - final int result = JOptionPane.showOptionDialog(component, configItem.warning(), - "Are you sure?", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, - null, new String[]{"Yes", "No"}, "No"); + pluginList.forEach(this::add); + revalidate(); + return; + } - if (result != JOptionPane.YES_OPTION) + // show plugins with keywords that matches all the given search terms + final String[] searchTerms = text.toLowerCase().split(" "); + pluginList.forEach(listItem -> + { + if (listItem.matchesSearchTerms(searchTerms)) { - openGroupConfigPanel(config, cd, configManager); - return; + add(listItem); } - } + }); - if (component instanceof JCheckBox) - { - JCheckBox checkbox = (JCheckBox) component; - configManager.setConfiguration(cd.getGroup().keyName(), cid.getItem().keyName(), "" + checkbox.isSelected()); - } - - if (component instanceof JSpinner) - { - JSpinner spinner = (JSpinner) component; - configManager.setConfiguration(cd.getGroup().keyName(), cid.getItem().keyName(), "" + spinner.getValue()); - } - - if (component instanceof JTextArea) - { - JTextArea textField = (JTextArea) component; - configManager.setConfiguration(cd.getGroup().keyName(), cid.getItem().keyName(), textField.getText()); - } - - if (component instanceof JColorChooser) - { - JColorChooser jColorChooser = (JColorChooser) component; - configManager.setConfiguration(cd.getGroup().keyName(), cid.getItem().keyName(), String.valueOf(jColorChooser.getColor().getRGB())); - } - - if (component instanceof JComboBox) - { - JComboBox jComboBox = (JComboBox) component; - configManager.setConfiguration(cd.getGroup().keyName(), cid.getItem().keyName(), ((Enum) jComboBox.getSelectedItem()).name()); - } - - if (component instanceof HotkeyButton) - { - HotkeyButton hotkeyButton = (HotkeyButton) component; - configManager.setConfiguration(cd.getGroup().keyName(), cid.getItem().keyName(), hotkeyButton.getValue()); - } + revalidate(); } - private void openGroupConfigPanel(Config config, ConfigDescriptor cd, ConfigManager configManager) + void openGroupConfigPanel(Config config, ConfigDescriptor cd) { + currentMode = DisplayMode.CONFIG_PANEL; scrollBarPosition = getScrollPane().getVerticalScrollBar().getValue(); removeAll(); String name = cd.getGroup().name() + " Configuration"; @@ -647,7 +456,7 @@ public class ConfigPanel extends PluginPanel configManager.setDefaultConfiguration(config, true); // Reload configuration panel - openGroupConfigPanel(config, cd, configManager); + openGroupConfigPanel(config, cd); }); add(resetButton); @@ -658,4 +467,112 @@ public class ConfigPanel extends PluginPanel revalidate(); getScrollPane().getVerticalScrollBar().setValue(0); } + + private void changeConfiguration(Config config, JComponent component, ConfigDescriptor cd, ConfigItemDescriptor cid) + { + ConfigItem configItem = cid.getItem(); + + if (!Strings.isNullOrEmpty(configItem.warning())) + { + final int result = JOptionPane.showOptionDialog(component, configItem.warning(), + "Are you sure?", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, + null, new String[]{"Yes", "No"}, "No"); + + if (result != JOptionPane.YES_OPTION) + { + openGroupConfigPanel(config, cd); + return; + } + } + + if (component instanceof JCheckBox) + { + JCheckBox checkbox = (JCheckBox) component; + configManager.setConfiguration(cd.getGroup().keyName(), cid.getItem().keyName(), "" + checkbox.isSelected()); + } + + if (component instanceof JSpinner) + { + JSpinner spinner = (JSpinner) component; + configManager.setConfiguration(cd.getGroup().keyName(), cid.getItem().keyName(), "" + spinner.getValue()); + } + + if (component instanceof JTextArea) + { + JTextArea textField = (JTextArea) component; + configManager.setConfiguration(cd.getGroup().keyName(), cid.getItem().keyName(), textField.getText()); + } + + if (component instanceof JColorChooser) + { + JColorChooser jColorChooser = (JColorChooser) component; + configManager.setConfiguration(cd.getGroup().keyName(), cid.getItem().keyName(), String.valueOf(jColorChooser.getColor().getRGB())); + } + + if (component instanceof JComboBox) + { + JComboBox jComboBox = (JComboBox) component; + configManager.setConfiguration(cd.getGroup().keyName(), cid.getItem().keyName(), ((Enum) jComboBox.getSelectedItem()).name()); + } + + if (component instanceof HotkeyButton) + { + HotkeyButton hotkeyButton = (HotkeyButton) component; + configManager.setConfiguration(cd.getGroup().keyName(), cid.getItem().keyName(), hotkeyButton.getValue()); + } + } + + 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); + }); + } + + @Override + public void onActivate() + { + super.onActivate(); + if (searchBar.getParent() != null) + { + searchBar.requestFocusInWindow(); + } + } + + enum DisplayMode + { + PLUGIN_LIST, + CONFIG_PANEL + } } \ No newline at end of file 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 f61c244c97..28040909f3 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 @@ -98,6 +98,6 @@ public class ConfigPlugin extends Plugin @Subscribe public void onPluginChanged(PluginChanged event) { - SwingUtilities.invokeLater(configPanel::rebuildPluginList); + SwingUtilities.invokeLater(configPanel::refreshPluginList); } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/FuzzySearch.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/FuzzySearch.java deleted file mode 100644 index 72f03e6c6e..0000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/config/FuzzySearch.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright (c) 2018, Tomas Slusny - * 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.util.Collection; -import java.util.Set; -import java.util.function.Consumer; -import java.util.stream.Collectors; -import lombok.Value; -import org.apache.commons.text.similarity.JaroWinklerDistance; - -public class FuzzySearch -{ - private static final JaroWinklerDistance FUZZY_SCORE = new JaroWinklerDistance(); - private static final double STRING_OCCURRENCE_MULTIPLIER = 3d; - - /** - * Try to find a match and call callback on each match, sorted by score and filtered by average - * - * @param query query to search for - * @param entries entries to search in - * @param callback callback to call - */ - public static void findAndProcess(final String query, final Collection entries, final Consumer callback) - { - // Calculate score for each entry matching query - final Set matches = entries.stream() - .map(entry -> new FuzzyMatch( - FUZZY_SCORE.apply(query, entry) - + calculateStringOccurrenceBonus(entry, query) - * STRING_OCCURRENCE_MULTIPLIER, - entry)) - .collect(Collectors.toSet()); - - // Calculate average score of the matches to filter out the less relevant ones - final double average = matches.stream().mapToDouble(m -> m.score).average().orElse(0); - final double max = matches.stream().mapToDouble(m -> m.score).max().orElse(0); - final double limit = Math.min(average * 1.7, max); - - matches.stream() - .filter(m -> m.score >= limit) - .sorted((left, right) -> Double.compare(right.score, left.score)) - .map(m -> m.value) - .forEach(callback); - } - - /** - * Calculates string occurrence bonus of query in the entry string - * @param entry entry string - * @param query query string - * @return string occurrence bonus - */ - private static double calculateStringOccurrenceBonus(final String entry, final String query) - { - // Exit early, no occurrence bonus for too long query - if (query.length() > entry.length()) - { - return 0; - } - - // Create relaxed variants of the input (e.g lower cased ones) - final String relaxedEntry = entry.toLowerCase(); - final String relaxedQuery = query.toLowerCase(); - - // Create base bonus - final double base = 1d / 6d; - - if (entry.equals(query)) - { - return base * 6d; - } - if (entry.equals(relaxedQuery) || relaxedQuery.equals(entry)) - { - return base * 5d; - } - if (relaxedEntry.equals(relaxedQuery)) - { - return base * 4d; - } - if (entry.contains(query)) - { - return base * 3d; - } - if (entry.contains(relaxedQuery) || relaxedEntry.contains(query)) - { - return base * 2d; - } - if (relaxedEntry.contains(relaxedQuery)) - { - return base; - } - - return 0; - } - - @Value - private static class FuzzyMatch - { - /** - * Score of the match - */ - double score; - - /** - * Match value - */ - String value; - } -} 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 new file mode 100644 index 0000000000..bbdd4b6954 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListItem.java @@ -0,0 +1,248 @@ +/* + * 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.plugins.config; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.GridLayout; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nullable; +import javax.imageio.ImageIO; +import javax.swing.ImageIcon; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.SwingConstants; +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.PluginPanel; +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 ImageIcon CONFIG_ICON; + private static final ImageIcon CONFIG_ICON_HOVER; + private static final ImageIcon ON_SWITCHER; + private static final ImageIcon OFF_SWITCHER; + + private final ConfigPanel configPanel; + private @Getter @Nullable final Plugin plugin; + private @Nullable final Config config; + private @Nullable final ConfigDescriptor configDescriptor; + + private final String name; + private final String description; + private final List keywords = new ArrayList<>(); + + private final JLabel configButton = new JLabel(CONFIG_ICON); + private final JLabel toggleButton = new JLabel(OFF_SWITCHER); + + private boolean isPluginEnabled = false; + + static + { + try + { + synchronized (ImageIO.class) + { + BufferedImage configIcon = ImageIO.read(ConfigPanel.class.getResourceAsStream("config_edit_icon.png")); + CONFIG_ICON = new ImageIcon(configIcon); + CONFIG_ICON_HOVER = new ImageIcon(SwingUtil.grayscaleOffset(configIcon, -100)); + ON_SWITCHER = new ImageIcon(ImageIO.read(ConfigPanel.class.getResourceAsStream("switchers/on.png"))); + OFF_SWITCHER = new ImageIcon(ImageIO.read(ConfigPanel.class.getResourceAsStream("switchers/off.png"))); + } + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + /** + * 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, @Nullable Config config, + @Nullable ConfigDescriptor configDescriptor) + { + final PluginDescriptor descriptor = plugin.getClass().getAnnotation(PluginDescriptor.class); + + this.configPanel = configPanel; + this.plugin = plugin; + this.config = config; + this.configDescriptor = configDescriptor; + this.name = descriptor.name(); + this.description = descriptor.description(); + Collections.addAll(keywords, name.toLowerCase().split(" ")); + Collections.addAll(keywords, descriptor.tags()); + + initialize(); + } + + /** + * Creates a new {@code PluginListItem} for a core configuration. + */ + PluginListItem(ConfigPanel configPanel, Config config, ConfigDescriptor configDescriptor, + String name, String description, String... tags) + { + this.configPanel = configPanel; + this.plugin = null; + this.config = config; + this.configDescriptor = configDescriptor; + this.name = name; + this.description = description; + Collections.addAll(keywords, name.toLowerCase().split(" ")); + Collections.addAll(keywords, tags); + + initialize(); + } + + private void initialize() + { + setLayout(new BorderLayout(3, 0)); + setPreferredSize(new Dimension(PluginPanel.PANEL_WIDTH, 20)); + + JLabel nameLabel = new JLabel(name); + nameLabel.setForeground(Color.WHITE); + + if (!description.isEmpty()) + { + nameLabel.setToolTipText("" + name + ":
" + description + ""); + } + + add(nameLabel, BorderLayout.CENTER); + + 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); + attachConfigButtonListener(); + buttonPanel.add(configButton); + + toggleButton.setPreferredSize(new Dimension(25, 0)); + toggleButton.setHorizontalAlignment(SwingConstants.RIGHT); + attachToggleButtonListener(); + buttonPanel.add(toggleButton); + } + + private void attachConfigButtonListener() + { + // no need for a listener if there are no config item to show + if (config == null || configDescriptor.getItems().stream().allMatch(item -> item.getItem().hidden())) + { + return; + } + + configButton.addMouseListener(new MouseAdapter() + { + @Override + public void mousePressed(MouseEvent mouseEvent) + { + configButton.setIcon(CONFIG_ICON); + configPanel.openGroupConfigPanel(config, configDescriptor); + } + + @Override + public void mouseEntered(MouseEvent e) + { + configButton.setIcon(CONFIG_ICON_HOVER); + } + + @Override + public void mouseExited(MouseEvent e) + { + configButton.setIcon(CONFIG_ICON); + } + }); + configButton.setVisible(true); + configButton.setToolTipText("Edit plugin configuration"); + } + + private void attachToggleButtonListener() + { + // no need for a listener if there is no plugin to enable / disable + if (plugin == null) + { + toggleButton.setEnabled(false); + return; + } + + toggleButton.addMouseListener(new MouseAdapter() + { + @Override + public void mousePressed(MouseEvent mouseEvent) + { + if (isPluginEnabled) + { + configPanel.stopPlugin(plugin, PluginListItem.this); + } + else + { + configPanel.startPlugin(plugin, PluginListItem.this); + } + } + }); + } + + void setPluginEnabled(boolean enabled) + { + isPluginEnabled = enabled; + toggleButton.setIcon(enabled ? ON_SWITCHER : OFF_SWITCHER); + toggleButton.setToolTipText(enabled ? "Disable plugin" : "Enable plugin"); + } + + /** + * 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) + { + for (String term : searchTerms) + { + if (keywords.stream().noneMatch((t) -> t.contains(term) || + DISTANCE.apply(t, term) > 0.9)) + { + return false; + } + } + return true; + } +}