Merge remote-tracking branch

'origin/GP-5646_ghidragon_drag_n_drop_program_tabs--SQUASHED'
(Closes #8099)
This commit is contained in:
Ryan Kurtz 2025-07-11 11:56:07 -04:00
commit f3d4ccbf2f
9 changed files with 192 additions and 28 deletions

View file

@ -47,7 +47,13 @@
window.</P>
<P>Those programs listed in bold are those that are hidden.</P>
<BLOCKQUOTE>
<BLOCKQUOTE>
<P><IMG src="help/shared/tip.png" alt="" border="0">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. </P>
</BLOCKQUOTE>
</BLOCKQUOTE>
<H2>Navigation Actions</H2>
<!-- Next and Previous -->
@ -118,7 +124,8 @@
<P>To execute this action, from the Tool menu, select <B>Navigation<IMG src=
"help/shared/arrow.gif" border="0">Go To Last Active Program</B>.</P>
</BLOCKQUOTE>
</BLOCKQUOTE>
</BLOCKQUOTE>>
<P align="left" class="providedbyplugin">Provided by: <I>Program Manager</I> Plugin</P>

View file

@ -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();

View file

@ -553,7 +553,7 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest {
}
private void selectTab(Program p) {
JPanel tab = runSwing(() -> panel.getTab(p));
GTab<Program> 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());

View file

@ -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|

View file

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

View file

@ -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;
}
/**