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

    -
  1. From the Tool menu, select File +
  2. From the Tool menu, select File Close
  3. 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

      -
    1. From the Tool menu, select File Close - All
    2. +
    3. From the Tool menu, select File + Close All
    4. 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

      -
    1. From the Tool menu, select File Close - Others
    2. +
    3. From the Tool menu, select File + Close Others
    4. -
    5. For each of the other programs that was changed, the Save Program? dialog appears.
    6. +
    7. 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)); }