GP-5646 reorder program tabs via drag-N-drop

tmp
This commit is contained in:
ghidragon 2025-07-11 11:37:40 -04:00
parent 9018e9a05a
commit 99992cf550
9 changed files with 192 additions and 28 deletions

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -16,7 +16,8 @@
package docking.widgets.tab;
import java.awt.*;
import java.awt.event.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.*;
import javax.swing.border.Border;
@ -32,7 +33,7 @@ import resources.Icons;
*
* @param <T> the type of the tab values
*/
class GTab<T> extends JPanel {
public class GTab<T> extends JPanel {
private final static Border TAB_BORDER = new GTabBorder(false);
private final static Border SELECTED_TAB_BORDER = new GTabBorder(true);
private static final String SELECTED_FONT_TABS_ID = "font.widget.tabs.selected";
@ -67,6 +68,7 @@ class GTab<T> extends JPanel {
nameLabel.setText(tabPanel.getDisplayName(value));
nameLabel.setIcon(tabPanel.getValueIcon(value));
nameLabel.setToolTipText(tabPanel.getValueToolTip(value));
Gui.registerFont(nameLabel, selected ? SELECTED_FONT_TABS_ID : FONT_TABS_ID);
add(nameLabel, BorderLayout.WEST);
@ -76,8 +78,8 @@ class GTab<T> extends JPanel {
closeLabel.setOpaque(true);
add(closeLabel, BorderLayout.EAST);
installMouseListener(this, new GTabMouseListener());
GTabMouseListener listener = new GTabMouseListener();
installMouseListener(this, listener);
initializeTabColors(false);
}
@ -85,6 +87,11 @@ class GTab<T> extends JPanel {
return value;
}
public void setSelected(boolean selected) {
this.selected = selected;
initializeTabColors(false);
}
void refresh() {
nameLabel.setText(tabPanel.getDisplayName(value));
nameLabel.setIcon(tabPanel.getValueIcon(value));
@ -96,9 +103,10 @@ class GTab<T> extends JPanel {
initializeTabColors(b);
}
private void installMouseListener(Container c, MouseListener listener) {
private void installMouseListener(Container c, GTabMouseListener listener) {
c.addMouseListener(listener);
c.addMouseMotionListener(listener);
Component[] children = c.getComponents();
for (Component element : children) {
if (element instanceof Container) {
@ -106,6 +114,7 @@ class GTab<T> extends JPanel {
}
else {
element.addMouseListener(listener);
element.addMouseMotionListener(listener);
}
}
}
@ -163,6 +172,16 @@ class GTab<T> extends JPanel {
tabPanel.selectTab(value);
}
}
@Override
public void mouseReleased(MouseEvent e) {
tabPanel.mouseReleased(GTab.this, e);
}
@Override
public void mouseDragged(MouseEvent e) {
tabPanel.mouseDragged(GTab.this, e);
}
}
}

View file

@ -15,10 +15,10 @@
*/
package docking.widgets.tab;
import java.awt.Component;
import java.awt.Container;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
@ -26,6 +26,7 @@ import java.util.stream.Collectors;
import javax.swing.*;
import ghidra.util.layout.HorizontalLayout;
import resources.ResourceManager;
import utility.function.Dummy;
/**
@ -66,6 +67,9 @@ public class GTabPanel<T> extends JPanel {
private Consumer<T> closeTabConsumer = t -> removeTab(t);
private boolean showTabsAlways = true;
private Cursor moveCursor = createMoveCursor();
private boolean isDragging;
/**
* Constructor
* @param tabTypeName the name of the type of values in the tab panel. This will be used to
@ -129,8 +133,9 @@ public class GTabPanel<T> extends JPanel {
* @param value the value for the new tab
*/
public void addTab(T value) {
doAddValue(value);
rebuildTabs();
if (doAddValue(value)) {
rebuildTabs();
}
}
/**
@ -198,14 +203,31 @@ public class GTabPanel<T> extends JPanel {
* @param value the value whose tab is to be selected
*/
public void selectTab(T value) {
if (value == selectedValue) {
return;
}
if (value != null && !allValues.contains(value)) {
throw new IllegalArgumentException(
"Attempted to set selected value to non added value");
}
closeTabList();
highlightedValue = null;
T oldValue = selectedValue;
selectedValue = value;
rebuildTabs();
if (isVisibleTab(selectedValue)) {
GTab<T> oldTab = getTab(oldValue);
if (oldTab != null) {
oldTab.setSelected(false);
}
GTab<T> newTab = getTab(value);
newTab.setSelected(true);
}
else {
rebuildTabs();
}
selectedTabConsumer.accept(value);
}
@ -401,6 +423,15 @@ public class GTabPanel<T> extends JPanel {
return null;
}
public GTab<T> getTab(T value) {
for (GTab<T> tab : allTabs) {
if (tab.getValue().equals(value)) {
return tab;
}
}
return null;
}
void showTabList() {
if (tabList != null) {
return;
@ -482,9 +513,13 @@ public class GTabPanel<T> extends JPanel {
return false;
}
private void doAddValue(T value) {
private boolean doAddValue(T value) {
Objects.requireNonNull(value);
allValues.add(value);
if (!allValues.contains(value)) {
allValues.add(value);
return true;
}
return false;
}
private void rebuildTabs() {
@ -648,13 +683,91 @@ public class GTabPanel<T> extends JPanel {
this.ignoreFocusLost = ignoreFocusLost;
}
/*testing*/public JPanel getTab(T value) {
for (GTab<T> tab : allTabs) {
if (tab.getValue().equals(value)) {
return tab;
void mouseDragged(GTab<T> draggedTab, MouseEvent e) {
isDragging = true;
clearAllHighlights();
GTab<T> targetTab = getTab(e);
if (targetTab == null) {
// if the mouse is not currently over a valid target tab, put the cursor back to the
// default cursor to indicate this is not a valid drop location. (Couldn't find a
// decent "nope" icon that looked good when converted to a cursor)
setCursor(Cursor.getDefaultCursor());
return;
}
setCursor(moveCursor);
if (targetTab != draggedTab) {
// we highlight the tab we are hovering over to indicate it is a valid drop target
targetTab.setHighlight(true);
}
}
void mouseReleased(GTab<T> draggedTab, MouseEvent e) {
if (!isDragging) {
return;
}
isDragging = false;
setCursor(Cursor.getDefaultCursor());
int targetTabIndex = getTabIndex(e);
if (targetTabIndex >= 0) {
int draggedTabIndex = allTabs.indexOf(draggedTab);
if (draggedTabIndex == targetTabIndex) {
return;
}
moveTab(draggedTab.getValue(), targetTabIndex);
}
}
private GTab<T> getTab(MouseEvent e) {
int index = getTabIndex(e);
if (index < 0) {
return null;
}
return allTabs.get(index);
}
private int getTabIndex(MouseEvent e) {
// this e is from a GTab component, so we need to convert to GTablePanel point
Point gTabPoint = e.getPoint();
Point p = SwingUtilities.convertPoint(e.getComponent(), gTabPoint, this);
Dimension size = getSize();
// if the point is outside of the the tab panel, not a valid drop target
if (p.x < 0 || p.y < 0 || p.x >= size.width || p.y >= size.height) {
return -1;
}
// find the tab the mouse is over
for (int i = 0; i < allTabs.size(); i++) {
GTab<T> tab = allTabs.get(i);
Rectangle tabBounds = tab.getBounds();
if (tabBounds.contains(p)) {
return i;
}
}
return null;
// we are in the area past the last tab, just return the last tab index
return allTabs.size() - 1;
}
public void moveTab(T value, int newIndex) {
List<T> newValues = new ArrayList<>(allValues);
newValues.remove(value);
newValues.add(newIndex, value);
allValues.clear();
allValues.addAll(newValues);
rebuildTabs();
}
private static Cursor createMoveCursor() {
Icon icon = ResourceManager.loadIcon("move.png");
Image image = ResourceManager.getImageIcon(icon).getImage();
return Toolkit.getDefaultToolkit().createCustomCursor(image, new Point(8, 8), "nope");
}
private void clearAllHighlights() {
allTabs.forEach(t -> t.setHighlight(false));
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 B

View file

@ -127,7 +127,7 @@ public class GTabPanelTest extends AbstractDockingTest {
setSelectedValue("ABCDEFGHIJK");
assertTrue(isVisibleTab("ABCDEFGHIJK"));
setSelectedValue("One");
assertFalse(isVisibleTab("ABCDEFGHIJK"));
assertTrue(isVisibleTab("ABCDEFGHIJK"));
}
@Test
@ -233,6 +233,21 @@ public class GTabPanelTest extends AbstractDockingTest {
gTabPanel.getAccessibleName());
}
@Test
public void testMoveTab() {
assertEquals("One", getValue(0));
assertEquals("Two", getValue(1));
assertEquals("Three Three Three", getValue(2));
moveTab("One", 2);
assertEquals("Two", getValue(0));
assertEquals("Three Three Three", getValue(1));
assertEquals("One", getValue(2));
}
private void moveTab(String value, int newIndex) {
runSwing(() -> gTabPanel.moveTab(value, newIndex));
}
private List<String> getHiddenTabs() {
return runSwing(() -> gTabPanel.getHiddenTabs());
}