Merge remote-tracking branch 'origin/GP-4475_ghidragon_improvements_to_GTabPanel--SQUASHED'

This commit is contained in:
Ryan Kurtz 2024-04-02 07:52:05 -04:00
commit 37c798604a
10 changed files with 375 additions and 166 deletions

View file

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

View file

@ -27,7 +27,7 @@
<H2>To Close a Program File</H2>
<OL type="1">
<LI>From the Tool menu, select <B>File<IMG src="help/shared/arrow.gif" border="0">
<LI>From the Tool menu, select <B>File<IMG alt="" src="help/shared/arrow.gif" border="0">
Close</B></LI>
<LI>If changes were made to the program and they haven't been saved yet, the <I>Save
@ -35,7 +35,7 @@
</OL>
<P align="center"><BR>
<IMG src="images/SaveProgram.png" border="0"></P>
<IMG alt="" src="images/SaveProgram.png" border="0"></P>
<BLOCKQUOTE>
<P>The buttons in the <I>Save Program?</I> dialog perform the following functions.</P>
@ -49,39 +49,50 @@
<LI><B>Cancel</B> - Leaves the program open in the current tool without any changes being
saved.</LI>
</UL>
</BLOCKQUOTE>
</BLOCKQUOTE><A name="Close_Program"></A>
<P align="left"><IMG border="0" src="help/shared/tip.png">&nbsp;&nbsp;&nbsp; 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.&nbsp; Programs can be closed by selecting the appropriate tab
and pressing the corresponding "x" button.</P>
<P align="left"><IMG alt="" border="0" src="help/shared/tip.png">&nbsp;&nbsp;&nbsp; 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.&nbsp; Programs can be closed by selecting the appropriate
tab and pressing the corresponding "x" button or right-clicking on the tab and choosing the
<B>close</B> menu option.</P>
<P align="center"><IMG border="0" src="images/ClosedTab.png"></P>
<P align="center"><IMG alt="" border="0" src="images/ClosedTab.png"></P>
<H2><A name="Close_All"></A>To Close All Programs</H2>
<BLOCKQUOTE>
<OL type="1">
<LI>From the Tool menu, select <B>File<IMG src="help/shared/arrow.gif" border="0"> Close
All</B></LI>
<LI>From the Tool menu, select <B>File<IMG alt="" src="help/shared/arrow.gif" border="0">
Close All</B></LI>
<LI>For each program that was changed, the <I>Save Program?</I> dialog appears.</LI>
</OL>
<P>&nbsp;</P>
<P align="left"><IMG alt="" border="0" src="help/shared/tip.png">&nbsp;&nbsp;&nbsp; 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 <B>Close All</B> menu option.</P>
</BLOCKQUOTE>
<H2><A name="Close_Others"></A>To Close All Programs Other Than The Current Program</H2>
<BLOCKQUOTE>
<OL type="1">
<LI>From the Tool menu, select <B>File<IMG src="help/shared/arrow.gif" border="0"> Close
Others</B></LI>
<LI>From the Tool menu, select <B>File<IMG alt="" src="help/shared/arrow.gif" border="0">
Close Others</B></LI>
<LI>For each of the other programs that was changed, the <I>Save Program?</I> dialog appears.</LI>
<LI>For each of the other programs that was changed, the <I>Save Program?</I> dialog
appears.</LI>
</OL>
<P>&nbsp;</P>
<P align="left"><IMG alt="" border="0" src="help/shared/tip.png">&nbsp;&nbsp;&nbsp; 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 <B>Close Others</B> menu
option.</P>
</BLOCKQUOTE>
<P align="left" class="providedbyplugin">Provided by: <I>Program Manager</I> Plugin</P>

View file

@ -293,6 +293,13 @@
it loses focus. Use the Windows menu to make the undocked window visible.</TD>
</TR>
<TR valign="middle">
<TD valign="top" width="200" align="left">Goto Dialog Memory</TD>
<TD valign="top" align="left">Selected means that the last goto query will
remain in the dialog the next time the dialog is invoked.</TD>
</TR>
<TR valign="middle">
<TD valign="top" width="200" align="left">Max Goto Entries</TD>
@ -301,11 +308,24 @@
Label</A> dialog</TD>
</TR>
<TR valign="middle">
<TD valign="top" width="200" align="left">Max Navigation History Size</TD>
<TD valign="top" align="left">Max number of items to retain in the navigation
history.</A> dialog</TD>
</TR>
<TR valign="middle">
<TD valign="top" width="200" align="left">Show Program Tabs Always</TD>
<TD valign="top" align="left">If selected, a program tab will be displayed even
there is only one program open.</TD>
</TR>
<TR valign="middle">
<TD valign="top" width="200" align="left">Subroutine Model</TD>
<TD valign="top" align="left">
<P>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 <A href=
"help/topics/BlockModel/Block_Model.htm#BlockModelDefinition">Block Models</A>
for a description of the valid Models.</P>
@ -370,16 +390,6 @@
<TH valign="top" align="left"><B>Description</B></TH>
</TR>
<TR>
<TD valign="top" width="200" align="left">Swing Look and Feel</TD>
<TD valign="top" align="left"><A name="Look_And_Feel"></A> 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.</TD>
</TR>
<TR>
<TD valign="top" width="200" align="left">Automatically Save Tools</TD>
@ -387,6 +397,24 @@
when the tool is closed.</TD>
</TR>
<TR>
<TD valign="top" width="200" align="left">Default Tool Launch Mode</TD>
<TD valign="top" align="left">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.</TD>
</TR>
<TR valign="middle">
<TD valign="top" width="200" align="left">Docking Windows On Top</TD>
<TD valign="top" align="left">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.</TD>
</TR>
<TR>
<TD valign="top" width="200" align="left">Restore Previous Project</TD>
@ -394,37 +422,26 @@
opens the previously loaded project on startup.</TD>
</TR>
<TR>
<TD valign="top" width="200" align="left">Default Tool Launch Mode</TD>
<TD valign="top" align="left">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.</TD>
<TR>
<TD valign="top" width="200" align="left">Show Tooltips</TD>
<TD valign="top" align="left">This controls whether or not Ghidra will show
tooltips.</TD>
</TR>
<TR>
<TD valign="top" width="200" align="left"><A name="Use_Inverted_Colors"></A>Use
Inverted Colors</TD>
<TD valign="top" align="left">
<P>This is a <B>prototype</B> 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.</P>
<BLOCKQUOTE style="background-color: #FFF0E0; color: black;">
<P><IMG alt="" border="0" src="help/shared/warning.png"> As a prototype
feature, this feature has many known issues, including:</P>
<UL>
<LI>Pre-generated content, such as images, icons and help files will have
inverted colors,</LI>
<LI>Some color combinations will be difficult to read</LI>
</UL>
</BLOCKQUOTE>
</TD>
<TR>
<TD valign="top" width="200" align="left">Use DataBuffer Output Compression</TD>
<TD valign="top" align="left">This controls whether or not Ghidra will compress
data being sent to the server.</TD>
</TR>
<TR>
<TD valign="top" width="200" align="left">Use Notification Animation</TD>
<TD valign="top" align="left">This controls whether or not Ghidra will use
automations to provide visual feedback that something is happening, such as
launching a tool.</TD>
</TR>
</TBODY>
</TABLE>
</CENTER>

View file

@ -100,4 +100,8 @@ public class ListingPanelContainer extends JPanel {
add(northComponent, BorderLayout.NORTH);
}
}
public JComponent getNorthPanel() {
return northComponent;
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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<T> extends JPanel {
private Function<T, String> nameFunction = v -> v.toString();
private Function<T, Icon> iconFunction = Dummy.function();
private Function<T, String> toolTipFunction = Dummy.function();
private Predicate<T> removeTabPredicate = Dummy.predicate();
private Consumer<T> selectedTabConsumer = Dummy.consumer();
private Consumer<T> removedTabConsumer = Dummy.consumer();
private Consumer<T> closeTabConsumer = t -> removeTab(t);
private boolean showTabsAlways = true;
/**
* Constructor
@ -93,12 +95,15 @@ public class GTabPanel<T> 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<T> 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<T> extends JPanel {
public void removeTabs(Collection<T> 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<T> 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<T> 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<T> 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<T> 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<T> removedTabConsumer) {
this.removedTabConsumer = removedTabConsumer;
public void setCloseTabConsumer(Consumer<T> closeTabConsumer) {
this.closeTabConsumer = closeTabConsumer;
}
/**
@ -379,6 +374,33 @@ public class GTabPanel<T> 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<T> 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<T> 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<T> selectedTab = new GTab<T>(this, selectedValue, true);
int availableWidth = getPanelWidth() - getTabWidth(selectedTab);
GTab<T> selectedTab = null;
int availableWidth = getPanelWidth();
if (selectedValue != null) {
selectedTab = new GTab<T>(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<T> 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) {

View file

@ -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<String> selectedValue = new AtomicReference<String>();
Consumer<String> 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<String> removedValue = new AtomicReference<String>();
Consumer<String> c = s -> removedValue.set(s);
runSwing(() -> gTabPanel.setRemovedTabConsumer(c));
AtomicReference<String> closedValue = new AtomicReference<String>();
Consumer<String> 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<String> removePredicateCallValue = new AtomicReference<String>();
Predicate<String> p = s -> {
removePredicateCallValue.set(s);
return true;
public void testSetRemoveTabConsumer() {
AtomicReference<String> closedValueReference = new AtomicReference<String>();
Consumer<String> 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<String> removePredicateCallValue = new AtomicReference<String>();
Predicate<String> 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<String> 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));
}