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 <slusnucky@gmail.com>
This commit is contained in:
Tomas Slusny
2018-03-07 15:44:07 +01:00
parent 716c052247
commit 381b111b4a
7 changed files with 332 additions and 161 deletions

View File

@@ -46,6 +46,7 @@ import net.runelite.client.discord.DiscordService;
import net.runelite.client.menus.MenuManager; import net.runelite.client.menus.MenuManager;
import net.runelite.client.plugins.PluginManager; import net.runelite.client.plugins.PluginManager;
import net.runelite.client.ui.ClientUI; import net.runelite.client.ui.ClientUI;
import net.runelite.client.ui.TitleToolbar;
import net.runelite.client.ui.overlay.OverlayRenderer; import net.runelite.client.ui.overlay.OverlayRenderer;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.slf4j.MDC; import org.slf4j.MDC;
@@ -93,6 +94,9 @@ public class RuneLite
@Inject @Inject
private ClientUI clientUI; private ClientUI clientUI;
@Inject
private TitleToolbar titleToolbar;
Client client; Client client;
public static void main(String[] args) throws Exception public static void main(String[] args) throws Exception
@@ -176,6 +180,9 @@ public class RuneLite
// Load the session, including saved configuration // Load the session, including saved configuration
sessionManager.loadSession(); sessionManager.loadSession();
// Refresh title toolbar
titleToolbar.refresh();
// Show UI after all plugins are loaded // Show UI after all plugins are loaded
clientUI.show(); clientUI.show();
} }

View File

@@ -0,0 +1,35 @@
/*
* Copyright (c) 2018, Tomas Slusny <slusnucky@gmail.com>
* 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;
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright (c) 2018, Tomas Slusny <slusnucky@gmail.com>
* 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;
}

View File

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

View File

@@ -63,10 +63,13 @@ import net.runelite.client.config.RuneLiteConfig;
import net.runelite.client.events.ClientUILoaded; import net.runelite.client.events.ClientUILoaded;
import net.runelite.client.events.PluginToolbarButtonAdded; import net.runelite.client.events.PluginToolbarButtonAdded;
import net.runelite.client.events.PluginToolbarButtonRemoved; 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.OSType;
import net.runelite.client.util.OSXUtil; import net.runelite.client.util.OSXUtil;
import net.runelite.client.util.SwingUtil; import net.runelite.client.util.SwingUtil;
import org.pushingpixels.substance.api.skin.SubstanceGraphiteLookAndFeel; 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.SubstanceCoreUtilities;
import org.pushingpixels.substance.internal.utils.SubstanceTitlePaneUtilities; import org.pushingpixels.substance.internal.utils.SubstanceTitlePaneUtilities;
@@ -102,9 +105,6 @@ public class ClientUI
@Getter @Getter
private TrayIcon trayIcon; private TrayIcon trayIcon;
@Getter
private TitleToolbar titleToolbar;
private final RuneLite runelite; private final RuneLite runelite;
private final RuneLiteProperties properties; private final RuneLiteProperties properties;
private final RuneLiteConfig config; private final RuneLiteConfig config;
@@ -114,6 +114,7 @@ public class ClientUI
private JPanel navContainer; private JPanel navContainer;
private PluginPanel pluginPanel; private PluginPanel pluginPanel;
private ClientPluginToolbar pluginToolbar; private ClientPluginToolbar pluginToolbar;
private ClientTitleToolbar titleToolbar;
private JButton currentButton; private JButton currentButton;
@Inject @Inject
@@ -247,6 +248,48 @@ public class ClientUI
SwingUtilities.invokeLater(() -> pluginToolbar.removeComponent(event.getButton())); 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. * Initialize UI.
* *
@@ -300,8 +343,7 @@ public class ClientUI
pluginToolbar = new ClientPluginToolbar(); pluginToolbar = new ClientPluginToolbar();
container.add(pluginToolbar); container.add(pluginToolbar);
titleToolbar = new TitleToolbar(properties); titleToolbar = new ClientTitleToolbar();
frame.add(container); frame.add(container);
}); });
} }
@@ -323,7 +365,7 @@ public class ClientUI
{ {
frame.getRootPane().setWindowDecorationStyle(JRootPane.FRAME); 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); titleToolbar.putClientProperty(SubstanceTitlePaneUtilities.EXTRA_COMPONENT_KIND, SubstanceTitlePaneUtilities.ExtraComponentKind.TRAILING);
titleBar.add(titleToolbar); titleBar.add(titleToolbar);

View File

@@ -29,12 +29,14 @@ import java.awt.image.BufferedImage;
import java.util.function.Supplier; import java.util.function.Supplier;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode;
/** /**
* UI navigation button. * UI navigation button.
*/ */
@Data @Data
@Builder @Builder
@EqualsAndHashCode(of = {"name", "tooltip"})
public class NavigationButton public class NavigationButton
{ {
/** /**

View File

@@ -1,5 +1,6 @@
/* /*
* Copyright (c) 2018 Abex * Copyright (c) 2017-2018, Adam <Adam@sigterm.info>
* Copyright (c) 2018, Tomas Slusny <slusnucky@gmail.com>
* All rights reserved. * All rights reserved.
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
@@ -24,174 +25,78 @@
*/ */
package net.runelite.client.ui; package net.runelite.client.ui;
import java.awt.Component; import com.google.common.eventbus.EventBus;
import java.awt.Container; import java.util.Comparator;
import java.awt.Desktop; import java.util.Iterator;
import java.awt.Dimension; import java.util.TreeSet;
import java.awt.Image; import javax.inject.Inject;
import java.awt.LayoutManager2; import javax.inject.Singleton;
import java.awt.event.MouseAdapter; import net.runelite.client.events.TitleToolbarButtonAdded;
import java.awt.event.MouseEvent; import net.runelite.client.events.TitleToolbarButtonRemoved;
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;
@Slf4j /**
public class TitleToolbar extends JPanel * Title toolbar buttons holder.
*/
@Singleton
public class TitleToolbar
{ {
private static final int TITLEBAR_SIZE = 23; private final EventBus eventBus;
private static final int ITEM_PADDING = 4; private final TreeSet<NavigationButton> 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 this.eventBus = eventBus;
// 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() /**
* Add navigation.
*
* @param button the button
*/
public void addNavigation(final NavigationButton button)
{
if (buttons.contains(button))
{ {
@Override return;
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);
} }
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; int index = buttons.headSet(button).size();
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));
}
button.setIcon(icon); if (buttons.remove(button))
button.setRolloverIcon(invertedIcon); {
button.putClientProperty(SubstanceSynapse.FLAT_LOOK, Boolean.TRUE); eventBus.post(new TitleToolbarButtonRemoved(button, index));
button.setFocusable(false); }
add(button);
revalidate();
repaint();
} }
@Override /**
public void remove(Component c) * Refresh all buttons
*/
public void refresh()
{ {
super.remove(c); final Iterator<NavigationButton> iterator = buttons.iterator();
revalidate(); int index = 0;
repaint();
while (iterator.hasNext())
{
final NavigationButton button = iterator.next();
eventBus.post(new TitleToolbarButtonRemoved(button, index));
eventBus.post(new TitleToolbarButtonAdded(button, index));
index++;
}
} }
} }