From 30681d89357ed814193e15df89d369351edf6f5b Mon Sep 17 00:00:00 2001 From: Hydrox6 Date: Sat, 9 May 2020 17:29:14 +0100 Subject: [PATCH] config: add support for sections --- .../client/config/ConfigDescriptor.java | 16 +-- .../runelite/client/config/ConfigItem.java | 2 + .../client/config/ConfigItemDescriptor.java | 20 ++- .../runelite/client/config/ConfigManager.java | 28 ++++- .../runelite/client/config/ConfigObject.java | 32 +++++ .../runelite/client/config/ConfigSection.java | 43 +++++++ .../config/ConfigSectionDescriptor.java | 52 ++++++++ .../client/plugins/config/ConfigPanel.java | 117 +++++++++++++++++- 8 files changed, 295 insertions(+), 15 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/config/ConfigObject.java create mode 100644 runelite-client/src/main/java/net/runelite/client/config/ConfigSection.java create mode 100644 runelite-client/src/main/java/net/runelite/client/config/ConfigSectionDescriptor.java diff --git a/runelite-client/src/main/java/net/runelite/client/config/ConfigDescriptor.java b/runelite-client/src/main/java/net/runelite/client/config/ConfigDescriptor.java index 34db13d2e9..b234e756d6 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/ConfigDescriptor.java +++ b/runelite-client/src/main/java/net/runelite/client/config/ConfigDescriptor.java @@ -24,26 +24,20 @@ */ package net.runelite.client.config; +import lombok.Getter; import java.util.Collection; +@Getter public class ConfigDescriptor { private final ConfigGroup group; + private final Collection sections; private final Collection items; - public ConfigDescriptor(ConfigGroup group, Collection items) + public ConfigDescriptor(ConfigGroup group, Collection sections, Collection items) { this.group = group; + this.sections = sections; this.items = items; } - - public ConfigGroup getGroup() - { - return group; - } - - public Collection getItems() - { - return items; - } } diff --git a/runelite-client/src/main/java/net/runelite/client/config/ConfigItem.java b/runelite-client/src/main/java/net/runelite/client/config/ConfigItem.java index b50094826e..a9a511cc08 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/ConfigItem.java +++ b/runelite-client/src/main/java/net/runelite/client/config/ConfigItem.java @@ -46,4 +46,6 @@ public @interface ConfigItem String warning() default ""; boolean secret() default false; + + String section() default ""; } diff --git a/runelite-client/src/main/java/net/runelite/client/config/ConfigItemDescriptor.java b/runelite-client/src/main/java/net/runelite/client/config/ConfigItemDescriptor.java index 852d9fa873..d6ced58e18 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/ConfigItemDescriptor.java +++ b/runelite-client/src/main/java/net/runelite/client/config/ConfigItemDescriptor.java @@ -27,11 +27,29 @@ package net.runelite.client.config; import lombok.Value; @Value -public class ConfigItemDescriptor +public class ConfigItemDescriptor implements ConfigObject { private final ConfigItem item; private final Class type; private final Range range; private final Alpha alpha; private final Units units; + + @Override + public String key() + { + return item.keyName(); + } + + @Override + public String name() + { + return item.name(); + } + + @Override + public int position() + { + return item.position(); + } } diff --git a/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java b/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java index 61c748047c..e4cbdcc5b1 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java +++ b/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java @@ -468,8 +468,32 @@ public class ConfigManager throw new IllegalArgumentException("Not a config group"); } + final List sections = Arrays.stream(inter.getDeclaredFields()) + .filter(m -> m.isAnnotationPresent(ConfigSection.class) && m.getType() == String.class) + .map(m -> + { + try + { + return new ConfigSectionDescriptor( + String.valueOf(m.get(inter)), + m.getDeclaredAnnotation(ConfigSection.class) + ); + } + catch (IllegalAccessException e) + { + log.warn("Unable to load section {}::{}", inter.getSimpleName(), m.getName()); + return null; + } + }) + .filter(Objects::nonNull) + .sorted((a, b) -> ComparisonChain.start() + .compare(a.getSection().position(), b.getSection().position()) + .compare(a.getSection().name(), b.getSection().name()) + .result()) + .collect(Collectors.toList()); + final List items = Arrays.stream(inter.getMethods()) - .filter(m -> m.getParameterCount() == 0) + .filter(m -> m.getParameterCount() == 0 && m.isAnnotationPresent(ConfigItem.class)) .map(m -> new ConfigItemDescriptor( m.getDeclaredAnnotation(ConfigItem.class), m.getReturnType(), @@ -483,7 +507,7 @@ public class ConfigManager .result()) .collect(Collectors.toList()); - return new ConfigDescriptor(group, items); + return new ConfigDescriptor(group, sections, items); } /** diff --git a/runelite-client/src/main/java/net/runelite/client/config/ConfigObject.java b/runelite-client/src/main/java/net/runelite/client/config/ConfigObject.java new file mode 100644 index 0000000000..0e8010e58e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/config/ConfigObject.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020, Hydrox6 + * 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.config; + +public interface ConfigObject +{ + String key(); + String name(); + int position(); +} diff --git a/runelite-client/src/main/java/net/runelite/client/config/ConfigSection.java b/runelite-client/src/main/java/net/runelite/client/config/ConfigSection.java new file mode 100644 index 0000000000..4a0f722d3f --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/config/ConfigSection.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019, Hydrox6 + * 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.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface ConfigSection +{ + String name(); + + String description(); + + int position(); + + boolean closedByDefault() default false; +} diff --git a/runelite-client/src/main/java/net/runelite/client/config/ConfigSectionDescriptor.java b/runelite-client/src/main/java/net/runelite/client/config/ConfigSectionDescriptor.java new file mode 100644 index 0000000000..9a8447ee49 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/config/ConfigSectionDescriptor.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2020, Hydrox6 + * 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.config; + +import lombok.Value; + +@Value +public class ConfigSectionDescriptor implements ConfigObject +{ + private final String key; + private final ConfigSection section; + + @Override + public String key() + { + return key; + } + + @Override + public String name() + { + return section.name(); + } + + @Override + public int position() + { + return section.position(); + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPanel.java index 20c04f79fd..db39c25e50 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 @@ -25,6 +25,7 @@ package net.runelite.client.plugins.config; import com.google.common.base.Strings; +import com.google.common.collect.ComparisonChain; import com.google.common.primitives.Ints; import java.awt.BorderLayout; import java.awt.Color; @@ -36,8 +37,12 @@ import java.awt.event.ItemEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; import javax.inject.Inject; import javax.swing.BorderFactory; +import javax.swing.BoxLayout; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JCheckBox; @@ -55,7 +60,9 @@ import javax.swing.ScrollPaneConstants; import javax.swing.SpinnerModel; import javax.swing.SpinnerNumberModel; import javax.swing.SwingUtilities; +import javax.swing.border.CompoundBorder; import javax.swing.border.EmptyBorder; +import javax.swing.border.MatteBorder; import javax.swing.event.ChangeListener; import javax.swing.text.JTextComponent; import lombok.extern.slf4j.Slf4j; @@ -63,6 +70,9 @@ import net.runelite.client.config.ConfigDescriptor; import net.runelite.client.config.ConfigItem; import net.runelite.client.config.ConfigItemDescriptor; import net.runelite.client.config.ConfigManager; +import net.runelite.client.config.ConfigObject; +import net.runelite.client.config.ConfigSection; +import net.runelite.client.config.ConfigSectionDescriptor; import net.runelite.client.config.Keybind; import net.runelite.client.config.ModifierlessKeybind; import net.runelite.client.config.Range; @@ -76,6 +86,7 @@ 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.ComboBoxListRenderer; import net.runelite.client.ui.components.colorpicker.ColorPickerManager; @@ -89,9 +100,15 @@ import net.runelite.client.util.Text; class ConfigPanel extends PluginPanel { private static final int SPINNER_FIELD_WIDTH = 6; + private static final ImageIcon SECTION_EXPAND_ICON; + private static final ImageIcon SECTION_EXPAND_ICON_HOVER; + private static final ImageIcon SECTION_RETRACT_ICON; + private static final ImageIcon SECTION_RETRACT_ICON_HOVER; static final ImageIcon BACK_ICON; static final ImageIcon BACK_ICON_HOVER; + private static final Map sectionExpandStates = new HashMap<>(); + private final FixedWidthPanel mainPanel; private final JLabel title; private final PluginToggleButton pluginToggle; @@ -118,6 +135,14 @@ class ConfigPanel extends PluginPanel final BufferedImage backIcon = ImageUtil.getResourceStreamFromClass(ConfigPanel.class, "config_back_icon.png"); BACK_ICON = new ImageIcon(backIcon); BACK_ICON_HOVER = new ImageIcon(ImageUtil.alphaOffset(backIcon, -100)); + + BufferedImage sectionRetractIcon = ImageUtil.getResourceStreamFromClass(ConfigPanel.class, "/util/arrow_right.png"); + sectionRetractIcon = ImageUtil.luminanceOffset(sectionRetractIcon, -121); + SECTION_EXPAND_ICON = new ImageIcon(sectionRetractIcon); + SECTION_EXPAND_ICON_HOVER = new ImageIcon(ImageUtil.alphaOffset(sectionRetractIcon, -100)); + final BufferedImage sectionExpandIcon = ImageUtil.rotateImage(sectionRetractIcon, Math.PI / 2); + SECTION_RETRACT_ICON = new ImageIcon(sectionExpandIcon); + SECTION_RETRACT_ICON_HOVER = new ImageIcon(ImageUtil.alphaOffset(sectionExpandIcon, -100)); } public ConfigPanel() @@ -205,11 +230,91 @@ class ConfigPanel extends PluginPanel rebuild(); } + private void toggleSection(ConfigSectionDescriptor csd, JButton button, JPanel contents) + { + boolean newState = !contents.isVisible(); + contents.setVisible(newState); + button.setIcon(newState ? SECTION_RETRACT_ICON : SECTION_EXPAND_ICON); + button.setRolloverIcon(newState ? SECTION_RETRACT_ICON_HOVER : SECTION_EXPAND_ICON_HOVER); + button.setToolTipText(newState ? "Retract" : "Expand"); + sectionExpandStates.put(csd, newState); + SwingUtilities.invokeLater(contents::revalidate); + } + private void rebuild() { mainPanel.removeAll(); ConfigDescriptor cd = pluginConfig.getConfigDescriptor(); + + final Map sectionWidgets = new HashMap<>(); + final Map topLevelPanels = new TreeMap<>((a, b) -> + ComparisonChain.start() + .compare(a.position(), b.position()) + .compare(a.name(), b.name()) + .result()); + + for (ConfigSectionDescriptor csd : cd.getSections()) + { + ConfigSection cs = csd.getSection(); + final boolean isOpen = sectionExpandStates.getOrDefault(csd, !cs.closedByDefault()); + + final JPanel section = new JPanel(); + section.setLayout(new BoxLayout(section, BoxLayout.Y_AXIS)); + section.setMinimumSize(new Dimension(PANEL_WIDTH, 0)); + + final JPanel sectionHeader = new JPanel(); + sectionHeader.setLayout(new BorderLayout()); + sectionHeader.setMinimumSize(new Dimension(PANEL_WIDTH, 0)); + // For whatever reason, the header extends out by a single pixel when closed. Adding a single pixel of + // border on the right only affects the width when closed, fixing the issue. + sectionHeader.setBorder(new CompoundBorder( + new MatteBorder(0, 0, 1, 0, ColorScheme.MEDIUM_GRAY_COLOR), + new EmptyBorder(0, 0, 3, 1))); + section.add(sectionHeader, BorderLayout.NORTH); + + final JButton sectionToggle = new JButton(isOpen ? SECTION_RETRACT_ICON : SECTION_EXPAND_ICON); + sectionToggle.setRolloverIcon(isOpen ? SECTION_RETRACT_ICON_HOVER : SECTION_EXPAND_ICON_HOVER); + sectionToggle.setPreferredSize(new Dimension(18, 0)); + sectionToggle.setBorder(new EmptyBorder(0, 0, 0, 5)); + sectionToggle.setToolTipText(isOpen ? "Retract" : "Expand"); + SwingUtil.removeButtonDecorations(sectionToggle); + sectionHeader.add(sectionToggle, BorderLayout.WEST); + + String name = cs.name(); + final JLabel sectionName = new JLabel(name); + sectionName.setForeground(ColorScheme.BRAND_ORANGE); + sectionName.setFont(FontManager.getRunescapeBoldFont()); + sectionName.setToolTipText("" + name + ":
" + cs.description() + ""); + sectionHeader.add(sectionName, BorderLayout.CENTER); + + final JPanel sectionContents = new JPanel(); + sectionContents.setLayout(new DynamicGridLayout(0, 1, 0, 5)); + sectionContents.setMinimumSize(new Dimension(PANEL_WIDTH, 0)); + sectionContents.setBorder(new CompoundBorder( + new MatteBorder(0, 0, 1, 0, ColorScheme.MEDIUM_GRAY_COLOR), + new EmptyBorder(BORDER_OFFSET, 0, BORDER_OFFSET, 0))); + sectionContents.setVisible(isOpen); + section.add(sectionContents, BorderLayout.SOUTH); + + // Add listeners to each part of the header so that it's easier to toggle them + final MouseAdapter adapter = new MouseAdapter() + { + @Override + public void mouseClicked(MouseEvent e) + { + toggleSection(csd, sectionToggle, sectionContents); + } + }; + sectionToggle.addActionListener(actionEvent -> toggleSection(csd, sectionToggle, sectionContents)); + sectionName.addMouseListener(adapter); + sectionHeader.addMouseListener(adapter); + + sectionWidgets.put(csd.getKey(), sectionContents); + + topLevelPanels.put(csd, section); + } + for (ConfigItemDescriptor cid : cd.getItems()) { if (cid.getItem().hidden()) @@ -427,9 +532,19 @@ class ConfigPanel extends PluginPanel item.add(button, BorderLayout.EAST); } - mainPanel.add(item); + JPanel section = sectionWidgets.get(cid.getItem().section()); + if (section == null) + { + topLevelPanels.put(cid, item); + } + else + { + section.add(item); + } } + topLevelPanels.values().forEach(mainPanel::add); + JButton resetButton = new JButton("Reset"); resetButton.addActionListener((e) -> {