diff --git a/Ghidra/Features/Base/certification.manifest b/Ghidra/Features/Base/certification.manifest
index 0e001258da..51ca6cb420 100644
--- a/Ghidra/Features/Base/certification.manifest
+++ b/Ghidra/Features/Base/certification.manifest
@@ -718,7 +718,6 @@ src/main/resources/images/ThunkFunction.gif||GHIDRA||||END|
src/main/resources/images/U.gif||GHIDRA||||END|
src/main/resources/images/Unpackage.gif||GHIDRA||||END|
src/main/resources/images/V.png||GHIDRA||||END|
-src/main/resources/images/VCRFastForward.gif||GHIDRA||||END|
src/main/resources/images/akregator.png||Oxygen Icons - LGPL 3.0||||END|
src/main/resources/images/application_double.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
src/main/resources/images/applications-development.png||Oxygen Icons - LGPL 3.0||||END|
@@ -876,7 +875,6 @@ src/main/resources/images/page_white.png||FAMFAMFAM Icons - CC 2.5|||famfamfam s
src/main/resources/images/page_white_c.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
src/main/resources/images/pencil16.png||GHIDRA||||END|
src/main/resources/images/pin.png||GHIDRA||||END|
-src/main/resources/images/pinkX.gif||GHIDRA||||END|
src/main/resources/images/play_again.png||GHIDRA||||END|
src/main/resources/images/preferences-system.png||Tango Icons - Public Domain|||tango|END|
src/main/resources/images/question_zero.png||GHIDRA||||END|
@@ -916,7 +914,6 @@ src/main/resources/images/view-sort-descending.png||Oxygen Icons - LGPL 3.0|||Ox
src/main/resources/images/window.png||GHIDRA||||END|
src/main/resources/images/wizard.png||Nuvola Icons - LGPL 2.1|||nuvola|END|
src/main/resources/images/x-office-document-template.png||Tango Icons - Public Domain|||tango icon set|END|
-src/main/resources/images/x.gif||GHIDRA||||END|
src/main/resources/images/xor.png||GHIDRA||||END|
src/main/resources/pcodetest/chunk1.hinc||GHIDRA||||END|
src/main/resources/pcodetest/chunk2.hinc||GHIDRA||||END|
diff --git a/Ghidra/Features/Base/src/main/help/help/topics/ProgramManagerPlugin/Closing_Program_Files.htm b/Ghidra/Features/Base/src/main/help/help/topics/ProgramManagerPlugin/Closing_Program_Files.htm
index 2b2388549d..4d2480ea47 100644
--- a/Ghidra/Features/Base/src/main/help/help/topics/ProgramManagerPlugin/Closing_Program_Files.htm
+++ b/Ghidra/Features/Base/src/main/help/help/topics/ProgramManagerPlugin/Closing_Program_Files.htm
@@ -27,7 +27,7 @@
To Close a Program File
- - From the Tool menu, select File
+ - From the Tool menu, select File
Close
- If changes were made to the program and they haven't been saved yet, the Save
@@ -35,7 +35,7 @@
- 
+
The buttons in the Save Program? dialog perform the following functions.
@@ -49,39 +49,50 @@
Cancel - Leaves the program open in the current tool without any changes being
saved.
-
+
-
If the listing
- window is open and multiple programs are open, the program names are displayed on tabs across
- the top of the listing window. Programs can be closed by selecting the appropriate tab
- and pressing the corresponding "x" button.
+
If the
+ listing window is open and multiple programs are open, the program names are displayed on tabs
+ across the top of the listing window. Programs can be closed by selecting the appropriate
+ tab and pressing the corresponding "x" button or right-clicking on the tab and choosing the
+ close menu option.
- 
+ 
To Close All Programs
- - From the Tool menu, select File
Close
- All
+ - From the Tool menu, select File
+ Close All
- For each program that was changed, the Save Program? dialog appears.
+
+
If the
+ listing window is open and multiple programs are open, you can also close all programs by
+ right-clicking on any tab and choosing the Close All menu option.
To Close All Programs Other Than The Current Program
- - From the Tool menu, select File
Close
- Others
+ - From the Tool menu, select File
+ Close Others
- - For each of the other programs that was changed, the Save Program? dialog appears.
+ - For each of the other programs that was changed, the Save Program? dialog
+ appears.
+
+
If the
+ listing window is open and multiple programs are open, you can also close other programs by
+ right-clicking on the tab of the program to keep and choosing the Close Others menu
+ option.
Provided by: Program Manager Plugin
diff --git a/Ghidra/Features/Base/src/main/help/help/topics/Tool/ToolOptions_Dialog.htm b/Ghidra/Features/Base/src/main/help/help/topics/Tool/ToolOptions_Dialog.htm
index f65a794c98..97ef8cdc1d 100644
--- a/Ghidra/Features/Base/src/main/help/help/topics/Tool/ToolOptions_Dialog.htm
+++ b/Ghidra/Features/Base/src/main/help/help/topics/Tool/ToolOptions_Dialog.htm
@@ -293,6 +293,13 @@
it loses focus. Use the Windows menu to make the undocked window visible.
+
+ Goto Dialog Memory |
+
+ Selected means that the last goto query will
+ remain in the dialog the next time the dialog is invoked. |
+
+
Max Goto Entries |
@@ -301,11 +308,24 @@
Label dialog
+
+ Max Navigation History Size |
+
+ Max number of items to retain in the navigation
+ history. dialog |
+
+
+
+ Show Program Tabs Always |
+
+ If selected, a program tab will be displayed even
+ there is only one program open. |
+
Subroutine Model |
- Sets the default subroutine model. This setting is mainly used when creating
+ Sets the default subroutine model. This setting is mainly used when creating
call graphs. See Block Models
for a description of the valid Models.
@@ -370,16 +390,6 @@
| Description |
-
-
- Swing Look and Feel |
-
- This controls the
- appearance of the UI widgets for things such as colors and fonts. Each operating
- system provides a different default Look and Feel. Some of these work better than
- others. |
-
-
Automatically Save Tools |
@@ -387,6 +397,24 @@
when the tool is closed.
+
+ Default Tool Launch Mode |
+
+ This controls if a new or already running tool should
+ be used during default launch. Tool "reuse" mode will open selected file within a
+ suitable running tool if one can be identified, otherwise a new tool will be
+ launched. |
+
+
+
+ Docking Windows On Top |
+
+ Selected means to show each undocked window on top of
+ its parent tool window; the undocked window will not get "lost" behind its parent
+ window. Unselected means that the undocked window may go behind other windows once
+ it loses focus. Use the Windows menu to make the undocked window visible. |
+
+
Restore Previous Project |
@@ -394,37 +422,26 @@
opens the previously loaded project on startup.
-
- Default Tool Launch Mode |
-
- This controls if a new or already running tool should
- be used during default launch. Tool "reuse" mode will open selected file within a
- suitable running tool if one can be identified, otherwise a new tool will be
- launched. |
+
+ Show Tooltips |
+
+ This controls whether or not Ghidra will show
+ tooltips. |
-
-
- Use
- Inverted Colors |
-
-
- This is a prototype feature that allows the user to invert each color
- of the UI. Doing this effectively creates a Dark Theme, which some users find
- less visually straining.
-
-
- As a prototype
- feature, this feature has many known issues, including:
-
-
- - Pre-generated content, such as images, icons and help files will have
- inverted colors,
-
- - Some color combinations will be difficult to read
-
-
- |
+
+ Use DataBuffer Output Compression |
+
+ This controls whether or not Ghidra will compress
+ data being sent to the server. |
+
+ Use Notification Animation |
+
+ This controls whether or not Ghidra will use
+ automations to provide visual feedback that something is happening, such as
+ launching a tool. |
+
+
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/nav/ListingPanelContainer.java b/Ghidra/Features/Base/src/main/java/ghidra/app/nav/ListingPanelContainer.java
index 5c4061d60a..4a4551cbd4 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/nav/ListingPanelContainer.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/nav/ListingPanelContainer.java
@@ -100,4 +100,8 @@ public class ListingPanelContainer extends JPanel {
add(northComponent, BorderLayout.NORTH);
}
}
+
+ public JComponent getNorthPanel() {
+ return northComponent;
+ }
}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/CodeViewerProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/CodeViewerProvider.java
index b2cd9cf11f..d7af298e11 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/CodeViewerProvider.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/CodeViewerProvider.java
@@ -38,6 +38,7 @@ import docking.widgets.fieldpanel.FieldPanel;
import docking.widgets.fieldpanel.HoverHandler;
import docking.widgets.fieldpanel.internal.FieldPanelCoordinator;
import docking.widgets.fieldpanel.support.*;
+import docking.widgets.tab.GTabPanel;
import generic.theme.GIcon;
import ghidra.app.context.ListingActionContext;
import ghidra.app.nav.ListingPanelContainer;
@@ -45,6 +46,7 @@ import ghidra.app.nav.LocationMemento;
import ghidra.app.plugin.core.clipboard.CodeBrowserClipboardProvider;
import ghidra.app.plugin.core.codebrowser.actions.*;
import ghidra.app.plugin.core.codebrowser.hover.ListingHoverService;
+import ghidra.app.plugin.core.progmgr.ProgramTabActionContext;
import ghidra.app.services.*;
import ghidra.app.util.*;
import ghidra.app.util.viewer.field.ListingField;
@@ -299,6 +301,17 @@ public class CodeViewerProvider extends NavigatableComponentProviderAdapter
}
return new OtherPanelContext(this, program);
}
+
+ JComponent northPanel = decorationPanel.getNorthPanel();
+ if (northPanel != null && northPanel.isAncestorOf((Component) source)) {
+ if (northPanel instanceof GTabPanel tabPanel) {
+ Program tabValue = (Program) tabPanel.getValueFor(event);
+ if (tabValue != null) {
+ return new ProgramTabActionContext(this, tabValue, tabPanel);
+ }
+ }
+ }
+
return createContext(getContextForMarginPanels(listingPanel, event));
}
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 16f7be18b3..e8a99fd060 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
@@ -23,6 +23,7 @@ import javax.swing.*;
import docking.ActionContext;
import docking.DockingUtils;
import docking.action.*;
+import docking.action.builder.ActionBuilder;
import docking.tool.ToolConstants;
import docking.widgets.tab.GTabPanel;
import generic.theme.GIcon;
@@ -32,10 +33,13 @@ import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.services.CodeViewerService;
import ghidra.app.services.ProgramManager;
import ghidra.framework.model.*;
+import ghidra.framework.options.OptionsChangeListener;
+import ghidra.framework.options.ToolOptions;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.program.model.listing.Program;
import ghidra.util.HelpLocation;
+import ghidra.util.bean.opteditor.OptionsVetoException;
/**
* Plugin to show a "tab" for each open program; the selected tab is the activated program.
@@ -53,9 +57,10 @@ import ghidra.util.HelpLocation;
eventsConsumed = { ProgramOpenedPluginEvent.class, ProgramClosedPluginEvent.class, ProgramActivatedPluginEvent.class, ProgramVisibilityChangePluginEvent.class }
)
//@formatter:on
-public class MultiTabPlugin extends Plugin implements DomainObjectListener {
+public class MultiTabPlugin extends Plugin implements DomainObjectListener, OptionsChangeListener {
private final static Icon TRANSIENT_ICON = new GIcon("icon.plugin.programmanager.transient");
private final static Icon EMPTY8_ICON = new GIcon("icon.plugin.programmanager.empty.small");
+ private static final String SHOW_TABS_ALWAYS = "Show Program Tabs Always";
//
// Unusual Code Alert!: We can't initialize these fields below because calling
@@ -82,12 +87,32 @@ public class MultiTabPlugin extends Plugin implements DomainObjectListener {
public MultiTabPlugin(PluginTool tool) {
super(tool);
-
createActions();
}
private void createActions() {
+ new ActionBuilder("Close Program", getName())
+ .popupMenuPath("Close")
+ .helpLocation(new HelpLocation("ProgramManagerPlugin", "Close_Program"))
+ .withContext(ProgramTabActionContext.class)
+ .onAction(c -> progService.closeProgram(c.getProgram(), false))
+ .buildAndInstall(tool);
+
+ new ActionBuilder("Close Other Programs", getName())
+ .popupMenuPath("Close Others")
+ .helpLocation(new HelpLocation("ProgramManagerPlugin", "Close_Others"))
+ .withContext(ProgramTabActionContext.class)
+ .onAction(c -> closeOtherPrograms(c.getProgram()))
+ .buildAndInstall(tool);
+
+ new ActionBuilder("Close All Programs", getName())
+ .popupMenuPath("Close All")
+ .helpLocation(new HelpLocation("ProgramManagerPlugin", "Close_All"))
+ .withContext(ProgramTabActionContext.class)
+ .onAction(c -> progService.closeAllPrograms(false))
+ .buildAndInstall(tool);
+
String firstGroup = "1";
String secondGroup = "2";
@@ -113,7 +138,7 @@ public class MultiTabPlugin extends Plugin implements DomainObjectListener {
@Override
public void actionPerformed(ActionContext context) {
// highlight the next tab
- nextProgramPressed();
+ cycleNextProgram(true);
}
};
goToNextProgramAction.setEnabled(false);
@@ -127,7 +152,7 @@ public class MultiTabPlugin extends Plugin implements DomainObjectListener {
@Override
public void actionPerformed(ActionContext context) {
// highlight the previous tab
- previousProgramPressed();
+ cycleNextProgram(false);
}
};
goToPreviousProgramAction.setEnabled(false);
@@ -164,6 +189,11 @@ public class MultiTabPlugin extends Plugin implements DomainObjectListener {
tool.addAction(goToPreviousProgramAction);
}
+ private void closeOtherPrograms(Program keepProgram) {
+ progService.setCurrentProgram(keepProgram);
+ progService.closeOtherPrograms(false);
+ }
+
private void updateActionEnablement() {
// the next/previous actions should not be enabled if no tabs are hidden
boolean enable = (tabPanel.getTabCount() > 1);
@@ -185,12 +215,11 @@ public class MultiTabPlugin extends Plugin implements DomainObjectListener {
tabPanel.showTabList(!tabPanel.isShowingTabList());
}
- private void highlightNextProgram(boolean forwardDirection) {
- tabPanel.highlightNextTab(forwardDirection);
- }
-
private void selectHighlightedProgram() {
- tabPanel.selectTab(tabPanel.getHighlightedTabValue());
+ Program highlightedTabValue = tabPanel.getHighlightedTabValue();
+ if (highlightedTabValue != null) {
+ tabPanel.selectTab(highlightedTabValue);
+ }
}
String getStringUsedInList(Program program) {
@@ -244,20 +273,15 @@ public class MultiTabPlugin extends Plugin implements DomainObjectListener {
KeyStroke stroke = KeyStroke.getKeyStrokeForEvent(e);
if (stroke.equals(NEXT_TAB_KEYSTROKE)) {
- nextProgramPressed();
+ cycleNextProgram(true);
}
else if (stroke.equals(PREVIOUS_TAB_KEYSTROKE)) {
- previousProgramPressed();
+ cycleNextProgram(false);
}
}
- private void nextProgramPressed() {
- highlightNextProgram(true);
- selectHighlightedProgramTimer.restart();
- }
-
- private void previousProgramPressed() {
- highlightNextProgram(false);
+ private void cycleNextProgram(boolean forward) {
+ tabPanel.highlightNextPreviousTab(forward);
selectHighlightedProgramTimer.restart();
}
@@ -276,13 +300,32 @@ public class MultiTabPlugin extends Plugin implements DomainObjectListener {
tabPanel.setIconFunction(p -> getIcon(p));
tabPanel.setToolTipFunction(p -> getToolTip(p));
tabPanel.setSelectedTabConsumer(p -> programSelected(p));
- tabPanel.setRemoveTabActionPredicate(p -> progService.closeProgram(p, false));
+ tabPanel.setCloseTabConsumer(p -> progService.closeProgram(p, false));
+
+ initOptions();
progService = tool.getService(ProgramManager.class);
cvService = tool.getService(CodeViewerService.class);
cvService.setNorthComponent(tabPanel);
}
+ private void initOptions() {
+ ToolOptions options = tool.getOptions(ToolConstants.TOOL_OPTIONS);
+ options.registerOption(SHOW_TABS_ALWAYS, false, null,
+ "If true, program tabs will be displayed even if only one");
+
+ tabPanel.setShowTabsAlways(options.getBoolean(SHOW_TABS_ALWAYS, false));
+ options.addOptionsChangeListener(this);
+ }
+
+ @Override
+ public void optionsChanged(ToolOptions options, String optionName, Object oldValue,
+ Object newValue) throws OptionsVetoException {
+ if (optionName.equals(SHOW_TABS_ALWAYS)) {
+ tabPanel.setShowTabsAlways((Boolean) newValue);
+ }
+ }
+
private Icon getIcon(Program program) {
ProjectLocator projectLocator = program.getDomainFile().getProjectLocator();
if (projectLocator != null && projectLocator.isTransient()) {
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramTabActionContext.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramTabActionContext.java
new file mode 100644
index 0000000000..925e835b06
--- /dev/null
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramTabActionContext.java
@@ -0,0 +1,39 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * 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.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.app.plugin.core.progmgr;
+
+import java.awt.Component;
+
+import docking.ComponentProvider;
+import docking.DefaultActionContext;
+import ghidra.program.model.listing.Program;
+
+/**
+ * Action context for program tabs
+ */
+public class ProgramTabActionContext extends DefaultActionContext {
+ public ProgramTabActionContext(ComponentProvider provider, Program program, Component source) {
+ super(provider, program, source);
+ }
+
+ /**
+ * Returns the program for the tab that was clicked on.
+ * @return the program for the tab that was clicked on
+ */
+ public Program getProgram() {
+ return (Program) getContextObject();
+ }
+}
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 083ee6f059..033bc019ea 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
@@ -279,7 +279,6 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest {
builder.createMemory("test", "0x0", 100);
Program p = doOpenProgram(builder.getProgram(), true);
p.setTemporary(false); // we need to be notified of changes
-
// select notepad
panel.selectTab(p);
int transactionID = p.startTransaction("test");
@@ -622,6 +621,7 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest {
// don't let focus issues hide the popup list
panel.setIgnoreFocus(true);
+ panel.setShowTabsAlways(true);
return p;
}
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 a86e20ec9a..db07bb7f6a 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,9 +15,11 @@
*/
package docking.widgets.tab;
+import java.awt.Container;
import java.awt.event.*;
import java.util.*;
-import java.util.function.*;
+import java.util.function.Consumer;
+import java.util.function.Function;
import java.util.stream.Collectors;
import javax.swing.*;
@@ -59,9 +61,9 @@ public class GTabPanel extends JPanel {
private Function nameFunction = v -> v.toString();
private Function iconFunction = Dummy.function();
private Function toolTipFunction = Dummy.function();
- private Predicate removeTabPredicate = Dummy.predicate();
private Consumer selectedTabConsumer = Dummy.consumer();
- private Consumer removedTabConsumer = Dummy.consumer();
+ private Consumer closeTabConsumer = t -> removeTab(t);
+ private boolean showTabsAlways = true;
/**
* Constructor
@@ -93,12 +95,15 @@ public class GTabPanel extends JPanel {
case KeyEvent.VK_SPACE:
case KeyEvent.VK_ENTER:
selectHighlightedValue();
+ e.consume();
break;
case KeyEvent.VK_LEFT:
- highlightNextTab(false);
+ highlightNextPreviousTab(false);
+ e.consume();
break;
case KeyEvent.VK_RIGHT:
- highlightNextTab(true);
+ highlightNextPreviousTab(true);
+ e.consume();
break;
}
}
@@ -148,11 +153,11 @@ public class GTabPanel extends JPanel {
highlightedValue = null;
// ensure there is a valid selected value
if (value == selectedValue) {
- selectedValue = allValues.isEmpty() ? null : allValues.iterator().next();
+ selectTab(null);
+ }
+ else {
+ rebuildTabs();
}
-
- rebuildTabs();
- removedTabConsumer.accept(value);
}
/**
@@ -162,12 +167,12 @@ public class GTabPanel extends JPanel {
public void removeTabs(Collection values) {
allValues.removeAll(values);
- // ensure there is a valid selected value
if (!allValues.contains(selectedValue)) {
- selectedValue = allValues.isEmpty() ? null : allValues.iterator().next();
+ selectTab(null);
+ }
+ else {
+ rebuildTabs();
}
-
- rebuildTabs();
}
/**
@@ -193,10 +198,7 @@ public class GTabPanel extends JPanel {
* @param value the value whose tab is to be selected
*/
public void selectTab(T value) {
- if (value == null) {
- return;
- }
- if (!allValues.contains(value)) {
+ if (value != null && !allValues.contains(value)) {
throw new IllegalArgumentException(
"Attempted to set selected value to non added value");
}
@@ -288,12 +290,12 @@ public class GTabPanel extends JPanel {
}
/**
- * Moves the highlight the next or previous tab from the current highlight. If there is no
+ * Moves the highlight to the next or previous tab from the current highlight. If there is no
* current highlight, it will highlight the next or previous tab from the selected tab.
* @param forward true moves the highlight to the right; otherwise move the highlight to the
* left
*/
- public void highlightNextTab(boolean forward) {
+ public void highlightNextPreviousTab(boolean forward) {
if (allValues.size() < 2) {
return;
}
@@ -347,20 +349,13 @@ public class GTabPanel extends JPanel {
}
/**
- * Sets the predicate that will be called before removing a tab via the gui close control. If
- * the predicate returns true, the tab will be removed, otherwise the remove will be cancelled.
- * @param removeTabPredicate the predicate called to decide if a tab value can be removed
+ * Sets the predicate that will be called before removing a tab via the gui close control. Note
+ * that that tab panel's default action is to remove the tab value, but if you set your own
+ * consumer, you have the responsibility to remove the value.
+ * @param closeTabConsumer the consumer called when the close gui control is clicked.
*/
- public void setRemoveTabActionPredicate(Predicate removeTabPredicate) {
- this.removeTabPredicate = removeTabPredicate;
- }
-
- /**
- * Sets the consumer to be notified if a tab value is removed.
- * @param removedTabConsumer the consumer to be notified when tab values are removed
- */
- public void setRemovedTabConsumer(Consumer removedTabConsumer) {
- this.removedTabConsumer = removedTabConsumer;
+ public void setCloseTabConsumer(Consumer closeTabConsumer) {
+ this.closeTabConsumer = closeTabConsumer;
}
/**
@@ -379,6 +374,33 @@ public class GTabPanel extends JPanel {
return tabList != null;
}
+ /**
+ * Sets whether or not tabs should be display when there is only one tab.
+ * @param b true to show one tab; false collapses tab panel when only one tab exists
+ */
+ public void setShowTabsAlways(boolean b) {
+ showTabsAlways = b;
+ rebuildTabs();
+ }
+
+ /**
+ * Returns the value of the tab that generated the given mouse event. If the mouse event
+ * is not from one of the tabs, then null is returned.
+ * @param event the MouseEvent to get a value for
+ * @return the value of the tab that generated the mouse event
+ */
+ @SuppressWarnings("unchecked")
+ public T getValueFor(MouseEvent event) {
+ Object source = event.getSource();
+ if (source instanceof JLabel label) {
+ Container parent = label.getParent();
+ if (parent instanceof GTab gTab) {
+ return (T) gTab.getValue();
+ }
+ }
+ return null;
+ }
+
void showTabList() {
if (tabList != null) {
return;
@@ -389,9 +411,7 @@ public class GTabPanel extends JPanel {
}
void closeTab(T value) {
- if (removeTabPredicate.test(value)) {
- removeTab(value);
- }
+ closeTabConsumer.accept(value);
}
private void selectHighlightedValue() {
@@ -465,30 +485,28 @@ public class GTabPanel extends JPanel {
private void doAddValue(T value) {
Objects.requireNonNull(value);
allValues.add(value);
-
- // make the first added value selected, non-empty panels must always have a selected value
- if (allValues.size() == 1) {
- selectedValue = value;
- }
}
private void rebuildTabs() {
allTabs.clear();
removeAll();
closeTabList();
- if (allValues.isEmpty()) {
- validate();
+ if (!shouldShowTabs()) {
+ revalidate();
repaint();
return;
}
- GTab selectedTab = new GTab(this, selectedValue, true);
- int availableWidth = getPanelWidth() - getTabWidth(selectedTab);
-
+ GTab selectedTab = null;
+ int availableWidth = getPanelWidth();
+ if (selectedValue != null) {
+ selectedTab = new GTab(this, selectedValue, true);
+ availableWidth -= getTabWidth(selectedTab);
+ }
createNonSelectedTabsForWidth(availableWidth);
// a negative available width means there wasn't even enough room for the selected value tab
- if (availableWidth >= 0) {
+ if (selectedValue != null && availableWidth >= 0) {
allTabs.add(getIndexToInsertSelectedValue(allTabs.size()), selectedTab);
}
@@ -504,24 +522,44 @@ public class GTabPanel extends JPanel {
}
updateTabColors();
updateAccessibleName();
- validate();
+ revalidate();
repaint();
}
+ private boolean shouldShowTabs() {
+ if (allValues.isEmpty()) {
+ return false;
+ }
+ if (allValues.size() == 1 && !showTabsAlways) {
+ return false;
+ }
+ return true;
+ }
+
private void updateAccessibleName() {
getAccessibleContext().setAccessibleName(getAccessibleName());
}
- private String getAccessibleName() {
- String panelName = tabTypeName + "Tab Panel";
+ String getAccessibleName() {
+ StringBuilder builder = new StringBuilder(tabTypeName);
+ builder.append(" Tab Panel: ");
if (allValues.isEmpty()) {
- return panelName + ": No Tabs";
+ builder.append("No Tabs");
+ return builder.toString();
+ }
+ if (selectedValue != null) {
+ builder.append(getDisplayName(selectedValue));
+ builder.append(" selected");
+ }
+ else {
+ builder.append("No Selected Tab");
}
- String accessibleName = panelName + ": " + getDisplayName(selectedValue) + "Selected";
if (highlightedValue != null) {
- accessibleName += ": " + getDisplayName(highlightedValue) + " highlighted";
+ builder.append(": ");
+ builder.append(getDisplayName(highlightedValue));
+ builder.append(" highlighted");
}
- return accessibleName;
+ return builder.toString();
}
private int getIndexToInsertSelectedValue(int maxIndex) {
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 ef0d708816..01d0693424 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
@@ -21,7 +21,6 @@ import java.awt.BorderLayout;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
-import java.util.function.Predicate;
import javax.swing.*;
@@ -64,17 +63,29 @@ public class GTabPanelTest extends AbstractDockingTest {
}
@Test
- public void testFirstTabIsSelectedByDefault() {
- assertEquals("One", getSelectedValue());
+ public void testNoTabIsSelectedByDefault() {
+ assertEquals(null, getSelectedValue());
+ }
+
+ @Test
+ public void testSettingNoTabSelected() {
+
+ AtomicReference selectedValue = new AtomicReference();
+ Consumer c = s -> selectedValue.set(s);
+ runSwing(() -> gTabPanel.setSelectedTabConsumer(c));
+ setSelectedValue("One");
+ assertEquals("One", selectedValue.get());
+ setSelectedValue(null);
+ assertEquals(null, selectedValue.get());
}
@Test
public void testAddValue() {
assertEquals(3, getTabCount());
- assertEquals("One", getSelectedValue());
+ assertEquals(null, getSelectedValue());
addValue("Four");
assertEquals(4, getTabCount());
- assertEquals("One", getSelectedValue());
+ assertEquals(null, getSelectedValue());
assertEquals("Four", getValue(3));
}
@@ -86,6 +97,7 @@ public class GTabPanelTest extends AbstractDockingTest {
@Test
public void testSwitchToInvalidValue() {
+ setSelectedValue("One");
try {
gTabPanel.selectTab("Four");
fail("expected exception");
@@ -99,11 +111,12 @@ public class GTabPanelTest extends AbstractDockingTest {
@Test
public void testCloseSelected() {
+ setSelectedValue("One");
assertEquals(3, getTabCount());
assertEquals("One", getSelectedValue());
removeTab("One");
assertEquals(2, getTabCount());
- assertEquals("Two", getSelectedValue());
+ assertNull(getSelectedValue());
}
@Test
@@ -148,50 +161,76 @@ public class GTabPanelTest extends AbstractDockingTest {
@Test
public void testRemovedConsumer() {
- AtomicReference removedValue = new AtomicReference();
- Consumer c = s -> removedValue.set(s);
- runSwing(() -> gTabPanel.setRemovedTabConsumer(c));
+ AtomicReference closedValue = new AtomicReference();
+ Consumer c = s -> closedValue.set(s);
+ runSwing(() -> gTabPanel.setCloseTabConsumer(c));
runSwing(() -> gTabPanel.closeTab("Two"));
- assertEquals("Two", removedValue.get());
+ assertEquals("Two", closedValue.get());
}
@Test
- public void testSetRemoveTabPredicateAcceptsRemove() {
- AtomicReference removePredicateCallValue = new AtomicReference();
- Predicate p = s -> {
- removePredicateCallValue.set(s);
- return true;
+ public void testSetRemoveTabConsumer() {
+ AtomicReference closedValueReference = new AtomicReference();
+ Consumer c = s -> {
+ closedValueReference.set(s);
+ gTabPanel.removeTab(s);
};
- runSwing(() -> gTabPanel.setRemoveTabActionPredicate(p));
+ runSwing(() -> gTabPanel.setCloseTabConsumer(c));
runSwing(() -> gTabPanel.closeTab("Two"));
- assertEquals("Two", removePredicateCallValue.get());
+ assertEquals("Two", closedValueReference.get());
assertEquals(2, getTabCount());
}
@Test
- public void testSetRemoveTabPredicateRejectsRemove() {
- AtomicReference removePredicateCallValue = new AtomicReference();
- Predicate p = s -> {
- removePredicateCallValue.set(s);
- return false;
- };
- runSwing(() -> gTabPanel.setRemoveTabActionPredicate(p));
- runSwing(() -> gTabPanel.closeTab("Two"));
- assertEquals("Two", removePredicateCallValue.get());
- assertEquals(3, getTabCount());
+ public void testHighlightNext() {
+ assertNull(getHighlightedValue());
+ highlightNextTab(true);
+ assertEquals("One", getHighlightedValue());
+ highlightNextTab(true);
+ assertEquals("Two", getHighlightedValue());
+ highlightNextTab(false);
+ assertEquals("One", getHighlightedValue());
+ highlightNextTab(false);
+ assertEquals("Three Three Three", getHighlightedValue());
+ setSelectedValue("One");
+ highlightNextTab(true);
+ assertEquals("Two", getHighlightedValue());
}
@Test
- public void testHighlightNext() {
- assertNull(gTabPanel.getHighlightedTabValue());
- runSwing(() -> gTabPanel.highlightNextTab(true));
- assertEquals("Two", gTabPanel.getHighlightedTabValue());
- runSwing(() -> gTabPanel.highlightNextTab(true));
- assertEquals("Three Three Three", gTabPanel.getHighlightedTabValue());
- runSwing(() -> gTabPanel.highlightNextTab(false));
- assertEquals("Two", gTabPanel.getHighlightedTabValue());
- runSwing(() -> gTabPanel.highlightNextTab(false));
- assertNull(gTabPanel.getHighlightedTabValue());
+ public void testGetAccessibleNameNoTabs() {
+ removeTab("One");
+ removeTab("Two");
+ removeTab("Three Three Three");
+ assertEquals("Test Tab Panel: No Tabs", gTabPanel.getAccessibleName());
+ }
+
+ @Test
+ public void testGetAccessibleNameNoTabSelected() {
+ setSelectedValue(null);
+ assertEquals("Test Tab Panel: No Selected Tab", gTabPanel.getAccessibleName());
+ }
+
+ @Test
+ public void testGetAccessiblNameTabSelected() {
+ setSelectedValue("Two");
+ assertEquals("Test Tab Panel: Two selected", gTabPanel.getAccessibleName());
+ }
+
+ @Test
+ public void testGetAccessiblNameNoTabSelectedAndTabHighighted() {
+ setSelectedValue(null);
+ highlightNextTab(true);
+ assertEquals("Test Tab Panel: No Selected Tab: One highlighted",
+ gTabPanel.getAccessibleName());
+ }
+
+ @Test
+ public void testGetAccessiblNameTabSelectedAndTabHighighted() {
+ setSelectedValue("One");
+ highlightNextTab(true);
+ assertEquals("Test Tab Panel: One selected: Two highlighted",
+ gTabPanel.getAccessibleName());
}
private List getHiddenTabs() {
@@ -210,6 +249,10 @@ public class GTabPanelTest extends AbstractDockingTest {
runSwing(() -> gTabPanel.selectTab(value));
}
+ private void highlightNextTab(boolean b) {
+ runSwing(() -> gTabPanel.highlightNextPreviousTab(b));
+ }
+
private void removeTab(String value) {
runSwing(() -> gTabPanel.removeTab(value));
}
@@ -222,6 +265,10 @@ public class GTabPanelTest extends AbstractDockingTest {
return runSwing(() -> gTabPanel.getSelectedTabValue());
}
+ private String getHighlightedValue() {
+ return runSwing(() -> gTabPanel.getHighlightedTabValue());
+ }
+
private String getValue(int i) {
return runSwing(() -> gTabPanel.getTabValues().get(i));
}