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

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.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);

View File

@@ -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
{
/**

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.
*
* 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<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
// 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<NavigationButton> 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++;
}
}
}