From 99992cf550cea0be24d39ee3d3a0a92dd0ceade1 Mon Sep 17 00:00:00 2001 From: ghidragon <106987263+ghidragon@users.noreply.github.com> Date: Fri, 11 Jul 2025 11:37:40 -0400 Subject: [PATCH] GP-5646 reorder program tabs via drag-N-drop tmp --- .../Navigating_Program_Files.htm | 11 +- .../plugin/core/progmgr/MultiTabPlugin.java | 7 +- .../core/progmgr/MultiTabPluginTest.java | 2 +- .../Framework/Docking/certification.manifest | 1 + .../main/java/docking/widgets/tab/GTab.java | 33 ++++- .../java/docking/widgets/tab/GTabPanel.java | 137 ++++++++++++++++-- .../src/main/resources/images/move.png | Bin 0 -> 194 bytes .../docking/widgets/tab/GTabPanelTest.java | 17 ++- .../java/generic/util/image/ImageUtils.java | 12 +- 9 files changed, 192 insertions(+), 28 deletions(-) create mode 100644 Ghidra/Framework/Docking/src/main/resources/images/move.png diff --git a/Ghidra/Features/Base/src/main/help/help/topics/ProgramManagerPlugin/Navigating_Program_Files.htm b/Ghidra/Features/Base/src/main/help/help/topics/ProgramManagerPlugin/Navigating_Program_Files.htm index 5d6b0943f4..1834474e39 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/ProgramManagerPlugin/Navigating_Program_Files.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/ProgramManagerPlugin/Navigating_Program_Files.htm @@ -47,7 +47,13 @@ window.

Those programs listed in bold are those that are hidden.

- +
+
+

The order of the tabs can be changed + using drag-n-drop. Drag the tab that you wish to move and drop it on the tab where you + like it to appear.

+
+

Navigation Actions

@@ -118,7 +124,8 @@

To execute this action, from the Tool menu, select NavigationGo To Last Active Program.

- + > +

Provided by: Program Manager Plugin

diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/MultiTabPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/MultiTabPlugin.java index bc14648244..db6509da84 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/MultiTabPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/MultiTabPlugin.java @@ -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. @@ -40,6 +40,7 @@ import ghidra.framework.plugintool.util.PluginStatus; import ghidra.program.model.listing.Program; import ghidra.util.HelpLocation; import ghidra.util.bean.opteditor.OptionsVetoException; +import help.Help; /** * Plugin to show a "tab" for each open program; the selected tab is the activated program. @@ -262,6 +263,8 @@ public class MultiTabPlugin extends Plugin implements DomainObjectListener, Opti tabPanel.setToolTipFunction(p -> getToolTip(p)); tabPanel.setSelectedTabConsumer(p -> programSelected(p)); tabPanel.setCloseTabConsumer(p -> progService.closeProgram(p, false)); + Help.getHelpService() + .registerHelp(tabPanel, new HelpLocation("ProgramManagerPlugin", "Navigate_File")); initOptions(); diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/progmgr/MultiTabPluginTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/progmgr/MultiTabPluginTest.java index 09c75a9c78..e40454851c 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/progmgr/MultiTabPluginTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/progmgr/MultiTabPluginTest.java @@ -553,7 +553,7 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest { } private void selectTab(Program p) { - JPanel tab = runSwing(() -> panel.getTab(p)); + GTab tab = runSwing(() -> panel.getTab(p)); Point point = runSwing(() -> tab.getLocationOnScreen()); clickMouse(tab, MouseEvent.BUTTON1, point.x + 1, point.y + 1, 1, 0); assertEquals(p, getSelectedTabValue()); diff --git a/Ghidra/Framework/Docking/certification.manifest b/Ghidra/Framework/Docking/certification.manifest index 8c19f29bc6..4646a84d49 100644 --- a/Ghidra/Framework/Docking/certification.manifest +++ b/Ghidra/Framework/Docking/certification.manifest @@ -95,6 +95,7 @@ src/main/resources/images/mail-folder-outbox.png||Oxygen Icons - LGPL 3.0|||Oxyg src/main/resources/images/mail-receive.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END| src/main/resources/images/media-playback-start.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END| src/main/resources/images/menu16.gif||GHIDRA||reviewed||END| +src/main/resources/images/move.png||GHIDRA||||END| src/main/resources/images/oxygen-edit-redo.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END| src/main/resources/images/page_code.png||FAMFAMFAM Icons - CC 2.5||||END| src/main/resources/images/page_excel.png||FAMFAMFAM Icons - CC 2.5||||END| diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tab/GTab.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tab/GTab.java index 3f90233f5e..2d7ac23d40 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tab/GTab.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tab/GTab.java @@ -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 the type of the tab values */ -class GTab extends JPanel { +public class GTab 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 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 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 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 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 extends JPanel { } else { element.addMouseListener(listener); + element.addMouseMotionListener(listener); } } } @@ -163,6 +172,16 @@ class GTab 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); + } } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tab/GTabPanel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tab/GTabPanel.java index 8b3bd11aeb..ebfa53d340 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tab/GTabPanel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tab/GTabPanel.java @@ -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 extends JPanel { private Consumer 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 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 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 oldTab = getTab(oldValue); + if (oldTab != null) { + oldTab.setSelected(false); + } + GTab newTab = getTab(value); + newTab.setSelected(true); + } + else { + rebuildTabs(); + } + selectedTabConsumer.accept(value); } @@ -401,6 +423,15 @@ public class GTabPanel extends JPanel { return null; } + public GTab getTab(T value) { + for (GTab tab : allTabs) { + if (tab.getValue().equals(value)) { + return tab; + } + } + return null; + } + void showTabList() { if (tabList != null) { return; @@ -482,9 +513,13 @@ public class GTabPanel 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 extends JPanel { this.ignoreFocusLost = ignoreFocusLost; } - /*testing*/public JPanel getTab(T value) { - for (GTab tab : allTabs) { - if (tab.getValue().equals(value)) { - return tab; + void mouseDragged(GTab draggedTab, MouseEvent e) { + isDragging = true; + clearAllHighlights(); + GTab 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 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 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 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 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)); } } diff --git a/Ghidra/Framework/Docking/src/main/resources/images/move.png b/Ghidra/Framework/Docking/src/main/resources/images/move.png new file mode 100644 index 0000000000000000000000000000000000000000..72a97117ea2ad3b2e3a7adb6b245bf2f0f93eb3d GIT binary patch literal 194 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!73?$#)eFPFP2=EDUWoQ7h7=UC0hyf%(tOExQ zG&D5)2MYXW`2U|Fru7Ms2C{%4<5I&tpg3bmkY6x^!?PP{K#qf_i(^Q|t>gqC5J_lY znAt2a>*E>84uSJCjoAa1q&W06D~L83v9mU=N;%hX(2<2%k2Sm}#psU14$%V54r7Lg X>GJ#8{@>67n#JJh>gTe~DWM4f$y-6O literal 0 HcmV?d00001 diff --git a/Ghidra/Framework/Docking/src/test/java/docking/widgets/tab/GTabPanelTest.java b/Ghidra/Framework/Docking/src/test/java/docking/widgets/tab/GTabPanelTest.java index a1dceeaa4a..79f4745967 100644 --- a/Ghidra/Framework/Docking/src/test/java/docking/widgets/tab/GTabPanelTest.java +++ b/Ghidra/Framework/Docking/src/test/java/docking/widgets/tab/GTabPanelTest.java @@ -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 getHiddenTabs() { return runSwing(() -> gTabPanel.getHiddenTabs()); } diff --git a/Ghidra/Framework/Gui/src/main/java/generic/util/image/ImageUtils.java b/Ghidra/Framework/Gui/src/main/java/generic/util/image/ImageUtils.java index d9067aa6b9..d43441e2a1 100644 --- a/Ghidra/Framework/Gui/src/main/java/generic/util/image/ImageUtils.java +++ b/Ghidra/Framework/Gui/src/main/java/generic/util/image/ImageUtils.java @@ -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. @@ -27,6 +27,7 @@ import javax.swing.*; import generic.theme.GThemeDefaults.Colors; import ghidra.util.MathUtilities; import ghidra.util.Msg; +import resources.ResourceManager; public class ImageUtils { @@ -310,7 +311,12 @@ public class ImageUtils { icon.paintIcon(null, g, 0, 0); g.dispose(); - return new ImageIcon(newImage); + ImageIcon imageIcon = new ImageIcon(newImage); + String iconName = ResourceManager.getIconName(icon); + if (iconName != null) { + imageIcon.setDescription(iconName); + } + return imageIcon; } /**