From 381b111b4a50c1564ce6883cff748a43f82a2e80 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Wed, 7 Mar 2018 15:44:07 +0100 Subject: [PATCH] Remove the need for custom TitleToolbar component - Extract title toolbar layout to separate component - Create new events for adding and removing title toolbar buttons - Create TitleToolbar through Guice Signed-off-by: Tomas Slusny --- .../java/net/runelite/client/RuneLite.java | 7 + .../events/TitleToolbarButtonAdded.java | 35 +++ .../events/TitleToolbarButtonRemoved.java | 35 +++ .../client/ui/ClientTitleToolbar.java | 145 ++++++++++++ .../java/net/runelite/client/ui/ClientUI.java | 54 ++++- .../runelite/client/ui/NavigationButton.java | 2 + .../net/runelite/client/ui/TitleToolbar.java | 215 +++++------------- 7 files changed, 332 insertions(+), 161 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/events/TitleToolbarButtonAdded.java create mode 100644 runelite-client/src/main/java/net/runelite/client/events/TitleToolbarButtonRemoved.java create mode 100644 runelite-client/src/main/java/net/runelite/client/ui/ClientTitleToolbar.java 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 5dfe7e7f90..6fdff16707 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLite.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLite.java @@ -46,6 +46,7 @@ import net.runelite.client.discord.DiscordService; import net.runelite.client.menus.MenuManager; import net.runelite.client.plugins.PluginManager; import net.runelite.client.ui.ClientUI; +import net.runelite.client.ui.TitleToolbar; import net.runelite.client.ui.overlay.OverlayRenderer; import org.slf4j.LoggerFactory; import org.slf4j.MDC; @@ -93,6 +94,9 @@ public class RuneLite @Inject private ClientUI clientUI; + @Inject + private TitleToolbar titleToolbar; + Client client; public static void main(String[] args) throws Exception @@ -176,6 +180,9 @@ public class RuneLite // Load the session, including saved configuration sessionManager.loadSession(); + // Refresh title toolbar + titleToolbar.refresh(); + // Show UI after all plugins are loaded clientUI.show(); } diff --git a/runelite-client/src/main/java/net/runelite/client/events/TitleToolbarButtonAdded.java b/runelite-client/src/main/java/net/runelite/client/events/TitleToolbarButtonAdded.java new file mode 100644 index 0000000000..a053963a6e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/events/TitleToolbarButtonAdded.java @@ -0,0 +1,35 @@ +/* + * 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.events; + +import lombok.Value; +import net.runelite.client.ui.NavigationButton; + +@Value +public class TitleToolbarButtonAdded +{ + private NavigationButton button; + private int index; +} diff --git a/runelite-client/src/main/java/net/runelite/client/events/TitleToolbarButtonRemoved.java b/runelite-client/src/main/java/net/runelite/client/events/TitleToolbarButtonRemoved.java new file mode 100644 index 0000000000..b0f39f05cb --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/events/TitleToolbarButtonRemoved.java @@ -0,0 +1,35 @@ +/* + * 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.events; + +import lombok.Value; +import net.runelite.client.ui.NavigationButton; + +@Value +public class TitleToolbarButtonRemoved +{ + private NavigationButton button; + private int index; +} diff --git a/runelite-client/src/main/java/net/runelite/client/ui/ClientTitleToolbar.java b/runelite-client/src/main/java/net/runelite/client/ui/ClientTitleToolbar.java new file mode 100644 index 0000000000..1f1e1b4dd9 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/ui/ClientTitleToolbar.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2018 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.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.LayoutManager2; +import java.util.HashMap; +import java.util.Map; +import javax.swing.JPanel; + +/** + * Client title toolbar component. + */ +public class ClientTitleToolbar extends JPanel +{ + public static final int TITLEBAR_SIZE = 23; + private static final int ITEM_PADDING = 4; + + private final Map componentMap = new HashMap<>(); + + /** + * Instantiates a new Client title toolbar. + */ + public ClientTitleToolbar() + { + // The only other layout manager that would manage it's preferred size without padding + // was the GroupLayout manager, which doesn't work with dynamic layouts like this one. + // Primarily, it would not remove components unless it was immediately repainted. + setLayout(new LayoutManager2() + { + @Override + public void addLayoutComponent(String name, Component comp) + { + } + + @Override + public void addLayoutComponent(Component comp, Object constraints) + { + } + + @Override + public void removeLayoutComponent(Component comp) + { + } + + @Override + public Dimension preferredLayoutSize(Container parent) + { + int width = parent.getComponentCount() * (TITLEBAR_SIZE + ITEM_PADDING); + return new Dimension(width, TITLEBAR_SIZE); + } + + @Override + public Dimension minimumLayoutSize(Container parent) + { + return preferredLayoutSize(parent); + } + + @Override + public Dimension maximumLayoutSize(Container parent) + { + return preferredLayoutSize(parent); + } + + @Override + public float getLayoutAlignmentX(Container target) + { + return 0; + } + + @Override + public float getLayoutAlignmentY(Container target) + { + return 0; + } + + @Override + public void invalidateLayout(Container target) + { + } + + @Override + public void layoutContainer(Container parent) + { + int x = 0; + for (Component c : parent.getComponents()) + { + x += ITEM_PADDING; + int height = c.getPreferredSize().height; + if (height > TITLEBAR_SIZE) + { + height = TITLEBAR_SIZE; + } + c.setBounds(x, (TITLEBAR_SIZE - height) / 2, TITLEBAR_SIZE, height); + x += TITLEBAR_SIZE; + } + } + }); + } + + public void addComponent(NavigationButton button, Component c) + { + if (componentMap.put(button, c) == null) + { + add(c); + revalidate(); + repaint(); + } + } + + public void removeComponent(NavigationButton button) + { + final Component component = componentMap.remove(button); + if (component != null) + { + remove(component); + revalidate(); + repaint(); + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/ui/ClientUI.java b/runelite-client/src/main/java/net/runelite/client/ui/ClientUI.java index 9b15018d90..d9b518849b 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/ClientUI.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/ClientUI.java @@ -63,10 +63,13 @@ import net.runelite.client.config.RuneLiteConfig; import net.runelite.client.events.ClientUILoaded; import net.runelite.client.events.PluginToolbarButtonAdded; import net.runelite.client.events.PluginToolbarButtonRemoved; +import net.runelite.client.events.TitleToolbarButtonAdded; +import net.runelite.client.events.TitleToolbarButtonRemoved; import net.runelite.client.util.OSType; import net.runelite.client.util.OSXUtil; import net.runelite.client.util.SwingUtil; import org.pushingpixels.substance.api.skin.SubstanceGraphiteLookAndFeel; +import org.pushingpixels.substance.internal.SubstanceSynapse; import org.pushingpixels.substance.internal.utils.SubstanceCoreUtilities; import org.pushingpixels.substance.internal.utils.SubstanceTitlePaneUtilities; @@ -102,9 +105,6 @@ public class ClientUI @Getter private TrayIcon trayIcon; - @Getter - private TitleToolbar titleToolbar; - private final RuneLite runelite; private final RuneLiteProperties properties; private final RuneLiteConfig config; @@ -114,6 +114,7 @@ public class ClientUI private JPanel navContainer; private PluginPanel pluginPanel; private ClientPluginToolbar pluginToolbar; + private ClientTitleToolbar titleToolbar; private JButton currentButton; @Inject @@ -247,6 +248,48 @@ public class ClientUI SwingUtilities.invokeLater(() -> pluginToolbar.removeComponent(event.getButton())); } + @Subscribe + public void onTitleToolbarButtonAdded(final TitleToolbarButtonAdded event) + { + if (!config.enableCustomChrome()) + { + return; + } + + SwingUtilities.invokeLater(() -> + { + + final int iconSize = ClientTitleToolbar.TITLEBAR_SIZE - 6; + final BufferedImage scaledImage = SwingUtil.resizeImage(event.getButton().getIcon(), iconSize, iconSize); + final JButton button = new JButton(); + button.setName(event.getButton().getName()); + button.setToolTipText(event.getButton().getTooltip()); + button.setIcon(new ImageIcon(scaledImage)); + button.setRolloverIcon(new ImageIcon(SwingUtil.createInvertedImage(scaledImage))); + button.putClientProperty(SubstanceSynapse.FLAT_LOOK, Boolean.TRUE); + button.setFocusable(false); + + if (event.getButton().getOnClick() != null) + { + button.addActionListener(e -> event.getButton().getOnClick().run()); + } + + event.getButton().setOnSelect(() -> button.setSelected(event.getButton().isSelected())); + titleToolbar.addComponent(event.getButton(), button); + }); + } + + @Subscribe + public void onTitleToolbarButtonRemoved(final TitleToolbarButtonRemoved event) + { + if (!config.enableCustomChrome()) + { + return; + } + + SwingUtilities.invokeLater(() -> titleToolbar.removeComponent(event.getButton())); + } + /** * Initialize UI. * @@ -300,8 +343,7 @@ public class ClientUI pluginToolbar = new ClientPluginToolbar(); container.add(pluginToolbar); - titleToolbar = new TitleToolbar(properties); - + titleToolbar = new ClientTitleToolbar(); frame.add(container); }); } @@ -323,7 +365,7 @@ public class ClientUI { frame.getRootPane().setWindowDecorationStyle(JRootPane.FRAME); - JComponent titleBar = SubstanceCoreUtilities.getTitlePaneComponent(frame); + final JComponent titleBar = SubstanceCoreUtilities.getTitlePaneComponent(frame); titleToolbar.putClientProperty(SubstanceTitlePaneUtilities.EXTRA_COMPONENT_KIND, SubstanceTitlePaneUtilities.ExtraComponentKind.TRAILING); titleBar.add(titleToolbar); diff --git a/runelite-client/src/main/java/net/runelite/client/ui/NavigationButton.java b/runelite-client/src/main/java/net/runelite/client/ui/NavigationButton.java index f85e0cf215..8ebb330b48 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/NavigationButton.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/NavigationButton.java @@ -29,12 +29,14 @@ import java.awt.image.BufferedImage; import java.util.function.Supplier; import lombok.Builder; import lombok.Data; +import lombok.EqualsAndHashCode; /** * UI navigation button. */ @Data @Builder +@EqualsAndHashCode(of = {"name", "tooltip"}) public class NavigationButton { /** diff --git a/runelite-client/src/main/java/net/runelite/client/ui/TitleToolbar.java b/runelite-client/src/main/java/net/runelite/client/ui/TitleToolbar.java index 9a5ffc1be4..80333c2da6 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/TitleToolbar.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/TitleToolbar.java @@ -1,5 +1,6 @@ /* - * Copyright (c) 2018 Abex + * Copyright (c) 2017-2018, Adam + * Copyright (c) 2018, Tomas Slusny * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -24,174 +25,78 @@ */ package net.runelite.client.ui; -import java.awt.Component; -import java.awt.Container; -import java.awt.Desktop; -import java.awt.Dimension; -import java.awt.Image; -import java.awt.LayoutManager2; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.awt.image.BufferedImage; -import java.io.IOException; -import java.net.URISyntaxException; -import java.net.URL; -import javax.imageio.ImageIO; -import javax.swing.ImageIcon; -import javax.swing.JButton; -import javax.swing.JPanel; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.RuneLiteProperties; -import org.pushingpixels.substance.internal.SubstanceSynapse; +import com.google.common.eventbus.EventBus; +import java.util.Comparator; +import java.util.Iterator; +import java.util.TreeSet; +import javax.inject.Inject; +import javax.inject.Singleton; +import net.runelite.client.events.TitleToolbarButtonAdded; +import net.runelite.client.events.TitleToolbarButtonRemoved; -@Slf4j -public class TitleToolbar extends JPanel +/** + * Title toolbar buttons holder. + */ +@Singleton +public class TitleToolbar { - private static final int TITLEBAR_SIZE = 23; - private static final int ITEM_PADDING = 4; + private final EventBus eventBus; + private final TreeSet buttons = new TreeSet<>(Comparator.comparing(NavigationButton::getTooltip)); - public TitleToolbar(RuneLiteProperties properties) + @Inject + private TitleToolbar(final EventBus eventBus) { - // The only other layout manager that would manage it's preferred size without padding - // was the GroupLayout manager, which doesn't work with dynamic layouts like this one. - // Primarily, it would not remove components unless it was immediately repainted. - setLayout(new LayoutManager2() + this.eventBus = eventBus; + } + + /** + * Add navigation. + * + * @param button the button + */ + public void addNavigation(final NavigationButton button) + { + if (buttons.contains(button)) { - @Override - public void addLayoutComponent(String name, Component comp) - { - } - - @Override - public void addLayoutComponent(Component comp, Object constraints) - { - } - - @Override - public void removeLayoutComponent(Component comp) - { - } - - @Override - public Dimension preferredLayoutSize(Container parent) - { - int width = parent.getComponentCount() * (TITLEBAR_SIZE + ITEM_PADDING); - return new Dimension(width, TITLEBAR_SIZE); - } - - @Override - public Dimension minimumLayoutSize(Container parent) - { - return preferredLayoutSize(parent); - } - - @Override - public Dimension maximumLayoutSize(Container parent) - { - return preferredLayoutSize(parent); - } - - @Override - public float getLayoutAlignmentX(Container target) - { - return 0; - } - - @Override - public float getLayoutAlignmentY(Container target) - { - return 0; - } - - @Override - public void invalidateLayout(Container target) - { - } - - @Override - public void layoutContainer(Container parent) - { - int x = 0; - for (Component c : parent.getComponents()) - { - x += ITEM_PADDING; - int height = c.getPreferredSize().height; - if (height > TITLEBAR_SIZE) - { - height = TITLEBAR_SIZE; - } - c.setBounds(x, (TITLEBAR_SIZE - height) / 2, TITLEBAR_SIZE, height); - x += TITLEBAR_SIZE; - } - } - }); - - try - { - BufferedImage discordIcon; - BufferedImage invertedIcon; - synchronized (ImageIO.class) - { - discordIcon = ImageIO.read(ClientUI.class.getResourceAsStream("discord.png")); - invertedIcon = ImageIO.read(ClientUI.class.getResourceAsStream("discord_inverted.png")); - } - - JButton discordButton = new JButton(); - discordButton.setToolTipText("Join Discord"); - discordButton.addMouseListener(new MouseAdapter() - { - @Override - public void mouseClicked(MouseEvent e) - { - super.mouseClicked(e); - try - { - Desktop.getDesktop().browse(new URL(properties.getDiscordInvite()).toURI()); - } - catch (IOException | URISyntaxException ex) - { - log.warn("error opening browser", ex); - } - } - }); - - addButton(discordButton, discordIcon, invertedIcon); + return; } - catch (IOException ex) + + if (buttons.add(button)) { - log.warn("unable to load discord button", ex); + int index = buttons.headSet(button).size(); + eventBus.post(new TitleToolbarButtonAdded(button, index)); } } - public void addButton(JButton button, Image iconImage, Image invertedIconImage) + /** + * Remove navigation. + * + * @param button the button + */ + public void removeNavigation(final NavigationButton button) { - final int iconSize = TITLEBAR_SIZE - 6; - ImageIcon icon = new ImageIcon(iconImage.getScaledInstance(iconSize, iconSize, Image.SCALE_SMOOTH)); - ImageIcon invertedIcon; - if (invertedIconImage == null) - { - invertedIcon = icon; - } - else - { - invertedIcon = new ImageIcon(invertedIconImage.getScaledInstance(iconSize, iconSize, Image.SCALE_SMOOTH)); - } + int index = buttons.headSet(button).size(); - button.setIcon(icon); - button.setRolloverIcon(invertedIcon); - button.putClientProperty(SubstanceSynapse.FLAT_LOOK, Boolean.TRUE); - button.setFocusable(false); - - add(button); - revalidate(); - repaint(); + if (buttons.remove(button)) + { + eventBus.post(new TitleToolbarButtonRemoved(button, index)); + } } - @Override - public void remove(Component c) + /** + * Refresh all buttons + */ + public void refresh() { - super.remove(c); - revalidate(); - repaint(); + final Iterator iterator = buttons.iterator(); + int index = 0; + + while (iterator.hasNext()) + { + final NavigationButton button = iterator.next(); + eventBus.post(new TitleToolbarButtonRemoved(button, index)); + eventBus.post(new TitleToolbarButtonAdded(button, index)); + index++; + } } }