From 15d06d840682554fc50213469d878254736fc369 Mon Sep 17 00:00:00 2001 From: Su-Shing Chen Date: Thu, 11 Jun 2020 07:06:03 +1200 Subject: [PATCH] xptracker: Add drag and drop reordering for tracker panel bars (#4118) This commit implements a custom component which extends JLayeredPane, allowing reordering child components via drag and drop. This requires its children components to forward the necessary MouseEvents. --- .../client/plugins/xptracker/XpInfoBox.java | 14 +- .../client/plugins/xptracker/XpPanel.java | 5 +- .../ui/components/DragAndDropReorderPane.java | 199 ++++++++++++++++++ .../components/MouseDragEventForwarder.java | 70 ++++++ 4 files changed, 283 insertions(+), 5 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/ui/components/DragAndDropReorderPane.java create mode 100644 runelite-client/src/main/java/net/runelite/client/ui/components/MouseDragEventForwarder.java diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpInfoBox.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpInfoBox.java index cbc0be4520..a1aab90b80 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpInfoBox.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpInfoBox.java @@ -35,6 +35,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.swing.ImageIcon; +import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JPanel; @@ -54,6 +55,7 @@ import net.runelite.client.ui.ColorScheme; import net.runelite.client.ui.DynamicGridLayout; import net.runelite.client.ui.FontManager; import net.runelite.client.ui.SkillColor; +import net.runelite.client.ui.components.MouseDragEventForwarder; import net.runelite.client.ui.components.ProgressBar; import net.runelite.client.util.ColorUtil; import net.runelite.client.util.LinkBrowser; @@ -80,7 +82,7 @@ class XpInfoBox extends JPanel private static final String ADD_STATE = "Add to canvas"; // Instance members - private final JPanel panel; + private final JComponent panel; @Getter(AccessLevel.PACKAGE) private final Skill skill; @@ -107,7 +109,7 @@ class XpInfoBox extends JPanel private boolean paused = false; - XpInfoBox(XpTrackerPlugin xpTrackerPlugin, XpTrackerConfig xpTrackerConfig, Client client, JPanel panel, Skill skill, SkillIconManager iconManager) + XpInfoBox(XpTrackerPlugin xpTrackerPlugin, XpTrackerConfig xpTrackerConfig, Client client, JComponent panel, Skill skill, SkillIconManager iconManager) { this.xpTrackerConfig = xpTrackerConfig; this.panel = panel; @@ -217,13 +219,19 @@ class XpInfoBox extends JPanel container.setComponentPopupMenu(popupMenu); progressBar.setComponentPopupMenu(popupMenu); + // forward mouse drag events to parent panel for drag and drop reordering + MouseDragEventForwarder mouseDragEventForwarder = new MouseDragEventForwarder(panel); + container.addMouseListener(mouseDragEventForwarder); + container.addMouseMotionListener(mouseDragEventForwarder); + progressBar.addMouseListener(mouseDragEventForwarder); + progressBar.addMouseMotionListener(mouseDragEventForwarder); + add(container, BorderLayout.NORTH); } void reset() { canvasItem.setText(ADD_STATE); - container.remove(statsPanel); panel.remove(this); panel.revalidate(); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPanel.java index 87c272c842..3c63ccf76e 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/xptracker/XpPanel.java @@ -31,6 +31,7 @@ import java.util.HashMap; import java.util.Map; import javax.swing.BoxLayout; import javax.swing.ImageIcon; +import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JPanel; @@ -44,6 +45,7 @@ import net.runelite.client.game.SkillIconManager; import net.runelite.client.ui.ColorScheme; import net.runelite.client.ui.FontManager; import net.runelite.client.ui.PluginPanel; +import net.runelite.client.ui.components.DragAndDropReorderPane; import net.runelite.client.ui.components.PluginErrorPanel; import net.runelite.client.util.LinkBrowser; import okhttp3.HttpUrl; @@ -120,9 +122,8 @@ class XpPanel extends PluginPanel overallPanel.add(overallIcon, BorderLayout.WEST); overallPanel.add(overallInfo, BorderLayout.CENTER); + final JComponent infoBoxPanel = new DragAndDropReorderPane(); - final JPanel infoBoxPanel = new JPanel(); - infoBoxPanel.setLayout(new BoxLayout(infoBoxPanel, BoxLayout.Y_AXIS)); layoutPanel.add(overallPanel); layoutPanel.add(infoBoxPanel); diff --git a/runelite-client/src/main/java/net/runelite/client/ui/components/DragAndDropReorderPane.java b/runelite-client/src/main/java/net/runelite/client/ui/components/DragAndDropReorderPane.java new file mode 100644 index 0000000000..32ecaaf69c --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/ui/components/DragAndDropReorderPane.java @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2020, Shingyx + * 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.components; + +import java.awt.Component; +import java.awt.Container; +import java.awt.LayoutManager; +import java.awt.Point; +import java.awt.dnd.DragSource; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import javax.swing.BoxLayout; +import javax.swing.JLayeredPane; +import javax.swing.SwingUtilities; + +/** + * Pane which allows reordering its components via drag and drop. + */ +public class DragAndDropReorderPane extends JLayeredPane +{ + private Point dragStartPoint; + private Component draggingComponent; + private int dragYOffset = 0; + private int dragIndex = -1; + + public DragAndDropReorderPane() + { + super(); + setLayout(new DragAndDropReorderLayoutManager()); + MouseAdapter mouseAdapter = new DragAndDropReorderMouseAdapter(); + addMouseListener(mouseAdapter); + addMouseMotionListener(mouseAdapter); + } + + @Override + public void setLayout(LayoutManager layoutManager) + { + if (layoutManager != null && !(layoutManager instanceof DragAndDropReorderLayoutManager)) + { + throw new IllegalArgumentException("DragAndDropReorderPane only supports DragAndDropReorderLayoutManager"); + } + super.setLayout(layoutManager); + } + + private void startDragging(Point point) + { + draggingComponent = getDefaultLayerComponentAt(dragStartPoint); + if (draggingComponent == null) + { + dragStartPoint = null; + return; + } + dragYOffset = SwingUtilities.convertPoint(this, dragStartPoint, draggingComponent).y; + dragIndex = getPosition(draggingComponent); + setLayer(draggingComponent, DRAG_LAYER); + moveDraggingComponent(point); + } + + private void drag(Point point) + { + moveDraggingComponent(point); + + // reorder components overlapping with the dragging components mid-point + Point draggingComponentMidPoint = SwingUtilities.convertPoint( + draggingComponent, + new Point(draggingComponent.getWidth() / 2, draggingComponent.getHeight() / 2), + this + ); + Component component = getDefaultLayerComponentAt(draggingComponentMidPoint); + if (component != null) + { + int index = getPosition(component); + dragIndex = index < dragIndex ? index : index + 1; + revalidate(); + } + } + + private void finishDragging() + { + if (draggingComponent != null) + { + setLayer(draggingComponent, DEFAULT_LAYER, dragIndex); + draggingComponent = null; + dragYOffset = 0; + dragIndex = -1; + revalidate(); + } + dragStartPoint = null; + } + + private void moveDraggingComponent(Point point) + { + // shift the dragging component to match it's earlier y offset with the mouse + int y = point.y - dragYOffset; + // clamp the height to stay within the pane + y = Math.max(y, 0); + y = Math.min(y, getHeight() - draggingComponent.getHeight()); + + draggingComponent.setLocation(new Point(0, y)); + } + + private Component getDefaultLayerComponentAt(Point point) + { + for (Component component : getComponentsInLayer(DEFAULT_LAYER)) + { + if (component.contains(point.x - component.getX(), point.y - component.getY())) + { + return component; + } + } + return null; + } + + private class DragAndDropReorderLayoutManager extends BoxLayout + { + private DragAndDropReorderLayoutManager() + { + super(DragAndDropReorderPane.this, BoxLayout.Y_AXIS); + } + + @Override + public void layoutContainer(Container target) + { + if (draggingComponent != null) + { + // temporarily move the dragging component to the default layer for correct layout calculation + Point location = draggingComponent.getLocation(); + setLayer(draggingComponent, DEFAULT_LAYER, dragIndex); + super.layoutContainer(target); + setLayer(draggingComponent, DRAG_LAYER); + draggingComponent.setLocation(location); + } + else + { + super.layoutContainer(target); + } + } + } + + private class DragAndDropReorderMouseAdapter extends MouseAdapter + { + @Override + public void mousePressed(MouseEvent e) + { + if (SwingUtilities.isLeftMouseButton(e) && getComponentCount() > 1) + { + dragStartPoint = e.getPoint(); + } + } + + @Override + public void mouseDragged(MouseEvent e) + { + if (SwingUtilities.isLeftMouseButton(e) && dragStartPoint != null) + { + Point point = e.getPoint(); + if (draggingComponent != null) + { + drag(point); + } + else if (point.distance(dragStartPoint) > DragSource.getDragThreshold()) + { + startDragging(point); + } + } + } + + @Override + public void mouseReleased(MouseEvent e) + { + if (SwingUtilities.isLeftMouseButton(e)) + { + finishDragging(); + } + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/ui/components/MouseDragEventForwarder.java b/runelite-client/src/main/java/net/runelite/client/ui/components/MouseDragEventForwarder.java new file mode 100644 index 0000000000..2e97f66d01 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/ui/components/MouseDragEventForwarder.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2020, Shingyx + * 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.components; + +import java.awt.Component; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import javax.swing.SwingUtilities; + +/** + * Forwards left mouse button drag events to the target Component. + */ +public class MouseDragEventForwarder extends MouseAdapter +{ + private final Component target; + + public MouseDragEventForwarder(Component target) + { + this.target = target; + } + + @Override + public void mousePressed(MouseEvent e) + { + processEvent(e); + } + + @Override + public void mouseDragged(MouseEvent e) + { + processEvent(e); + } + + @Override + public void mouseReleased(MouseEvent e) + { + processEvent(e); + } + + private void processEvent(MouseEvent e) + { + if (SwingUtilities.isLeftMouseButton(e)) + { + MouseEvent eventForTarget = SwingUtilities.convertMouseEvent((Component) e.getSource(), e, target); + target.dispatchEvent(eventForTarget); + } + } +}