config: add support for sections

This commit is contained in:
Hydrox6
2020-05-09 17:29:14 +01:00
committed by Adam
parent b453624dd9
commit 30681d8935
8 changed files with 295 additions and 15 deletions

View File

@@ -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<ConfigSectionDescriptor> sections;
private final Collection<ConfigItemDescriptor> items;
public ConfigDescriptor(ConfigGroup group, Collection<ConfigItemDescriptor> items)
public ConfigDescriptor(ConfigGroup group, Collection<ConfigSectionDescriptor> sections, Collection<ConfigItemDescriptor> items)
{
this.group = group;
this.sections = sections;
this.items = items;
}
public ConfigGroup getGroup()
{
return group;
}
public Collection<ConfigItemDescriptor> getItems()
{
return items;
}
}

View File

@@ -46,4 +46,6 @@ public @interface ConfigItem
String warning() default "";
boolean secret() default false;
String section() default "";
}

View File

@@ -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();
}
}

View File

@@ -468,8 +468,32 @@ public class ConfigManager
throw new IllegalArgumentException("Not a config group");
}
final List<ConfigSectionDescriptor> 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<ConfigItemDescriptor> 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);
}
/**

View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) 2020, Hydrox6 <ikada@protonmail.ch>
* 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();
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright (c) 2019, Hydrox6 <ikada@protonmail.ch>
* 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;
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright (c) 2020, Hydrox6 <ikada@protonmail.ch>
* 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();
}
}

View File

@@ -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<ConfigSectionDescriptor, Boolean> 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<String, JPanel> sectionWidgets = new HashMap<>();
final Map<ConfigObject, JPanel> 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("<html>" + name + ":<br>" + cs.description() + "</html>");
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) ->
{