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 Navigation
Go 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 0000000000..72a97117ea
Binary files /dev/null and b/Ghidra/Framework/Docking/src/main/resources/images/move.png differ
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;
}
/**