diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/AbstractCodeBrowserPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/AbstractCodeBrowserPlugin.java
index 9051c072c4..b6f591f88b 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/AbstractCodeBrowserPlugin.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/codebrowser/AbstractCodeBrowserPlugin.java
@@ -350,7 +350,11 @@ public abstract class AbstractCodeBrowserPlugin
ex
@Override
public void setNorthComponent(JComponent comp) {
connectedProvider.setNorthComponent(comp);
+ }
+ @Override
+ public void requestFocus() {
+ connectedProvider.requestFocus();
}
@Override
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/MultiTabPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/MultiTabPanel.java
deleted file mode 100644
index 3298d28b60..0000000000
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/MultiTabPanel.java
+++ /dev/null
@@ -1,1088 +0,0 @@
-/* ###
- * 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.*;
-import java.awt.event.*;
-import java.util.*;
-import java.util.List;
-import java.util.Map.Entry;
-
-import javax.swing.*;
-import javax.swing.border.*;
-
-import docking.actions.KeyBindingUtils;
-import docking.widgets.label.GDLabel;
-import docking.widgets.label.GIconLabel;
-import generic.theme.*;
-import generic.util.WindowUtilities;
-import ghidra.framework.model.ProjectLocator;
-import ghidra.program.model.listing.Program;
-import ghidra.util.layout.HorizontalLayout;
-import resources.Icons;
-
-/**
- * Panel to show a "tab" for an object. ChangeListeners are notified when a tab is selected.
- */
-public class MultiTabPanel extends JPanel {
- private static final String FONT_TABS_ID = "font.plugin.tabs";
- private static final String FONT_TABS_LIST_ID = "font.plugin.tabs.list";
- //@formatter:off
- private final static Color SELECTED_TAB_COLOR = new GColor("color.bg.listing.tabs.selected");
- private final static Color HIGHLIGHTED_TAB_BG_COLOR = new GColor("color.bg.listing.tabs.highlighted");
- private final static Icon EMPTY16_ICON = Icons.EMPTY_ICON;
- private final static Icon EMPTY8_ICON = new GIcon("icon.plugin.programmanager.empty.small");
- private final static Icon CLOSE_ICON = new GIcon("icon.plugin.programmanager.close");
- private final static Icon HIGHLIGHT_CLOSE_ICON = new GIcon("icon.plugin.programmanager.close.highlight");
- private final static Icon LIST_ICON = new GIcon("icon.plugin.programmanager.list");
- private final static Icon TRANSIENT_ICON = new GIcon("icon.plugin.programmanager.transient");
-
- private final static Color TEXT_SELECTION_COLOR = new GColor("color.fg.listing.tabs.text.selected");
- private final static Color TEXT_NON_SELECTION_COLOR = new GColor("color.fg.listing.tabs.text.unselected");
- private final static Color BG_SELECTION_COLOR = SELECTED_TAB_COLOR;
- private final static Color BG_NON_SELECTION_COLOR = new GColor("color.bg.listing.tabs.unselected");
- private static final Color BG_COLOR_MORE_TABS_HOVER = new GColor("color.bg.listing.tabs.more.tabs.hover");
- //@formatter:on
-
- private static final String DEFAULT_HIDDEN_COUNT_STR = "99";
-
- /** A list of tabs that are hidden from view due to space constraints */
- private List hiddenTabList;
-
- /** A visible tab list */
- private List visibleTabList;
-
- /** A linked map to maintain insertion order mapping of programs and their associated tabs */
- private Map linkedProgramMap;
-
- private Program currentProgram;
- private Program highlightedProgram;
- private MultiTabPlugin multiTabPlugin;
- private ProgramListPanel programListPanel;
- private Border defaultListLabelBorder;
- private JLabel showHiddenListLabel;
- private JDialog listWindow;
- private JTextField filterField;
-
- // for testing
- private boolean ignoreFocus;
-
- MultiTabPanel(MultiTabPlugin multiTabPlugin) {
- this.multiTabPlugin = multiTabPlugin;
- setLayout(new HorizontalLayout(0));
-
- // we use a linked map to maintain insertion order
- linkedProgramMap = new LinkedHashMap<>();
- hiddenTabList = new ArrayList<>();
- visibleTabList = new ArrayList<>();
-
- // Create a border that is designed to draw a rectangle along the bottom of the
- // panel that will accent the selected tab. This line is intended to appear as
- // it is part of the selected tab.
- Border outerBorder = new MatteBorder(0, 0, 3, 0, SELECTED_TAB_COLOR);
- Border innerBorder = new BottomOnlyBevelBorder();
- setBorder(BorderFactory.createCompoundBorder(outerBorder, innerBorder));
-
- showHiddenListLabel = createLabel();
-
- addComponentListener(new ComponentAdapter() {
- @Override
- public void componentResized(ComponentEvent e) {
- hideListWindow();
- packTabs(currentProgram);
- }
- });
- setMinimumSize(new Dimension(30, 20));
- }
-
- void addProgram(Program program) {
- if (linkedProgramMap.containsKey(program)) {
- return;
- }
-
- JPanel panel = createProgramTab(program, false);
- add(panel);
- packTabs(currentProgram);
- }
-
- /**
- * Remove all tabs in the panel.
- */
- @Override
- public void removeAll() {
- currentProgram = null;
- ArrayList list = new ArrayList<>(linkedProgramMap.keySet());
-
- for (Program element : list) {
- doRemoveProgram(element);
- }
- linkedProgramMap.clear();
- visibleTabList.clear();
- hiddenTabList.clear();
- hideListWindow();
- }
-
- Program getSelectedProgram() {
- return currentProgram;
- }
-
- /**
- * Refresh label displayed in the tab for the given object.
- * @param program object associated with a tab
- */
- void refresh(Program program) {
- TabPanel panel = linkedProgramMap.get(program);
- if (panel == null) {
- return;
- }
-
- panel.refresh();
- packTabs(currentProgram);
- }
-
- /**
- * Set the selected tab that corresponds to the given program.
- * @param program The program to select.
- */
- void setSelectedProgram(Program program) {
- if (currentProgram == program || !linkedProgramMap.containsKey(program)) {
- return;
- }
-
- // create the selected tab so that it will be used in setTabVisible()
- TabPanel panel = linkedProgramMap.get(program);
- panel = createProgramTab(program, true);
- linkedProgramMap.put(program, panel);
-
- clearSelectedProgram();
- currentProgram = program;
- setTabVisible(program);
-
- repaint();
- multiTabPlugin.programSelected(currentProgram);
- }
-
- int getTabCount() {
- return linkedProgramMap.size();
- }
-
- boolean containsProgram(Program program) {
- return linkedProgramMap.get(program) != null;
- }
-
- ////////////////////////////////////////////////////////////
- // For JUnit tests
-
- int getVisibleTabCount() {
- return visibleTabList.size();
- }
-
- int getHiddenCount() {
- return hiddenTabList.size();
- }
-
- JPanel getTab(Object obj) {
- return linkedProgramMap.get(obj);
- }
-
- boolean isHidden(Object obj) {
- JPanel panel = linkedProgramMap.get(obj);
- if (panel != null) {
- return hiddenTabList.contains(panel);
- }
- return false;
- }
-
- private TabPanel createProgramTab(final Program program, boolean isSelected) {
- final JPanel labelPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 1));
- labelPanel.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 10));
-
- JLabel nameLabel = new GDLabel();
- nameLabel.setIconTextGap(1);
- nameLabel.setName("objectName"); // junit access
- Gui.registerFont(nameLabel, FONT_TABS_ID);
- Color foregroundColor = isSelected ? TEXT_SELECTION_COLOR : TEXT_NON_SELECTION_COLOR;
- nameLabel.setForeground(foregroundColor);
-
- labelPanel.add(nameLabel);
-
- JLabel iconLabel = new GIconLabel(isSelected ? CLOSE_ICON : EMPTY16_ICON);
-
- iconLabel.setToolTipText("Close");
- iconLabel.setName("Close"); // junit access
- iconLabel.setOpaque(true);
-
- MouseListener iconSwitcherMouseListener = new MouseAdapter() {
- @Override
- public void mouseEntered(MouseEvent e) {
- if (e.getSource() == iconLabel) {
- iconLabel.setIcon(HIGHLIGHT_CLOSE_ICON);
- }
- else {
- iconLabel.setIcon(CLOSE_ICON);
- }
- }
-
- @Override
- public void mouseExited(MouseEvent e) {
- if (program == currentProgram) {
- iconLabel.setIcon(CLOSE_ICON);
- }
- else {
- iconLabel.setIcon(EMPTY16_ICON);
- }
- }
- };
-
- Color backgroundColor = isSelected ? BG_SELECTION_COLOR : BG_NON_SELECTION_COLOR;
- labelPanel.setBackground(backgroundColor);
- iconLabel.setBackground(backgroundColor);
-
- TabPanel tabPanel = null;
- if (isSelected) {
- tabPanel =
- new SelectedPanel(backgroundColor, program, nameLabel, labelPanel, iconLabel);
- }
- else {
- tabPanel = new TabPanel(backgroundColor, program, nameLabel, labelPanel, iconLabel);
- }
- tabPanel.refresh();
-
- GridBagLayout gbl = new GridBagLayout();
- tabPanel.setLayout(gbl);
-
- GridBagConstraints gbc = new GridBagConstraints();
- gbc.gridx = 0;
- gbc.gridy = 0;
- gbc.anchor = GridBagConstraints.WEST;
- gbc.weightx = 1.0;
- gbl.setConstraints(labelPanel, gbc);
- tabPanel.add(labelPanel);
-
- gbc = new GridBagConstraints();
- gbc.gridx = 1;
- gbc.gridy = 0;
- gbc.anchor = GridBagConstraints.NORTHEAST;
- gbl.setConstraints(iconLabel, gbc);
- tabPanel.add(iconLabel);
- tabPanel.setBorder(new BottomlessBevelBorder());
-
- // this listener gets added to every component in the tab panel we are creating
- MouseListener tabSelectionMouseListener = new MouseAdapter() {
-
- // intentionally using mousePressed() and not mouseClicked() (see tracker 3415)
- @Override
- public void mousePressed(MouseEvent e) {
- // close the list window if the user has clicked outside of the window
- if (!(e.getSource() instanceof JList)) {
- hideListWindow();
- }
-
- if (e.isPopupTrigger()) {
- return; // allow popup triggers to show actions without changing tabs
- }
-
- // Tracker SCR 3605 - hitting 'X' to close tab doesn't work if tab is not selected
- if (e.getSource() == iconLabel) {
- doRemoveProgram(program);
- return;
- }
-
- if (!linkedProgramMap.containsKey(program) || currentProgram == program) {
- return; // object was removed or selection is the same
- }
- clearSelectedProgram();
- setSelectedProgram(program);
- hideListWindow();
- multiTabPlugin.programSelected(currentProgram);
- }
- };
-
- addMouseListener(tabPanel, tabSelectionMouseListener);
- addMouseListener(tabPanel, iconSwitcherMouseListener);
-
- linkedProgramMap.put(program, tabPanel);
-
- tabPanel.setMinimumSize(new Dimension(20, 20));
- return tabPanel;
- }
-
- private void clearSelectedProgram() {
- if (currentProgram == null) {
- return;
- }
-
- // resets the selected tab to be a 'normal' unselected tab
- TabPanel panel = linkedProgramMap.get(currentProgram);
- panel = createProgramTab(currentProgram, false);
- linkedProgramMap.put(currentProgram, panel);
- }
-
- private void doRemoveProgram(Program program) {
- JPanel panel = linkedProgramMap.get(program);
- if (panel == null) {
- return;
- }
-
- if (!multiTabPlugin.removeProgram(program)) {
- return;
- }
-
- removeProgram(program);
- }
-
- /**
- * Remove the tab for the specified object.
- * @param program object associated with a tab to remove
- */
- void removeProgram(Program program) {
- highlightedProgram = null;
- JPanel panel = linkedProgramMap.get(program);
- if (panel == null) {
- return;
- }
-
- remove(panel);
- linkedProgramMap.remove(program);
- visibleTabList.remove(panel);
- hiddenTabList.remove(panel);
- if (program == currentProgram) {
- currentProgram = null;
- }
- packTabs(currentProgram);
-
- revalidate();
- repaint();
-
- hideListWindow();
- }
-
- void showProgramList() {
- // simply show the program list pop-up and let the user pick the program they want
- showListFromKeyboardAction(currentProgram);
- }
-
- void highlightNextProgram(boolean forwardDirection) {
-
- if (highlightedProgram != null) {
- highlightedProgram = getNextProgram(highlightedProgram, forwardDirection);
- }
- else if (listWindowIsShowing()) {
- highlightedProgram = getFirstProgramForDirection(forwardDirection);
- hideListWindow();
- }
- else {
- highlightedProgram = getNextProgram(currentProgram, forwardDirection);
- }
-
- // this is assumed to mean that the next program is in the hidden list
- if (highlightedProgram == null) {
- showList(showHiddenListLabel);
- }
-
- setHighlightedTab(highlightedProgram); // if this is null, then it will clear the display
- repaint(); // update to paint the highlighted tab
- }
-
- void selectHighlightedProgram() {
- setHighlightedTab(null); // if this is null, then it will clear the display
- if (highlightedProgram == null) {
- return;
- }
-
- setSelectedProgram(highlightedProgram);
- repaint();
- }
-
- private Program getFirstProgramForDirection(boolean forwardDirection) {
- int index = 0; // first item in forward direction
- if (!forwardDirection) {
- index = visibleTabList.size() - 1;
- }
-
- TabPanel panel = visibleTabList.get(index);
- return getProgramForPanel(panel);
- }
-
- private Program getNextProgram(Program startProgram, boolean forwardDirection) {
- TabPanel tabPanel = linkedProgramMap.get(startProgram);
- int index = getNextProgramIndex(visibleTabList.indexOf(tabPanel), forwardDirection);
-
- // next tab is visible, return it
- if (index >= 0) {
- TabPanel panel = visibleTabList.get(index);
- return getProgramForPanel(panel);
- }
- return null; // tab not visible
- }
-
- private int getNextProgramIndex(int visibleListIndex, boolean forwardDirection) {
- boolean hasHiddenPrograms = hiddenTabList.size() != 0;
-
- if (forwardDirection) {
- visibleListIndex++;
- if (visibleListIndex == visibleTabList.size()) { // reached the end, what to do next?
- return hasHiddenPrograms ? -1 : 0; // if no hidden tabs, then jump to the front
- }
- return visibleListIndex;
- }
-
- // going backwards
- visibleListIndex--;
- if (visibleListIndex < 0) { // was the first in list, what to do next?
- return hasHiddenPrograms ? -1 : visibleTabList.size() - 1; // no hidden tabs, circle around to the back
- }
-
- return visibleListIndex;
- }
-
- private Program getProgramForPanel(TabPanel panel) {
- Set> entrySet = linkedProgramMap.entrySet();
- for (Entry entry : entrySet) {
- if (entry.getValue() == panel) {
- return entry.getKey();
- }
- }
-
- return null; // shouldn't happen
- }
-
- private void setHighlightedTab(Program highlightedProgram) {
- // reset all colors
- Collection values = linkedProgramMap.values();
- for (TabPanel tabPanel : values) {
- tabPanel.paintHighlightedColor(false);
- }
-
- TabPanel tabPanel = linkedProgramMap.get(highlightedProgram);
- if (tabPanel != null) {
- tabPanel.paintHighlightedColor(true);
- }
- }
-
- private void hideListWindow() {
- if (listWindow != null) {
- listWindow.setVisible(false);
- filterField.setText("");
- }
- }
-
- private void hideListWindowDueToFocusChange() {
- if (!ignoreFocus) {
- hideListWindow();
- }
- }
-
- /*testing*/ void setIgnoreFocus(boolean ignoreFocus) {
- this.ignoreFocus = ignoreFocus;
- }
-
- private void setTabVisible(Program program) {
- JPanel panel = linkedProgramMap.get(program);
- if (visibleTabList.contains(panel)) {
- return;
- }
-
- packTabs(program);
- }
-
- /**
- * Make sure that the tabs fit in the given panel width. If all of the tabs do not fit, then
- * a subset will be used. This method makes sure that the current program is always in the
- * list of visible tabs.
- * @param selectedProgram The currently selected program.
- */
- private void packTabs(Program selectedProgram) {
- // get the number of tabs that will fit into the display and make sure the current proram's
- // tab is in that list
- List newVisibleTabList = getTabsThatFitInView();
- newVisibleTabList = ensureCurrentProgramTabInView(selectedProgram, newVisibleTabList);
-
- // collect all of the hidden tabs
- Collection allTabPanels = linkedProgramMap.values();
- List newHiddenTabList = new ArrayList<>(allTabPanels);
- newHiddenTabList.removeAll(newVisibleTabList);
-
- // update visible tabs within this parent panel
- visibleTabList = newVisibleTabList;
- hiddenTabList = newHiddenTabList;
- highlightedProgram = null;
-
- super.removeAll(); // careful not to call our removeAll() here
-
- setVisible(allTabPanels.size() > 1);
- if (isVisible()) {
- for (JPanel panel : newVisibleTabList) {
- add(panel);
- }
- updateListLabel();
- }
-
- revalidate();
- repaint();
- }
-
- private List getTabsThatFitInView() {
- int availableWidth = getWidth() - showHiddenListLabel.getPreferredSize().width;
-
- // get the number of tabs that will fit into the visible display
- List newVisibleTabList = new ArrayList<>();
- List allTabsList = new ArrayList<>(linkedProgramMap.values());
- int usedWidth = 0;
- for (TabPanel panel : allTabsList) {
- int currentTabWidth = panel.getPreferredSize().width;
- if ((availableWidth > 0) && (usedWidth + currentTabWidth > availableWidth)) {
- break;
- }
- usedWidth += currentTabWidth;
- newVisibleTabList.add(panel);
- }
-
- // check for the boundary condition where all elements would fit in the display if we
- // don't show the label indicating tabs are hidden. The boundary case is when there
- // is only one hidden element that could potentially be put into the view
- if (allTabsList.size() - newVisibleTabList.size() == 1) {
- TabPanel lastPanel = allTabsList.get(allTabsList.size() - 1);
- if (usedWidth + lastPanel.getPreferredSize().width < getWidth()) {
- newVisibleTabList.add(lastPanel);
- }
- }
-
- return newVisibleTabList;
- }
-
- private List ensureCurrentProgramTabInView(Program activeProgram,
- List newVisibleTabList) {
-
- // no current tab to add
- TabPanel currentTabPanel = linkedProgramMap.get(activeProgram);
- if (currentTabPanel == null) {
- return newVisibleTabList;
- }
-
- // make sure the current tab is in the visible list
- if (newVisibleTabList.contains(currentTabPanel)) {
- return newVisibleTabList;
- }
-
- // make sure that the current space can at least fit the current tab
- int availablePanelWidth = getWidth();
- availablePanelWidth -= showHiddenListLabel.getWidth();
-
- int currentTabWidth = currentTabPanel.getPreferredSize().width;
- if (currentTabWidth > availablePanelWidth) {
- // not enough space for the current tab, so don't show any tabs
- newVisibleTabList.clear();
- return newVisibleTabList;
- }
-
- // get the spaced used by the tabs
- int usedWidth = 0;
- for (JPanel panel : newVisibleTabList) {
- usedWidth += panel.getPreferredSize().width;
- }
-
- // remove items from the end of the visible list until we have room for the current tab
- for (int i = newVisibleTabList.size() - 1; i >= 0; i--) {
- TabPanel lastPanel = newVisibleTabList.remove(i);
- int width = lastPanel.getPreferredSize().width;
- usedWidth -= width;
- int newAvailableWidth = (availablePanelWidth - usedWidth);
- if (newAvailableWidth >= currentTabWidth) {
- // we have enough room now
- newVisibleTabList.add(currentTabPanel);
- break;
- }
- }
-
- return newVisibleTabList;
- }
-
- private void updateListLabel() {
- int hiddenCnt = hiddenTabList.size();
- if (hiddenCnt == 0) {
- remove(showHiddenListLabel);
- return;
- }
-
- // Make sure it stays to the right of the tabs
- remove(showHiddenListLabel);
- add(showHiddenListLabel);
- showHiddenListLabel.setText(Integer.toString(hiddenCnt));
- }
-
- private boolean listWindowIsShowing() {
- return listWindow != null && listWindow.isShowing();
- }
-
- private JLabel createLabel() {
- JLabel newLabel = new GDLabel(DEFAULT_HIDDEN_COUNT_STR, LIST_ICON, SwingConstants.LEFT);
- newLabel.setIconTextGap(0);
- Gui.registerFont(newLabel, FONT_TABS_LIST_ID);
- newLabel.setBorder(BorderFactory.createEmptyBorder(4, 4, 0, 4));
- newLabel.setToolTipText("Show Tab List");
- newLabel.setName("showList");
- newLabel.setBackground(BG_COLOR_MORE_TABS_HOVER);
-
- defaultListLabelBorder = newLabel.getBorder();
- final Border hoverBorder = BorderFactory.createBevelBorder(BevelBorder.RAISED);
- newLabel.addMouseListener(new MouseAdapter() {
- @Override
- public void mouseClicked(MouseEvent e) {
- if (listWindowIsShowing()) {
- hideListWindow();
- return;
- }
-
- showList(e.getComponent());
- }
-
- @Override
- public void mouseEntered(MouseEvent e) {
- // show a raised border, like a button (if the window is not already visible)
- if (listWindowIsShowing()) {
- return;
- }
-
- newLabel.setBorder(hoverBorder);
- newLabel.setOpaque(true);
- }
-
- @Override
- public void mouseExited(MouseEvent e) {
- // restore the button-like appearance to normal
- resetListLabelAppearance();
- }
- });
-
- newLabel.setPreferredSize(newLabel.getPreferredSize());
-
- return newLabel;
- }
-
- private void resetListLabelAppearance() {
- showHiddenListLabel.setBorder(defaultListLabelBorder);
- showHiddenListLabel.setOpaque(false);
- }
-
- private void showListFromKeyboardAction(Program startProgram) {
- if (!listWindowIsShowing()) {
- showList(null);
- }
-
- programListPanel.selectProgram(startProgram);
- }
-
- private void showList(Component source) {
- if (listWindow == null) {
- createListWindow();
- }
- else {
- programListPanel.setProgramLists(getProgramList(false), getProgramList(true));
- }
- listWindow.pack();
-
- setListLocationBelowLabel((JLabel) source);
- listWindow.setVisible(true);
-
- resetListLabelAppearance();
-
- programListPanel.requestFocus();
- }
-
- private void setListLocationBelowLabel(JLabel label) {
-
- Rectangle bounds = listWindow.getBounds();
-
- // no label implies we are launched from a keyboard event
- if (label == null) {
-
- Point centerPoint = WindowUtilities.centerOnComponent(getParent(), listWindow);
- bounds.setLocation(centerPoint);
- WindowUtilities.ensureEntirelyOnScreen(getParent(), bounds);
- listWindow.setBounds(bounds);
- return;
- }
-
- // show the window just below the label that launched it
- Point p = label.getLocationOnScreen();
- int x = p.x;
- int y = p.y + label.getHeight() + 3;
- bounds.setLocation(x, y);
-
- // fixes problem where popup gets clipped when going across screens
- WindowUtilities.ensureOnScreen(label, bounds);
- listWindow.setBounds(bounds);
- }
-
- private void createListWindow() {
- Window parent = findParent();
- if (parent instanceof Dialog) {
- listWindow = new JDialog((Dialog) parent);
- }
- else {
- listWindow = new JDialog((Frame) parent);
- }
- listWindow.setUndecorated(true);
-
- listWindow.addWindowFocusListener(new WindowFocusListener() {
- @Override
- public void windowGainedFocus(WindowEvent e) {
- // don't care
- }
-
- @Override
- public void windowLostFocus(WindowEvent e) {
- hideListWindowDueToFocusChange();
- }
- });
-
- KeyListener listener = new KeyAdapter() {
-
- @Override
- public void keyPressed(KeyEvent e) {
- multiTabPlugin.keyTypedFromListWindow(e);
- }
- };
-
- programListPanel =
- new ProgramListPanel(getProgramList(false), getProgramList(true), multiTabPlugin);
- final JList> list = programListPanel.getList();
- filterField = programListPanel.getFilterField();
- list.addMouseListener(new MouseAdapter() {
- @Override
- public void mouseClicked(MouseEvent e) {
- processWindowSelection();
- }
- });
-
- list.addKeyListener(listener);
-
- list.addKeyListener(new KeyAdapter() {
- @Override
- public void keyPressed(KeyEvent e) {
- if (e.getKeyCode() == KeyEvent.VK_ENTER) {
- processWindowSelection();
- }
- else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
- hideListWindow();
- }
- }
- });
-
- list.addFocusListener(new FocusAdapter() {
- @Override
- public void focusLost(FocusEvent focusEvent) {
- // close the window when the user focuses another component
- if (focusEvent.getOppositeComponent() != filterField) {
- hideListWindowDueToFocusChange();
- }
- }
- });
-
- filterField.addKeyListener(listener);
-
- filterField.addKeyListener(new KeyAdapter() {
- @Override
- public void keyPressed(KeyEvent e) {
- if ((e.getKeyCode() == KeyEvent.VK_ENTER) ||
- (e.getKeyCode() == KeyEvent.VK_ESCAPE) || (e.getKeyCode() == KeyEvent.VK_UP) ||
- (e.getKeyCode() == KeyEvent.VK_DOWN)) {
- KeyboardFocusManager focusManager =
- KeyboardFocusManager.getCurrentKeyboardFocusManager();
- focusManager.redispatchEvent(list, e);
- }
- }
- });
-
- listWindow.getContentPane().add(programListPanel);
-
- parent.addWindowListener(new WindowAdapter() {
- @Override
- public void windowDeactivated(WindowEvent e) {
- if (!(e.getOppositeWindow() == listWindow)) {
- hideListWindowDueToFocusChange();
- }
- }
- });
- parent.addComponentListener(new ComponentAdapter() {
- @Override
- public void componentMoved(ComponentEvent e) {
- hideListWindow();
- }
- });
- setActionMap();
- }
-
- private void processWindowSelection() {
- Program selectedProgram = programListPanel.getSelectedProgram();
- if (selectedProgram == null) {
- return; // no list selection
- }
-
- hideListWindow();
- if (selectedProgram == currentProgram) {
- return; // selection is already the active tab
- }
-
- setSelectedProgram(selectedProgram);
- }
-
- private List getProgramList(boolean getVisiblePrograms) {
- List panelList = null;
- if (getVisiblePrograms) {
- panelList = new ArrayList<>(visibleTabList);
- }
- else {
- panelList = new ArrayList<>(hiddenTabList);
- }
-
- List list = new ArrayList<>();
- Set> entrySet = linkedProgramMap.entrySet();
- for (Entry entry : entrySet) {
- JPanel panel = entry.getValue();
- if (panelList.contains(panel)) {
- list.add(entry.getKey());
- }
- }
-
- return list;
- }
-
- private Window findParent() {
- Container parent = getParent();
- while (!(parent instanceof Window) && !(parent instanceof JFrame)) {
- parent = parent.getParent();
- }
- return (Window) parent;
- }
-
- private void addMouseListener(Container c, MouseListener listener) {
-
- c.addMouseListener(listener);
- Component[] children = c.getComponents();
- for (Component element : children) {
- if (element instanceof Container) {
- addMouseListener((Container) element, listener);
- }
- else {
- element.addMouseListener(listener);
- }
- }
- }
-
- private void setActionMap() {
- AbstractAction escAction = new AbstractAction("Exit Window") {
- @Override
- public void actionPerformed(ActionEvent e) {
- hideListWindow();
- }
- };
-
- JComponent rootPane = listWindow.getRootPane();
- KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
- KeyBindingUtils.registerAction(rootPane, ks, escAction, JComponent.WHEN_IN_FOCUSED_WINDOW);
- }
-
- private String getProgramName(Program program) {
- String name = program.toString();
- if (multiTabPlugin != null) {
- name =
- (multiTabPlugin.isChanged(program) ? "*" : " ") + multiTabPlugin.getName(program);
- }
- return name;
- }
-
-//==================================================================================================
-// Inner Classes
-//==================================================================================================
-
- private class TabPanel extends JPanel {
- private Color defaultBgColor;
- private Color defaultFgColor;
- protected final JLabel nameLabel;
- protected final JPanel labelPanel;
- protected final JLabel iconLabel;
- private final Program program;
-
- private TabPanel(Color backgroundColor, Program program, JLabel nameLabel,
- JPanel labelPanel, JLabel iconLabel) {
- this.defaultBgColor = backgroundColor;
- this.defaultFgColor = nameLabel.getForeground();
- this.program = program;
- this.nameLabel = nameLabel;
- this.labelPanel = labelPanel;
- this.iconLabel = iconLabel;
-
- setBackground(backgroundColor);
- }
-
- void refresh() {
- String name = getProgramName(program);
- ProjectLocator projectLocator = program.getDomainFile().getProjectLocator();
- if (projectLocator != null && projectLocator.isTransient()) {
- nameLabel.setIcon(TRANSIENT_ICON);
- }
- else {
- nameLabel.setIcon(EMPTY8_ICON);
- }
- nameLabel.setText(name);
- String toolTip = multiTabPlugin != null ? multiTabPlugin.getToolTip(program) : null;
- nameLabel.setToolTipText(toolTip);
- }
-
- void paintHighlightedColor(boolean paintHighlight) {
- Color newBgColor = defaultBgColor;
- Color newFgColor = defaultFgColor;
- if (paintHighlight) {
- newBgColor = HIGHLIGHTED_TAB_BG_COLOR;
- newFgColor = TEXT_SELECTION_COLOR;
-
- }
-
- setBackground(newBgColor);
- nameLabel.setBackground(newBgColor);
- nameLabel.setForeground(newFgColor);
- labelPanel.setBackground(newBgColor);
- iconLabel.setBackground(newBgColor);
- }
- }
-
- // a panel that paints below it's bounds in order to connect the panel and the border
- // below it visually
- private class SelectedPanel extends TabPanel {
- private SelectedPanel(Color backgroundColor, Program program, JLabel nameLabel,
- JPanel labelPanel, JLabel iconLabel) {
- super(backgroundColor, program, nameLabel, labelPanel, iconLabel);
- setBorder(new BottomlessBevelBorder());
-
- // color our widgets special
- setBackground(BG_SELECTION_COLOR);
- labelPanel.setBackground(BG_SELECTION_COLOR);
- nameLabel.setForeground(TEXT_SELECTION_COLOR);
- iconLabel.setBackground(BG_SELECTION_COLOR);
- iconLabel.setIcon(CLOSE_ICON);
- }
-
- @Override
- protected void paintComponent(Graphics g) {
- Shape saveClip = g.getClip();
- Color oldColor = g.getColor();
- Rectangle bounds = saveClip.getBounds();
- g.setClip(bounds.x, bounds.y, bounds.width, getHeight() + 2);
- g.setColor(getBackground());
- g.fillRect(0, 0, getWidth(), getHeight() + 2);
- g.setColor(oldColor);
- g.setClip(saveClip);
- super.paintComponent(g);
- }
-
- @Override
- void paintHighlightedColor(boolean paintHighlight) {
- super.paintHighlightedColor(paintHighlight);
- Color foreground = TEXT_NON_SELECTION_COLOR;
- if (paintHighlight) {
- foreground = TEXT_SELECTION_COLOR;
- }
-
- // this tab is selected, so change the foreground to be readable
- nameLabel.setForeground(foreground);
- }
- }
-
- // This class doesn't paint the bottom border in order to make the object appear to be
- // connected to the component below. This class also paints its side borders below its
- // bounds for the same reason.
- class BottomlessBevelBorder extends BevelBorder {
- public BottomlessBevelBorder() {
- super(RAISED);
- }
-
- @Override
- // overridden to reduce the space below, since there is no component
- public Insets getBorderInsets(Component c) {
- Insets borderInsets = super.getBorderInsets(c);
- borderInsets.bottom = 0;
- return borderInsets;
- }
-
- @Override
- protected void paintRaisedBevel(Component c, Graphics g, int x, int y, int width,
- int height) {
- Color oldColor = g.getColor();
- int h = height;
- int w = width;
-
- g.translate(x, y);
-
- Shape saveClip = g.getClip();
- Rectangle bounds = saveClip.getBounds();
- g.setClip(bounds.x, bounds.y, bounds.width, getHeight() + 2);
-
- g.setColor(getShadowOuterColor(c));
- g.drawLine(0, 0, 0, h); // left outer
- g.setColor(getHighlightOuterColor(c));
- g.drawLine(1, 0, w - 2, 0); // upper outer
-
- g.setColor(getHighlightInnerColor(c));
- g.drawLine(1, 1, 1, h); // left inner
- g.drawLine(2, 1, w - 3, 1); // upper inner
-
- // bottom outer
- g.setColor(getShadowOuterColor(c));
- g.drawLine(w - 1, 0, w - 1, h); // right outer
-
- // bottom inner
- g.setColor(getShadowInnerColor(c));
- g.drawLine(w - 2, 1, w - 2, h); // right inner
-
- g.setClip(saveClip);
-
- g.translate(-x, -y);
- g.setColor(oldColor);
-
- }
- }
-
- // a bevel border to paint only it's bottom edge, but with the highlight normally found at
- // the top edge
- class BottomOnlyBevelBorder extends BevelBorder {
- public BottomOnlyBevelBorder() {
- super(RAISED);
- }
-
- @Override
- protected void paintRaisedBevel(Component c, Graphics g, int x, int y, int width,
- int height) {
- Color oldColor = g.getColor();
- int h = height;
- int w = width;
-
- g.translate(x, y);
-
- // bottom outer
- g.setColor(getHighlightOuterColor(c));
- g.drawLine(0, h - 1, w - 1, h - 1);
-
- // bottom inner
- g.setColor(getShadowInnerColor(c));
-
- g.translate(-x, -y);
- g.setColor(oldColor);
-
- }
- }
-}
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 62052e6a5e..16f7be18b3 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
@@ -18,13 +18,14 @@ package ghidra.app.plugin.core.progmgr;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
-import javax.swing.KeyStroke;
-import javax.swing.Timer;
+import javax.swing.*;
import docking.ActionContext;
import docking.DockingUtils;
import docking.action.*;
import docking.tool.ToolConstants;
+import docking.widgets.tab.GTabPanel;
+import generic.theme.GIcon;
import ghidra.app.CorePluginPackage;
import ghidra.app.events.*;
import ghidra.app.plugin.PluginCategoryNames;
@@ -53,18 +54,21 @@ import ghidra.util.HelpLocation;
)
//@formatter:on
public class MultiTabPlugin extends Plugin implements DomainObjectListener {
+ 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");
//
- // Unusual Code Alert!: We can't initialize these in the fields above because calling
+ // Unusual Code Alert!: We can't initialize these fields below because calling
// DockingUtils calls into Swing code. Further, we don't want Swing code being accessed
// when the Plugin classes are loaded, as they get loaded in the headless environment.
+ // So these fields are not static.
//
private final KeyStroke NEXT_TAB_KEYSTROKE =
KeyStroke.getKeyStroke(KeyEvent.VK_F9, DockingUtils.CONTROL_KEY_MODIFIER_MASK);
private final KeyStroke PREVIOUS_TAB_KEYSTROKE =
KeyStroke.getKeyStroke(KeyEvent.VK_F8, DockingUtils.CONTROL_KEY_MODIFIER_MASK);
- private MultiTabPanel tabPanel;
+ private GTabPanel tabPanel;
private ProgramManager progService;
private CodeViewerService cvService;
private DockingAction goToProgramAction;
@@ -173,20 +177,20 @@ public class MultiTabPlugin extends Plugin implements DomainObjectListener {
private void switchToProgram(Program program) {
if (lastActiveProgram != null) {
- tabPanel.setSelectedProgram(lastActiveProgram);
+ tabPanel.selectTab(lastActiveProgram);
}
}
private void showProgramList() {
- tabPanel.showProgramList();
+ tabPanel.showTabList(!tabPanel.isShowingTabList());
}
private void highlightNextProgram(boolean forwardDirection) {
- tabPanel.highlightNextProgram(forwardDirection);
+ tabPanel.highlightNextTab(forwardDirection);
}
private void selectHighlightedProgram() {
- tabPanel.selectHighlightedProgram();
+ tabPanel.selectTab(tabPanel.getHighlightedTabValue());
}
String getStringUsedInList(Program program) {
@@ -257,26 +261,52 @@ public class MultiTabPlugin extends Plugin implements DomainObjectListener {
selectHighlightedProgramTimer.restart();
}
- boolean isChanged(Object obj) {
- return ((Program) obj).isChanged();
- }
-
@Override
public void domainObjectChanged(DomainObjectChangedEvent ev) {
if (ev.getSource() instanceof Program) {
Program program = (Program) ev.getSource();
- tabPanel.refresh(program);
+ tabPanel.refreshTab(program);
}
}
@Override
protected void init() {
- tabPanel = new MultiTabPanel(this);
+ tabPanel = new GTabPanel("Program");
+ tabPanel.setNameFunction(p -> getTabName(p));
+ tabPanel.setIconFunction(p -> getIcon(p));
+ tabPanel.setToolTipFunction(p -> getToolTip(p));
+ tabPanel.setSelectedTabConsumer(p -> programSelected(p));
+ tabPanel.setRemoveTabActionPredicate(p -> progService.closeProgram(p, false));
+
progService = tool.getService(ProgramManager.class);
cvService = tool.getService(CodeViewerService.class);
cvService.setNorthComponent(tabPanel);
}
+ private Icon getIcon(Program program) {
+ ProjectLocator projectLocator = program.getDomainFile().getProjectLocator();
+ if (projectLocator != null && projectLocator.isTransient()) {
+ return TRANSIENT_ICON;
+ }
+ return EMPTY8_ICON;
+ }
+
+ private String getTabName(Program program) {
+ DomainFile df = program.getDomainFile();
+ String tabName = df.getName();
+ if (df.isReadOnly()) {
+ int version = df.getVersion();
+ if (!df.canSave() && version != DomainFile.DEFAULT_VERSION) {
+ tabName += "@" + version;
+ }
+ tabName = tabName + " [Read-Only]";
+ }
+ if (program.isChanged()) {
+ tabName = "*" + tabName;
+ }
+ return tabName;
+ }
+
boolean removeProgram(Program program) {
return progService.closeProgram(program, false);
}
@@ -284,13 +314,14 @@ public class MultiTabPlugin extends Plugin implements DomainObjectListener {
void programSelected(Program program) {
if (program != progService.getCurrentProgram()) {
progService.setCurrentProgram(program);
+ cvService.requestFocus();
}
}
private void add(Program prog) {
if (progService.isVisible(prog)) {
- tabPanel.addProgram(prog);
+ tabPanel.addTab(prog);
prog.removeListener(this);
prog.addListener(this);
updateActionEnablement();
@@ -299,7 +330,7 @@ public class MultiTabPlugin extends Plugin implements DomainObjectListener {
private void remove(Program prog) {
prog.removeListener(this);
- tabPanel.removeProgram(prog);
+ tabPanel.removeTab(prog);
updateActionEnablement();
}
@@ -326,8 +357,8 @@ public class MultiTabPlugin extends Plugin implements DomainObjectListener {
if (prog != null) {
add(prog);
- if (tabPanel.getSelectedProgram() != prog) {
- tabPanel.setSelectedProgram(prog);
+ if (tabPanel.getSelectedTabValue() != prog) {
+ tabPanel.selectTab(prog);
updateActionEnablement();
}
}
@@ -338,7 +369,7 @@ public class MultiTabPlugin extends Plugin implements DomainObjectListener {
add(prog);
if (progService.getCurrentProgram() != prog) {
currentProgram = prog;
- tabPanel.setSelectedProgram(prog);
+ tabPanel.selectTab(prog);
updateActionEnablement();
}
}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramListPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramListPanel.java
deleted file mode 100644
index d1ba660ebe..0000000000
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/progmgr/ProgramListPanel.java
+++ /dev/null
@@ -1,269 +0,0 @@
-/* ###
- * 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.*;
-import java.awt.event.MouseEvent;
-import java.awt.event.MouseMotionAdapter;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-
-import javax.swing.*;
-import javax.swing.border.Border;
-import javax.swing.event.DocumentEvent;
-import javax.swing.event.DocumentListener;
-import javax.swing.text.BadLocationException;
-import javax.swing.text.Document;
-
-import docking.widgets.list.GListCellRenderer;
-import generic.theme.GColor;
-import generic.theme.GThemeDefaults.Colors;
-import ghidra.program.model.listing.Program;
-
-/**
- * Panel that displays the overflow of currently open programs that can be chosen.
- *
- * Programs that don't have a visible tab are displayed in bold.
- */
-class ProgramListPanel extends JPanel {
-
- private static final Color BACKGROUND_COLOR = new GColor("color.bg.listing.tabs.list");
- private static final Color FOREGROUND_COLOR = new GColor("color.fg.listing.tabs.list");
-
- private List hiddenList;
- private List shownList;
- private JList programList;
- private MultiTabPlugin multiTabPlugin;
- private DefaultListModel listModel;
- private JTextField filterField;
-
- /**
- * Construct a new ObjectListPanel.
- * @param hiddenList list of Programs that are not showing (tabs are not visible)
- * @param shownList list of Programs that are that are showing
- * @param multiTabPlugin has info about the program represented by a tab
- */
- ProgramListPanel(List hiddenList, List shownList,
- MultiTabPlugin multiTabPlugin) {
- super(new BorderLayout());
- this.hiddenList = hiddenList;
- this.shownList = shownList;
- this.multiTabPlugin = multiTabPlugin;
- create();
- }
-
- /**
- * Set the object lists.
- * @param hiddenList list of Objects that are not showing (tabs are not visible)
- * @param shownList list of Objects that are showing
- */
- void setProgramLists(List hiddenList, List shownList) {
- this.hiddenList = hiddenList;
- this.shownList = shownList;
- initListModel();
- programList.clearSelection();
- }
-
- JList getList() {
- return programList;
- }
-
- JTextField getFilterField() {
- return filterField;
- }
-
- /**
- * Return the selected Object in the JList.
- * @return null if no object is selected
- */
- Program getSelectedProgram() {
- int index = programList.getSelectedIndex();
- if (index >= 0) {
- return listModel.get(index);
- }
- return null;
- }
-
- void selectProgram(Program program) {
- int index = listModel.indexOf(program);
- programList.setSelectedIndex(index);
- }
-
- @Override
- public void requestFocus() {
- filterField.requestFocus();
- filterField.selectAll();
- filterList(filterField.getText());
- }
-
- private void create() {
-
- listModel = new DefaultListModel<>();
- initListModel();
- programList = new JList<>(listModel);
-
- // Some LaFs use different selection colors depending on whether the list has focus. This
- // list does not get focus, so the selection color does not look correct when interacting
- // with the list. Setting the color here updates the list to always use the focused
- // selected color.
- programList.setSelectionBackground(new GColor("system.color.bg.selected.view"));
-
- programList.setBorder(BorderFactory.createEmptyBorder(5, 0, 0, 0));
- programList.addMouseMotionListener(new MouseMotionAdapter() {
- @Override
- public void mouseMoved(MouseEvent e) {
- int index = programList.locationToIndex(e.getPoint());
- if (index >= 0) {
- programList.setSelectedIndex(index);
- }
- }
- });
-
- programList.setCellRenderer(new ProgramListCellRenderer());
- JScrollPane sp = new JScrollPane();
- sp.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
- sp.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
- sp.setBorder(BorderFactory.createEmptyBorder());
-
- JPanel northPanel = new JPanel();
- northPanel.setLayout(new BoxLayout(northPanel, BoxLayout.Y_AXIS));
-
- filterField = createFilterField();
- northPanel.add(filterField);
-
- JSeparator separator = new JSeparator();
- northPanel.add(separator);
- northPanel.setBackground(BACKGROUND_COLOR);
-
- add(northPanel, BorderLayout.NORTH);
- add(programList, BorderLayout.CENTER);
-
- // add some padding around the panel
- Border innerBorder = BorderFactory.createEmptyBorder(5, 5, 5, 5);
- Border outerBorder = BorderFactory.createLineBorder(Colors.BORDER);
- Border compoundBorder = BorderFactory.createCompoundBorder(outerBorder, innerBorder);
- setBorder(compoundBorder);
-
- setBackground(BACKGROUND_COLOR);
- }
-
- private JTextField createFilterField() {
- JTextField newFilterField = new JTextField(20);
- newFilterField.setBackground(BACKGROUND_COLOR);
- newFilterField.setForeground(FOREGROUND_COLOR);
- newFilterField.setBorder(BorderFactory.createEmptyBorder(0, 0, 5, 0));
-
- newFilterField.getDocument().addDocumentListener(new DocumentListener() {
- @Override
- public void changedUpdate(DocumentEvent e) {
- filter(e.getDocument());
- }
-
- @Override
- public void insertUpdate(DocumentEvent e) {
- filter(e.getDocument());
- }
-
- @Override
- public void removeUpdate(DocumentEvent e) {
- filter(e.getDocument());
- }
-
- private void filter(Document document) {
- try {
- String text = document.getText(0, document.getLength());
- filterList(text);
- }
- catch (BadLocationException e) {
- // shouldn't happen; don't care
- }
- }
- });
-
- return newFilterField;
- }
-
- private void filterList(String filterText) {
- List allDataList = new ArrayList<>();
- allDataList.addAll(hiddenList);
- allDataList.addAll(shownList);
-
- boolean hasFilter = filterText.trim().length() != 0;
- if (hasFilter) {
- String lowerCaseFilterText = filterText.toLowerCase();
- for (Iterator iterator = allDataList.iterator(); iterator.hasNext();) {
- Program program = iterator.next();
- String programString = multiTabPlugin.getStringUsedInList(program).toLowerCase();
- if (programString.indexOf(lowerCaseFilterText) < 0) {
- iterator.remove();
- }
- }
- }
-
- listModel.clear();
- for (Program program : allDataList) {
- listModel.addElement(program);
- }
-
- // select something in the list so that the user can make a selection from the keyboard
- if (listModel.getSize() > 0) {
- int selectedIndex = programList.getSelectedIndex();
- if (selectedIndex < 0) {
- programList.setSelectedIndex(0);
- }
- }
- }
-
- private void initListModel() {
- listModel.clear();
- for (Program element : hiddenList) {
- listModel.addElement(element);
- }
- for (Program element : shownList) {
- listModel.addElement(element);
- }
- }
-
- private class ProgramListCellRenderer extends GListCellRenderer {
-
- @Override
- protected String getItemText(Program program) {
- return multiTabPlugin.getStringUsedInList(program);
- }
-
- @Override
- public Component getListCellRendererComponent(JList extends Program> list, Program value,
- int index, boolean isSelected, boolean hasFocus) {
- super.getListCellRendererComponent(list, value, index, isSelected, hasFocus);
-
- if (hiddenList.contains(value)) {
- setBold();
- }
- if (isSelected) {
- setBackground(list.getSelectionBackground());
- setForeground(list.getSelectionForeground());
- }
- else {
- setBackground(list.getBackground());
- setForeground(list.getForeground());
- }
-
- return this;
- }
-
- }
-}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/services/CodeViewerService.java b/Ghidra/Features/Base/src/main/java/ghidra/app/services/CodeViewerService.java
index 091b3f3e1b..bc36f05086 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/services/CodeViewerService.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/services/CodeViewerService.java
@@ -225,4 +225,9 @@ public interface CodeViewerService {
* @param listener the listener to be notified;
*/
public void removeListingDisplayListener(AddressSetDisplayListener listener);
+
+ /**
+ * Request that the main connected Listing view gets focus
+ */
+ public void requestFocus();
}
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 313e509245..083ee6f059 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
@@ -20,16 +20,18 @@ import static org.junit.Assert.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.math.BigInteger;
-import java.util.*;
+import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicReference;
import javax.swing.*;
-import javax.swing.Timer;
import org.junit.*;
import docking.action.DockingAction;
import docking.widgets.fieldpanel.FieldPanel;
+import docking.widgets.searchlist.SearchList;
+import docking.widgets.searchlist.SearchListModel;
+import docking.widgets.tab.*;
import generic.test.TestUtils;
import generic.theme.GThemeDefaults.Colors.Palette;
import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin;
@@ -57,7 +59,7 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest {
private String[] programNames = { "notepad", "login", "tms" };
private Program[] programs;
private ProgramManager pm;
- private MultiTabPanel panel;
+ private GTabPanel panel;
private MarkerService markerService;
@Before
@@ -97,7 +99,7 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest {
openPrograms(programNames);
assertNotNull(panel);
assertEquals(programNames.length, panel.getTabCount());
- assertEquals(programs[programs.length - 1], panel.getSelectedProgram());
+ assertEquals(programs[programs.length - 1], panel.getSelectedTabValue());
}
@Test
@@ -105,7 +107,7 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest {
openPrograms(programNames);
assertEquals(programNames.length, panel.getTabCount());
- panel.addProgram(programs[0]);
+ panel.addTab(programs[0]);
assertEquals(programNames.length, panel.getTabCount());
}
@@ -117,13 +119,13 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest {
JPanel tab = panel.getTab(programs[1]);
Point p = tab.getLocationOnScreen();
clickMouse(tab, MouseEvent.BUTTON1, p.x + 1, p.y + 1, 1, 0);
- assertEquals(programs[1], panel.getSelectedProgram());
+ assertEquals(programs[1], panel.getSelectedTabValue());
// select first tab
tab = panel.getTab(programs[0]);
p = tab.getLocationOnScreen();
clickMouse(tab, MouseEvent.BUTTON1, p.x + 1, p.y + 1, 1, 0);
- assertEquals(programs[0], panel.getSelectedProgram());
+ assertEquals(programs[0], panel.getSelectedTabValue());
}
@Test
@@ -152,10 +154,10 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest {
programNames = new String[] { "notepad", "login", "tms", "taskman", "TestGhidraSearches" };
openPrograms(programNames);
- assertEquals(3, panel.getHiddenCount());
+ assertEquals(3, panel.getHiddenTabs().size());
- runSwing(() -> panel.removeProgram(programs[3]));
- assertEquals(2, panel.getHiddenCount());
+ runSwing(() -> panel.removeTab(programs[3]));
+ assertEquals(2, panel.getHiddenTabs().size());
}
@Test
@@ -190,29 +192,29 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest {
@Test
public void testShowList() throws Exception {
- setFrameSize(600, 500);
+ setFrameSize(650, 500);
programNames = new String[] { "notepad", "login", "tms", "taskman", "TestGhidraSearches" };
openPrograms(programNames);
assertEquals(programNames.length, panel.getTabCount());
- assertEquals(3, panel.getVisibleTabCount());
- assertEquals(2, panel.getHiddenCount());
+ assertEquals(3, panel.getVisibleTabs().size());
+ assertEquals(2, panel.getHiddenTabs().size());
- ProgramListPanel listPanel = showList();
+ TabListPopup> tabListPopup = showList();
- JList> list = findComponent(listPanel, JList.class);
+ @SuppressWarnings("unchecked")
+ SearchList list = findComponent(tabListPopup, SearchList.class);
assertNotNull(list);
-
- ListModel> model = list.getModel();
+ SearchListModel model = list.getModel();
Program[] hiddenPrograms = new Program[] { programs[2], programs[3] };// 4 tabs fit before 5th program was open
for (int i = 0; i < hiddenPrograms.length; i++) {
- assertEquals(hiddenPrograms[i], model.getElementAt(i));
+ assertEquals(hiddenPrograms[i], model.getElementAt(i).value());
}
Program[] shownPrograms = new Program[] { programs[0], programs[1], programs[4] };
for (int i = 0; i < shownPrograms.length; i++) {
- assertEquals(shownPrograms[i], model.getElementAt(i + 2));
+ assertEquals(shownPrograms[i], model.getElementAt(i + 2).value());
}
}
@@ -223,18 +225,13 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest {
programNames = new String[] { "notepad", "login", "tms", "taskman", "TestGhidraSearches" };
openPrograms(programNames);
- ProgramListPanel listPanel = showList();
+ TabListPopup> tabListPopup = showList();
- JList> list = findComponent(listPanel, JList.class);
-
- // the first item is expected to be 'login', since the current program is
- // 'TestGhidraSearches' and that only fits with 'notepad', the rest our put into the
- // list in order.
- list.setSelectedIndex(0);
- waitForSwing();
-
- triggerText(listPanel.getFilterField(), "\n");
- assertEquals(programs[1], panel.getSelectedProgram());
+ @SuppressWarnings("unchecked")
+ SearchList list = findComponent(tabListPopup, SearchList.class);
+ list.setSelectedItem(programs[1]);
+ triggerText(list.getFilterField(), "\n");
+ assertEquals(programs[1], panel.getSelectedTabValue());
}
@Test
@@ -244,12 +241,12 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest {
programNames = new String[] { "notepad", "login", "tms", "taskman", "TestGhidraSearches" };
openPrograms(programNames);
- ProgramListPanel listPanel = showList();
- Window window = windowForComponent(listPanel);
+ TabListPopup> tabListPopup = showList();
+ Window window = windowForComponent(tabListPopup);
assertTrue(window.isShowing());
// remove notepad
- runSwing(() -> panel.removeProgram(programs[0]));
+ runSwing(() -> panel.removeTab(programs[0]));
assertTrue(!window.isShowing());
}
@@ -259,23 +256,21 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest {
setFrameSize(500, 500);
programNames = new String[] { "notepad", "login", "tms", "taskman", "TestGhidraSearches" };
openPrograms(programNames);
- JLabel listLabel = (JLabel) findComponentByName(panel, "showList");
- assertNotNull(listLabel);
+
+ HiddenValuesButton control = findComponent(tool.getToolFrame(), HiddenValuesButton.class);
+ assertNotNull(control);
+
assertEquals(programNames.length, panel.getTabCount());
- assertEquals(2, panel.getVisibleTabCount());
- assertEquals(3, panel.getHiddenCount());
+ assertEquals(2, panel.getVisibleTabs().size());
+ assertEquals(3, panel.getHiddenTabs().size());
setFrameSize(925, 500);
- listLabel = (JLabel) findComponentByName(panel, "showList");
+ control = findComponent(tool.getToolFrame(), HiddenValuesButton.class);
- if (listLabel != null) {
- printResizeDebug();
- }
-
- assertNull(listLabel);
- assertEquals(5, panel.getVisibleTabCount());
- assertEquals(0, panel.getHiddenCount());
+ assertNull(control);
+ assertEquals(5, panel.getVisibleTabs().size());
+ assertEquals(0, panel.getHiddenTabs().size());
}
@Test
@@ -286,7 +281,7 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest {
p.setTemporary(false); // we need to be notified of changes
// select notepad
- panel.setSelectedProgram(p);
+ panel.selectTab(p);
int transactionID = p.startTransaction("test");
try {
SymbolTable symTable = p.getSymbolTable();
@@ -296,10 +291,10 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest {
p.endTransaction(transactionID, true);
}
p.flushEvents();
- runSwing(() -> panel.refresh(p));
+ runSwing(() -> panel.refreshTab(p));
JPanel tab = panel.getTab(p);
- JLabel label = (JLabel) findComponentByName(tab, "objectName");
+ JLabel label = (JLabel) findComponentByName(tab, "Tab Label");
assertTrue(label.getText().startsWith("*"));
}
@@ -310,8 +305,8 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest {
openPrograms(programNames);
assertHidden(programs[1]);
- runSwing(() -> panel.setSelectedProgram(programs[1]));
- assertEquals(programs[1], panel.getSelectedProgram());
+ runSwing(() -> panel.selectTab(programs[1]));
+ assertEquals(programs[1], panel.getSelectedTabValue());
assertShowing(programs[1]);
}
@@ -320,7 +315,7 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest {
openPrograms_HideLastOpened();
- Program startProgram = panel.getSelectedProgram();
+ Program startProgram = panel.getSelectedTabValue();
MultiTabPlugin plugin = env.getPlugin(MultiTabPlugin.class);
DockingAction action =
@@ -331,13 +326,13 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest {
JPanel tab = panel.getTab(programs[1]);
Point p = tab.getLocationOnScreen();
clickMouse(tab, MouseEvent.BUTTON1, p.x + 1, p.y + 1, 1, 0);
- assertEquals(programs[1], panel.getSelectedProgram());
- assertTrue(!startProgram.equals(panel.getSelectedProgram()));
+ assertEquals(programs[1], panel.getSelectedTabValue());
+ assertTrue(!startProgram.equals(panel.getSelectedTabValue()));
assertTrue(action.isEnabled());
performAction(action, true);
- assertEquals(startProgram, panel.getSelectedProgram());
+ assertEquals(startProgram, panel.getSelectedTabValue());
}
@Test
@@ -369,18 +364,19 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest {
assertEquals(BigInteger.valueOf(4), fp.getCursorLocation().getIndex());
}
+ @SuppressWarnings("unchecked")
@Test
public void testTabUpdate() throws Exception {
Program p = openDummyProgram("login", true);
// select second tab (the "login" program)
- panel = findComponent(tool.getToolFrame(), MultiTabPanel.class);
+ panel = findComponent(tool.getToolFrame(), GTabPanel.class);
// don't let focus issues hide the popup list
panel.setIgnoreFocus(true);
- panel.setSelectedProgram(p);
- assertEquals(p, panel.getSelectedProgram());
+ panel.selectTab(p);
+ assertEquals(p, panel.getSelectedTabValue());
addComment(p);
@@ -395,7 +391,7 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest {
// Check the name on the tab and in the tooltip.
JPanel tabPanel = getTabPanel(p);
- JLabel label = (JLabel) findComponentByName(tabPanel, "objectName");
+ JLabel label = (JLabel) findComponentByName(tabPanel, "Tab Label");
assertEquals("*" + newName + " [Read-Only]", label.getText());
assertTrue(label.getToolTipText().endsWith("/" + newName + " [Read-Only]*"));
}
@@ -429,7 +425,7 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest {
// by trial-and-error, we know that 'tms' is the last visible program tab
// after resizing
- setFrameSize(500, 500);
+ setFrameSize(550, 500);
assertShowing(programs[2]);
assertHidden(programs[3]);
@@ -445,7 +441,8 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest {
// select the first visible tab and go backwards to trigger the list
selectTab(programs[0]);
performPreviousAction();
- assertListWindowShowing();
+ listWindow = getListWindow();
+ assertTrue(listWindow.isShowing());
}
@Test
@@ -465,20 +462,21 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest {
// by trial-and-error, we know that 'tms' is the last visible program tab
// after resizing
- setFrameSize(500, 500);
+ setFrameSize(600, 500);
assertShowing(programs[2]);
assertHidden(programs[3]);
// select 'tms', which is the last tab before the list is shown
selectTab(programs[2]);
performNextAction();
- assertListWindowShowing();
+ Window window = getListWindow();
+ assertTrue(window.isShowing());
// the newly selected program should the first program, as the selection
// should have left the window and wrapped around 'notepad'
performNextAction();
assertProgramSelected(programs[0]);
- assertListWindowHidden();
+ assertFalse(window.isShowing());
//
// Now try the other direction, which should wrap back around the other direction,
@@ -486,11 +484,12 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest {
//
selectTab(programs[0]);// start off at the first tab
performPreviousAction();
- assertListWindowShowing();
+ Window listWindow = getListWindow();
+ assertTrue(listWindow.isShowing());
performPreviousAction();
assertProgramSelected(programs[2]);// 'tms'--last visible program
- assertListWindowHidden();
+ assertFalse(listWindow.isShowing());
}
//==================================================================================================
@@ -498,65 +497,26 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest {
//==================================================================================================
private void assertProgramSelected(Program p) {
- Program selectedProgram = panel.getSelectedProgram();
+ Program selectedProgram = panel.getSelectedTabValue();
assertEquals(selectedProgram, p);
}
- private void printResizeDebug() {
- //
- // To show the '>>' label, the number of tabs must exceed the room visible to show them
- //
-
- // frame size
-
- // available width
- int panelWidth = panel.getWidth();
- System.out.println("available width: " + panelWidth);
-
- // size label
- int totalWidth = 0;
- JComponent listLabel = (JComponent) getInstanceField("showHiddenListLabel", panel);
- System.out.println("label width: " + listLabel.getWidth());
- totalWidth = listLabel.getWidth();
-
- // size of each tab's panel
- Map, ?> map = (Map, ?>) getInstanceField("linkedProgramMap", panel);
- Collection> values = map.values();
- for (Object object : values) {
- JComponent c = (JComponent) object;
- totalWidth += c.getWidth();
- System.out.println("\t" + c.getWidth());
- }
-
- System.out.println("Total width: " + totalWidth + " out of " + panelWidth);
- }
-
private void assertShowing(Program p) throws Exception {
waitForConditionWithoutFailing(() -> {
- boolean isHidden = runSwing(() -> panel.isHidden(p));
+ boolean isHidden = runSwing(() -> panel.getHiddenTabs().contains(p));
return !isHidden;
});
- boolean isHidden = runSwing(() -> panel.isHidden(p));
+ boolean isHidden = runSwing(() -> panel.getHiddenTabs().contains(p));
if (isHidden) {
capture(tool.getToolFrame(), "multi.tabs.program2.should.be.showing");
}
- assertFalse(runSwing(() -> panel.isHidden(p)));
+ assertFalse(runSwing(() -> panel.getHiddenTabs().contains(p)));
}
private void assertHidden(Program p) {
- assertTrue(runSwing(() -> panel.isHidden(p)));
- }
-
- private void assertListWindowHidden() {
- Window listWindow = getListWindow();
- assertFalse(listWindow.isShowing());
- }
-
- private void assertListWindowShowing() {
- Window listWindow = getListWindow();
- assertTrue(listWindow.isShowing());
+ assertTrue(runSwing(() -> panel.getHiddenTabs().contains(p)));
}
private MarkerSet createMarkers(final Program p) {
@@ -567,9 +527,10 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest {
}
private Window getListWindow() {
- Window window = windowForComponent(panel);
- ProgramListPanel listPanel = findComponent(window, ProgramListPanel.class, true);
- return windowForComponent(listPanel);
+ TabListPopup> tabList =
+ (TabListPopup>) waitForWindowByTitleContaining("Popup Window Showing");
+
+ return tabList;
}
private void performPreviousAction() throws Exception {
@@ -596,10 +557,10 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest {
JPanel tab = panel.getTab(p);
Point point = tab.getLocationOnScreen();
clickMouse(tab, MouseEvent.BUTTON1, point.x + 1, point.y + 1, 1, 0);
- assertEquals(p, panel.getSelectedProgram());
+ assertEquals(p, panel.getSelectedTabValue());
}
- private JPanel getTabPanel(final Program p) {
+ private JPanel getTabPanel(Program p) {
final AtomicReference ref = new AtomicReference<>();
runSwing(() -> ref.set(panel.getTab(p)));
@@ -652,11 +613,12 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest {
return doOpenProgram(program, makeCurrent);
}
+ @SuppressWarnings("unchecked")
private Program doOpenProgram(Program p, boolean makeCurrent) {
int programState = makeCurrent ? ProgramManager.OPEN_CURRENT : ProgramManager.OPEN_VISIBLE;
pm.openProgram(p, programState);
waitForSwing();
- panel = findComponent(tool.getToolFrame(), MultiTabPanel.class);
+ panel = findComponent(tool.getToolFrame(), GTabPanel.class);
// don't let focus issues hide the popup list
panel.setIgnoreFocus(true);
@@ -684,15 +646,15 @@ public class MultiTabPluginTest extends AbstractGhidraHeadedIntegrationTest {
waitForSwing();
}
- private ProgramListPanel showList() {
- JLabel listLabel = (JLabel) findComponentByName(panel, "showList");
- Point p = listLabel.getLocationOnScreen();
- clickMouse(listLabel, MouseEvent.BUTTON1, p.x + 3, p.y + 2, 1, 0);
+ private TabListPopup> showList() {
+ HiddenValuesButton control = findComponent(panel, HiddenValuesButton.class);
+ Point p = control.getLocationOnScreen();
+ clickMouse(control, MouseEvent.BUTTON1, p.x + 3, p.y + 2, 1, 0);
waitForSwing();
- Window window = windowForComponent(panel);
- ProgramListPanel listPanel = findComponent(window, ProgramListPanel.class, true);
- assertNotNull(listPanel);
- return listPanel;
+ TabListPopup> tabList =
+ (TabListPopup>) waitForWindowByTitleContaining("Popup Window Showing");
+ assertNotNull(tabList);
+ return tabList;
}
}
diff --git a/Ghidra/Features/ProgramDiff/src/test.slow/java/ghidra/app/plugin/core/diff/DiffTest.java b/Ghidra/Features/ProgramDiff/src/test.slow/java/ghidra/app/plugin/core/diff/DiffTest.java
index d3419822f2..8a0c84d8aa 100644
--- a/Ghidra/Features/ProgramDiff/src/test.slow/java/ghidra/app/plugin/core/diff/DiffTest.java
+++ b/Ghidra/Features/ProgramDiff/src/test.slow/java/ghidra/app/plugin/core/diff/DiffTest.java
@@ -20,6 +20,7 @@ import static org.junit.Assert.*;
import java.awt.Color;
import java.awt.Window;
import java.math.BigInteger;
+import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -33,14 +34,13 @@ import docking.DialogComponentProvider;
import docking.action.DockingActionIf;
import docking.widgets.fieldpanel.FieldPanel;
import docking.widgets.fieldpanel.support.FieldLocation;
+import docking.widgets.tab.GTabPanel;
import ghidra.app.cmd.data.CreateDataCmd;
import ghidra.app.events.ProgramLocationPluginEvent;
import ghidra.app.events.ProgramSelectionPluginEvent;
-import ghidra.app.plugin.core.progmgr.MultiTabPanel;
import ghidra.app.plugin.core.progmgr.MultiTabPlugin;
import ghidra.app.util.viewer.field.OpenCloseField;
import ghidra.app.util.viewer.listingpanel.ListingModel;
-import ghidra.framework.plugintool.Plugin;
import ghidra.program.database.ProgramBuilder;
import ghidra.program.database.ProgramDB;
import ghidra.program.model.address.AddressSet;
@@ -642,7 +642,7 @@ public class DiffTest extends DiffTestAdapter {
builder4.createMemory(".data", "0x1008000", 0x600);
ProgramDB program4 = builder4.getProgram();
- tool.removePlugins(new Plugin[] { pt });
+ tool.removePlugins(Arrays.asList(pt));
tool.addPlugin(MultiTabPlugin.class.getName());
openProgram(program3);
openProgram(program4);
@@ -653,7 +653,7 @@ public class DiffTest extends DiffTestAdapter {
ProgramSelection expectedSelection = new ProgramSelection(getSetupAllDiffsSet());
checkIfSameSelection(expectedSelection, diffPlugin.getDiffHighlightSelection());
- MultiTabPanel panel = findComponent(tool.getToolFrame(), MultiTabPanel.class);
+ GTabPanel panel = getTabPanel();
assertEquals(true, isDiffing());
assertEquals(true, isShowingDiff());
@@ -762,7 +762,7 @@ public class DiffTest extends DiffTestAdapter {
builder4.createMemory(".data", "0x1008000", 0x600);
ProgramDB program4 = builder4.getProgram();
- tool.removePlugins(new Plugin[] { pt });
+ tool.removePlugins(Arrays.asList(pt));
tool.addPlugin(MultiTabPlugin.class.getName());
openProgram(program3);
openProgram(program4);
@@ -773,7 +773,7 @@ public class DiffTest extends DiffTestAdapter {
ProgramSelection expectedSelection = new ProgramSelection(getSetupAllDiffsSet());
checkIfSameSelection(expectedSelection, diffPlugin.getDiffHighlightSelection());
- MultiTabPanel panel = findComponent(tool.getToolFrame(), MultiTabPanel.class);
+ GTabPanel panel = getTabPanel();
assertEquals(true, isDiffing());
assertEquals(true, isShowingDiff());
@@ -850,6 +850,10 @@ public class DiffTest extends DiffTestAdapter {
//==================================================================================================
// Private Methods
//==================================================================================================
+ @SuppressWarnings("unchecked")
+ private GTabPanel getTabPanel() {
+ return findComponent(tool.getToolFrame(), GTabPanel.class);
+ }
private Color getBgColor(FieldPanel fp, BigInteger index) {
return runSwing(() -> fp.getBackgroundColor(index));
@@ -930,9 +934,8 @@ public class DiffTest extends DiffTestAdapter {
return true;
}
- private void selectTab(final MultiTabPanel panel, final Program pgm) {
- runSwing(() -> invokeInstanceMethod("setSelectedProgram", panel,
- new Class[] { Program.class }, new Object[] { pgm }), true);
+ private void selectTab(GTabPanel panel, Program pgm) {
+ runSwing(() -> panel.selectTab(pgm));
waitForSwing();
}
diff --git a/Ghidra/Framework/Docking/certification.manifest b/Ghidra/Framework/Docking/certification.manifest
index 9bf45ce2b5..6f21b84484 100644
--- a/Ghidra/Framework/Docking/certification.manifest
+++ b/Ghidra/Framework/Docking/certification.manifest
@@ -32,6 +32,7 @@ src/main/resources/images/Minus.png||GHIDRA||||END|
src/main/resources/images/Plus.png||GHIDRA||reviewed||END|
src/main/resources/images/StackFrameElement.png||GHIDRA||reviewed||END|
src/main/resources/images/StackFrame_Red.png||GHIDRA||reviewed||END|
+src/main/resources/images/VCRFastForward.gif||GHIDRA||||END|
src/main/resources/images/accessories-text-editor.png||Tango Icons - Public Domain||||END|
src/main/resources/images/application-vnd.oasis.opendocument.spreadsheet-template.png||Oxygen Icons - LGPL 3.0|||oxygen|END|
src/main/resources/images/application_xp.png||FAMFAMFAM Icons - CC 2.5|||fam fam|END|
@@ -85,6 +86,7 @@ src/main/resources/images/page_code.png||FAMFAMFAM Icons - CC 2.5||||END|
src/main/resources/images/page_excel.png||FAMFAMFAM Icons - CC 2.5||||END|
src/main/resources/images/page_go.png||FAMFAMFAM Icons - CC 2.5||||END|
src/main/resources/images/page_green.png||FAMFAMFAM Icons - CC 2.5||||END|
+src/main/resources/images/pinkX.gif||GHIDRA||||END|
src/main/resources/images/play.png||GHIDRA||||END|
src/main/resources/images/preferences-system-windows.png||Tango Icons - Public Domain||||END|
src/main/resources/images/software-update-available.png||Tango Icons - Public Domain|||tango icon set|END|
@@ -99,6 +101,7 @@ src/main/resources/images/view-filter.png||Oxygen Icons - LGPL 3.0|||Oxygen icon
src/main/resources/images/warning.help.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END|
src/main/resources/images/www_128.png||Nuvola Icons - LGPL 2.1|||nuvola www.png|END|
src/main/resources/images/www_16.png||Nuvola Icons - LGPL 2.1|||nuvola www 16x16|END|
+src/main/resources/images/x.gif||GHIDRA||||END|
src/main/resources/images/zoom.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
src/main/resources/images/zoom_in.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
src/main/resources/images/zoom_out.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
diff --git a/Ghidra/Framework/Docking/data/docking.theme.properties b/Ghidra/Framework/Docking/data/docking.theme.properties
index 8a5a1cac42..26fe3ad3a6 100644
--- a/Ghidra/Framework/Docking/data/docking.theme.properties
+++ b/Ghidra/Framework/Docking/data/docking.theme.properties
@@ -42,6 +42,18 @@ color.fg.fieldpanel = color.fg
color.bg.fieldpanel.selection = color.bg.selection
color.bg.fieldpanel.highlight = color.bg.highlight
+color.bg.widget.tabs.selected = [color]system.color.bg.selected.view
+color.fg.widget.tabs.selected = [color]system.color.fg.selected.view
+color.bg.widget.tabs.unselected = [color]system.color.bg.control
+color.fg.widget.tabs.unselected = color.fg
+color.bg.widget.tabs.highlighted = color.palette.lightcornflowerblue
+
+color.bg.widget.tabs.list = [color]system.color.bg.tooltip
+color.bg.widget.tabs.more.tabs.hover = color.bg.widget.tabs.selected
+
+color.fg.widget.tabs.list = color.fg
+
+
icon.folder.new = folder_add.png
icon.toggle.expand = expand.gif
icon.toggle.collapse = collapse.gif
@@ -104,6 +116,14 @@ icon.widget.table.header.help = info_small.png
icon.widget.table.header.help.hovered = info_small_hover.png
icon.widget.table.header.pending = icon.pending
+icon.widget.tabs.empty.small = empty8x16.png
+icon.widget.tabs.close = x.gif
+icon.widget.tabs.close.highlight = pinkX.gif
+icon.widget.tabs.list = VCRFastForward.gif
+font.widget.tabs.selected = sansserif-plain-11
+font.widget.tabs = sansserif-plain-11
+font.widget.tabs.list = sansserif-bold-9
+
icon.dialog.error.expandable.report = icon.spreadsheet
icon.dialog.error.expandable.exception = program_obj.png
icon.dialog.error.expandable.frame = StackFrameElement.png
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/ComponentPlaceholder.java b/Ghidra/Framework/Docking/src/main/java/docking/ComponentPlaceholder.java
index 2571fd477d..1fff84ea1a 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/ComponentPlaceholder.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/ComponentPlaceholder.java
@@ -15,10 +15,9 @@
*/
package docking;
-import java.awt.*;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
+import java.awt.Frame;
+import java.awt.Window;
+import java.util.*;
import javax.swing.*;
@@ -277,7 +276,7 @@ public class ComponentPlaceholder {
* Requests focus for the component associated with this placeholder.
*/
void requestFocus() {
- Component tmp = comp;// put in temp variable in case another thread deletes it
+ DockableComponent tmp = comp;// put in temp variable in case another thread deletes it
if (tmp == null) {
return;
}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/DialogComponentProvider.java b/Ghidra/Framework/Docking/src/main/java/docking/DialogComponentProvider.java
index 15e8b4fe47..77a3affa58 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/DialogComponentProvider.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/DialogComponentProvider.java
@@ -45,7 +45,7 @@ import utility.function.Callback;
* all the gui elements to appear in the dialog, then use tool.showDialog() to display your dialog.
*/
public class DialogComponentProvider
- implements ActionContextProvider, StatusListener, TaskListener {
+ implements ActionContextProvider, StatusListener, TaskListener {
private static final Color FG_COLOR_ALERT = new GColor("color.fg.dialog.status.alert");
private static final Color FG_COLOR_ERROR = new GColor("color.fg.dialog.status.error");
@@ -103,6 +103,7 @@ public class DialogComponentProvider
private boolean isTransient = false;
private Dimension defaultSize;
+ private String accessibleDescription;
/**
* Constructor for a DialogComponentProvider that will be modal and will include a status line and
@@ -134,7 +135,7 @@ public class DialogComponentProvider
* doing so.
*/
protected DialogComponentProvider(String title, boolean modal, boolean includeStatus,
- boolean includeButtons, boolean canRunTasks) {
+ boolean includeButtons, boolean canRunTasks) {
this.modal = modal;
this.title = title;
rootPanel = new JPanel(new BorderLayout()) {
@@ -639,10 +640,19 @@ public class DialogComponentProvider
Swing.runIfSwingOrRunLater(() -> doSetStatusText(text, type, alert));
}
+ /**
+ * Sets a description of the dialog that will be read by screen readers when the dialog
+ * is made visible.
+ * @param description a description of the dialog
+ */
+ public void setAccessibleDescription(String description) {
+ this.accessibleDescription = description;
+ }
+
private void doSetStatusText(String text, MessageType type, boolean alert) {
SystemUtilities
- .assertThisIsTheSwingThread("Setting text must be performed on the Swing thread");
+ .assertThisIsTheSwingThread("Setting text must be performed on the Swing thread");
statusLabel.setText(text);
statusLabel.setForeground(getStatusColor(type));
@@ -677,7 +687,7 @@ public class DialogComponentProvider
// must be on Swing; this allows us to synchronize the 'alerting' flag
SystemUtilities
- .assertThisIsTheSwingThread("Alerting must be performed on the Swing thread");
+ .assertThisIsTheSwingThread("Alerting must be performed on the Swing thread");
if (isAlerting) {
return;
@@ -736,7 +746,7 @@ public class DialogComponentProvider
}
protected void showProgressBar(String localTitle, boolean hasProgress, boolean canCancel,
- int delay) {
+ int delay) {
taskMonitorComponent.reset();
Runnable r = () -> {
if (delay <= 0) {
@@ -852,7 +862,7 @@ public class DialogComponentProvider
* @see #hideTaskMonitorComponent()
*/
public TaskMonitor showTaskMonitorComponent(String localTitle, boolean hasProgress,
- boolean canCancel) {
+ boolean canCancel) {
showProgressBar(localTitle, hasProgress, canCancel, DEFAULT_DELAY);
return taskMonitorComponent;
}
@@ -1096,6 +1106,9 @@ public class DialogComponentProvider
void setDialog(DockingDialog dialog) {
this.dialog = dialog;
+ if (dialog != null) {
+ dialog.getAccessibleContext().setAccessibleDescription(accessibleDescription);
+ }
}
DockingDialog getDialog() {
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/actions/dialog/ActionChooserDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/actions/dialog/ActionChooserDialog.java
index 3833843485..bdf76071c0 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/actions/dialog/ActionChooserDialog.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/actions/dialog/ActionChooserDialog.java
@@ -59,6 +59,10 @@ public class ActionChooserDialog extends DialogComponentProvider {
addOKButton();
addCancelButton();
updateTitle();
+ setAccessibleDescription(
+ "This dialog initialy shows only locally relevant actions. Repeat initial keybinding " +
+ "to show More. Use up down arrows to scroll through list of actions and press" +
+ " enter to invoke selected action. Type text to filter list.");
}
@Override
@@ -86,14 +90,12 @@ public class ActionChooserDialog extends DialogComponentProvider {
* @param dialog the DialogComponentProvider that has focus
* @param context the ActionContext that is active and will be used to invoke the chosen action
*/
- public ActionChooserDialog(Tool tool, DialogComponentProvider dialog,
- ActionContext context) {
+ public ActionChooserDialog(Tool tool, DialogComponentProvider dialog, ActionContext context) {
this(dialog.getActions(), new HashSet<>(), context);
}
private ActionChooserDialog(Set localActions,
- Set globalActions,
- ActionContext context) {
+ Set globalActions, ActionContext context) {
this(new ActionsModel(localActions, globalActions, context));
}
@@ -138,6 +140,7 @@ public class ActionChooserDialog extends DialogComponentProvider {
private JComponent buildMainPanel() {
JPanel panel = new JPanel(new BorderLayout());
+ panel.setBorder(BorderFactory.createEmptyBorder(5, 2, 0, 2));
searchList = new SearchList(model, (a, c) -> actionChosen(a)) {
@Override
protected BiPredicate createFilter(String text) {
@@ -148,6 +151,8 @@ public class ActionChooserDialog extends DialogComponentProvider {
searchList.setSelectionCallback(this::itemSelected);
searchList.setInitialSelection(); // update selection after adding our listener
searchList.setItemRenderer(new ActionRenderer());
+ searchList.setDisplayNameFunction(
+ (t, c) -> getActionDisplayName(t, c) + " " + getKeyBindingString(t));
panel.add(searchList);
return panel;
}
@@ -156,14 +161,13 @@ public class ActionChooserDialog extends DialogComponentProvider {
if (!canPerformAction(action)) {
return;
}
-
+ ActionContext context = model.getContext();
close();
- scheduleActionAfterFocusRestored(action);
+ scheduleActionAfterFocusRestored(action, context);
}
- private void scheduleActionAfterFocusRestored(DockingActionIf action) {
+ private void scheduleActionAfterFocusRestored(DockingActionIf action, ActionContext context) {
KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager();
- ActionContext context = model.getContext();
actionRunner = new ActionRunner(action, context);
kfm.addPropertyChangeListener("permanentFocusOwner", actionRunner);
}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/searchlist/DefaultSearchListModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/searchlist/DefaultSearchListModel.java
index eff66aeb01..bdd8ac2d2a 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/searchlist/DefaultSearchListModel.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/searchlist/DefaultSearchListModel.java
@@ -127,14 +127,19 @@ public class DefaultSearchListModel extends AbstractListModel> getFilteredEntries(BiPredicate filter) {
List> entries = new ArrayList<>();
- for (String category : dataMap.keySet()) {
+ Iterator it = dataMap.keySet().iterator();
+ while (it.hasNext()) {
+ String category = it.next();
List list = getFilteredItems(category, filter);
for (T value : list) {
boolean isFirst = list.get(0) == value;
- boolean isLast = list.get(list.size() - 1) == value;
- entries.add(new SearchListEntry(value, category, isFirst, isLast));
+ boolean isLastInCateogry = list.get(list.size() - 1) == value;
+ boolean isLastCategory = !it.hasNext();
+ boolean showSeparator = isLastInCateogry && !isLastCategory;
+ entries.add(new SearchListEntry(value, category, isFirst, showSeparator));
}
}
+
return entries;
}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/searchlist/SearchList.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/searchlist/SearchList.java
index 74309b6dc6..8e3906ca3b 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/searchlist/SearchList.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/searchlist/SearchList.java
@@ -43,6 +43,9 @@ public class SearchList extends JPanel {
private Consumer selectedConsumer = Dummy.consumer();
private ListCellRenderer> itemRenderer = new DefaultItemRenderer();
private String currentFilterText;
+ private boolean showCategories = true;
+ private boolean singleClickMode = false;
+ private BiFunction displayNameFunction = (t, c) -> t.toString();
/**
* Construct a new SearchList given a model and an chosen item callback.
@@ -55,8 +58,8 @@ public class SearchList extends JPanel {
this.model = model;
this.chosenItemCallback = Dummy.ifNull(chosenItemCallback);
- add(buildFilterField(), BorderLayout.NORTH);
add(buildList(), BorderLayout.CENTER);
+ add(buildFilterField(), BorderLayout.NORTH);
model.addListDataListener(new SearchListDataListener());
modelChanged();
}
@@ -69,6 +72,14 @@ public class SearchList extends JPanel {
return textField.getText();
}
+ /**
+ * Returns the search list model.
+ * @return the model
+ */
+ public SearchListModel getModel() {
+ return model;
+ }
+
/**
* Sets the current filter text
* @param text the text to set as the current filter
@@ -89,6 +100,17 @@ public class SearchList extends JPanel {
return null;
}
+ public void setSelectedItem(T t) {
+ ListModel> listModel = jList.getModel();
+ for (int i = 0; i < listModel.getSize(); i++) {
+ SearchListEntry entry = listModel.getElementAt(i);
+ if (entry.value().equals(t)) {
+ jList.setSelectedIndex(i);
+ return;
+ }
+ }
+ }
+
/**
* Sets a consumer to be notified whenever the selected item changes.
* @param consumer the consumer to be notified whenever the selected item changes.
@@ -112,9 +134,39 @@ public class SearchList extends JPanel {
*/
public void setInitialSelection() {
jList.clearSelection();
- if (model.getSize() > 0) {
- jList.setSelectedIndex(0);
- }
+ }
+
+ /**
+ * Sets an option to display categories in the list or not.
+ * @param b true to show categories, false to not shoe them
+ */
+ public void setShowCategories(boolean b) {
+ showCategories = b;
+ }
+
+ /**
+ * Sets an option for the list to respond to either double or single mouse clicks. By default,
+ * it responds to a double click.
+ * @param b true for single click mode, false for double click mode
+ */
+ public void setSingleClickMode(boolean b) {
+ singleClickMode = b;
+ }
+
+ public void setDisplayNameFunction(BiFunction nameFunction) {
+ this.displayNameFunction = nameFunction;
+ }
+
+ public void setMouseHoverSelection() {
+ jList.addMouseMotionListener(new MouseMotionAdapter() {
+ @Override
+ public void mouseMoved(MouseEvent e) {
+ int index = jList.locationToIndex(e.getPoint());
+ if (index >= 0) {
+ jList.setSelectedIndex(index);
+ }
+ }
+ });
}
/**
@@ -124,15 +176,20 @@ public class SearchList extends JPanel {
model.dispose();
}
+ private String getDisplayName(T value, String category) {
+ return displayNameFunction.apply(value, category);
+ }
+
private Component buildList() {
JPanel panel = new JPanel(new BorderLayout());
- panel.setBorder(BorderFactory.createEmptyBorder(0, 5, 5, 5));
+ panel.setBorder(BorderFactory.createEmptyBorder(2, 0, 0, 0));
jList = new JList>(model);
JScrollPane jScrollPane = new JScrollPane(jList);
jScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
jList.setCellRenderer(new SearchListRenderer());
jList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
jList.addKeyListener(new ListKeyListener());
+ jList.setVisibleRowCount(Math.min(model.getSize(), 20));
jList.addListSelectionListener(e -> {
if (e.getValueIsAdjusting()) {
return;
@@ -141,19 +198,26 @@ public class SearchList extends JPanel {
selectedConsumer.accept(selectedItem);
});
jList.addMouseListener(new GMouseListenerAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ if (singleClickMode && e.getButton() == MouseEvent.BUTTON1) {
+ chooseItem();
+ return;
+ }
+ super.mouseClicked(e);
+ }
+
@Override
public void doubleClickTriggered(MouseEvent e) {
chooseItem();
}
});
-
panel.add(jScrollPane, BorderLayout.CENTER);
return panel;
}
private Component buildFilterField() {
JPanel panel = new JPanel(new BorderLayout());
- panel.setBorder(BorderFactory.createEmptyBorder(10, 5, 5, 5));
textField = new JTextField();
panel.add(textField, BorderLayout.CENTER);
textField.addKeyListener(new TextFieldKeyListener());
@@ -189,7 +253,7 @@ public class SearchList extends JPanel {
return width + 10;
}
- private void chooseItem() {
+ public void chooseItem() {
SearchListEntry selectedValue = jList.getSelectedValue();
if (selectedValue != null) {
chosenItemCallback.accept(selectedValue.value(), selectedValue.category());
@@ -236,30 +300,32 @@ public class SearchList extends JPanel {
panel.setBorder(normalBorder);
// only display the category for the first entry in that category
- if (value.isFirst()) {
+ if (value.showCategory()) {
categoryLabel.setText(value.category());
}
// Display a separator at the bottom of the last entry in the category to make
// category boundaries
- if (value.isLast()) {
+ if (value.drawSeparator()) {
panel.setBorder(lastEntryBorder);
panel.add(jSeparator, BorderLayout.SOUTH);
}
Dimension size = categoryLabel.getPreferredSize();
categoryLabel.setPreferredSize(new Dimension(categoryWidth, size.height));
Component itemRendererComp =
- itemRenderer.getListCellRendererComponent(list, value, index,
- isSelected, false);
+ itemRenderer.getListCellRendererComponent(list, value, index, isSelected, false);
Color background = itemRendererComp.getBackground();
- panel.add(categoryLabel, BorderLayout.WEST);
+ if (showCategories) {
+ panel.add(categoryLabel, BorderLayout.WEST);
+ }
panel.add(itemRendererComp, BorderLayout.CENTER);
panel.setBackground(background);
categoryLabel.setOpaque(true);
categoryLabel.setBackground(background);
categoryLabel.setForeground(itemRendererComp.getForeground());
-
+ panel.getAccessibleContext()
+ .setAccessibleName(getDisplayName(value.value(), value.category()));
return panel;
}
}
@@ -268,11 +334,10 @@ public class SearchList extends JPanel {
@Override
public Component getListCellRendererComponent(JList extends SearchListEntry> list,
- SearchListEntry value, int index,
- boolean isSelected, boolean hasFocus) {
+ SearchListEntry value, int index, boolean isSelected, boolean hasFocus) {
- JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index,
- isSelected, false);
+ JLabel label =
+ (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, false);
SearchListEntry entry = value;
T t = entry.value();
label.setText(t.toString());
@@ -308,21 +373,25 @@ public class SearchList extends JPanel {
}
else if (keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_DOWN) {
KeyboardFocusManager.getCurrentKeyboardFocusManager().redispatchEvent(jList, e);
+ jList.requestFocus();
}
}
}
private class ListKeyListener extends KeyAdapter {
@Override
- public void keyPressed(KeyEvent e) {
- int keyCode = e.getKeyCode();
+ public void keyTyped(KeyEvent e) {
+ if (e.getKeyChar() == '\n') {
+ chooseItem();
+
+ }
+ int keyCode = e.getKeyChar();
if (keyCode == KeyEvent.VK_ENTER) {
chooseItem();
}
- else if (keyCode != KeyEvent.VK_UP && keyCode != KeyEvent.VK_DOWN) {
- KeyboardFocusManager.getCurrentKeyboardFocusManager().redispatchEvent(textField, e);
- textField.requestFocus();
- }
+
+ KeyboardFocusManager.getCurrentKeyboardFocusManager().redispatchEvent(textField, e);
+ textField.requestFocus();
}
}
@@ -354,8 +423,12 @@ public class SearchList extends JPanel {
@Override
public boolean test(T t, String category) {
- return t.toString().toLowerCase().contains(filterText);
+ return getDisplayName(t, category).toLowerCase().contains(filterText);
}
}
+ public JTextField getFilterField() {
+ return textField;
+ }
+
}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/searchlist/SearchListEntry.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/searchlist/SearchListEntry.java
index 4ac4033508..a467634b4d 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/searchlist/SearchListEntry.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/searchlist/SearchListEntry.java
@@ -19,13 +19,14 @@ package docking.widgets.searchlist;
* An record to hold the list item and additional information needed to properly render the item.
* @param value the list item (T)
* @param category the category for the item
- * @param isFirst true if this is the first item in the category (categories are only displayed for
- * the first entry)
- * @param isLast true if this is the last item in the category (a separator line is displayed
- * between categories)
+ * @param showCategory true if this is the first item in the category and therefor the category
+ * should be displayed.
+ * @param drawSeparator if true, then a separator line should be drawn after this entry. This
+ * should only be the case for the last entry in a category (and not the last category.)
*
* @param the type of list items
*/
-public record SearchListEntry(T value, String category, boolean isFirst, boolean isLast) {
+public record SearchListEntry(T value, String category, boolean showCategory,
+ boolean drawSeparator) {
}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tab/GTab.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tab/GTab.java
new file mode 100644
index 0000000000..3f90233f5e
--- /dev/null
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tab/GTab.java
@@ -0,0 +1,168 @@
+/* ###
+ * 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 docking.widgets.tab;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+import javax.swing.border.Border;
+
+import docking.widgets.label.GDLabel;
+import docking.widgets.label.GIconLabel;
+import generic.theme.*;
+import ghidra.util.layout.HorizontalLayout;
+import resources.Icons;
+
+/**
+ * Component for representing individual tabs within a {@link GTabPanel}.
+ *
+ * @param the type of the tab values
+ */
+class GTab extends JPanel {
+ private final static Border TAB_BORDER = new GTabBorder(false);
+ private final static Border SELECTED_TAB_BORDER = new GTabBorder(true);
+ private static final String SELECTED_FONT_TABS_ID = "font.widget.tabs.selected";
+ private static final String FONT_TABS_ID = "font.widget.tabs";
+ private final static Icon EMPTY16_ICON = Icons.EMPTY_ICON;
+ private final static Icon CLOSE_ICON = new GIcon("icon.widget.tabs.close");
+ private final static Icon HIGHLIGHT_CLOSE_ICON = new GIcon("icon.widget.tabs.close.highlight");
+ private final static Color TAB_FG_COLOR = new GColor("color.fg.widget.tabs.unselected");
+ private final static Color SELECTED_TAB_FG_COLOR = new GColor("color.fg.widget.tabs.selected");
+ private final static Color HIGHLIGHTED_TAB_BG_COLOR =
+ new GColor("color.bg.widget.tabs.highlighted");
+
+ final static Color TAB_BG_COLOR = new GColor("color.bg.widget.tabs.unselected");
+ final static Color SELECTED_TAB_BG_COLOR = new GColor("color.bg.widget.tabs.selected");
+
+ private GTabPanel tabPanel;
+ private T value;
+ private boolean selected;
+ private JLabel closeLabel;
+ private JLabel nameLabel;
+
+ GTab(GTabPanel gTabPanel, T value, boolean selected) {
+ super(new HorizontalLayout(10));
+ this.tabPanel = gTabPanel;
+ this.value = value;
+ this.selected = selected;
+
+ setBorder(selected ? SELECTED_TAB_BORDER : TAB_BORDER);
+
+ nameLabel = new GDLabel();
+ nameLabel.setName("Tab Label");
+ nameLabel.setText(tabPanel.getDisplayName(value));
+ nameLabel.setIcon(tabPanel.getValueIcon(value));
+ nameLabel.setToolTipText(tabPanel.getValueToolTip(value));
+ Gui.registerFont(nameLabel, selected ? SELECTED_FONT_TABS_ID : FONT_TABS_ID);
+ add(nameLabel, BorderLayout.WEST);
+
+ closeLabel = new GIconLabel(selected ? CLOSE_ICON : EMPTY16_ICON);
+ closeLabel.setToolTipText("Close");
+ closeLabel.setName("Close");
+ closeLabel.setOpaque(true);
+ add(closeLabel, BorderLayout.EAST);
+
+ installMouseListener(this, new GTabMouseListener());
+
+ initializeTabColors(false);
+ }
+
+ T getValue() {
+ return value;
+ }
+
+ void refresh() {
+ nameLabel.setText(tabPanel.getDisplayName(value));
+ nameLabel.setIcon(tabPanel.getValueIcon(value));
+ nameLabel.setToolTipText(tabPanel.getValueToolTip(value));
+ repaint();
+ }
+
+ void setHighlight(boolean b) {
+ initializeTabColors(b);
+ }
+
+ private void installMouseListener(Container c, MouseListener listener) {
+
+ c.addMouseListener(listener);
+ Component[] children = c.getComponents();
+ for (Component element : children) {
+ if (element instanceof Container) {
+ installMouseListener((Container) element, listener);
+ }
+ else {
+ element.addMouseListener(listener);
+ }
+ }
+ }
+
+ private void initializeTabColors(boolean isHighlighted) {
+ Color fg = getForegroundColor(isHighlighted);
+ Color bg = getBackgroundColor(isHighlighted);
+ setBackground(bg);
+ nameLabel.setBackground(bg);
+ nameLabel.setForeground(fg);
+ closeLabel.setBackground(bg);
+ }
+
+ private Color getBackgroundColor(boolean isHighlighted) {
+ if (isHighlighted) {
+ return HIGHLIGHTED_TAB_BG_COLOR;
+ }
+ return selected ? SELECTED_TAB_BG_COLOR : TAB_BG_COLOR;
+ }
+
+ private Color getForegroundColor(boolean isHighlighted) {
+ if (isHighlighted || selected) {
+ return SELECTED_TAB_FG_COLOR;
+ }
+ return TAB_FG_COLOR;
+ }
+
+ private class GTabMouseListener extends MouseAdapter {
+ @Override
+ public void mouseEntered(MouseEvent e) {
+ closeLabel.setIcon(e.getSource() == closeLabel ? HIGHLIGHT_CLOSE_ICON : CLOSE_ICON);
+ }
+
+ @Override
+ public void mouseExited(MouseEvent e) {
+ closeLabel.setIcon(selected ? CLOSE_ICON : EMPTY16_ICON);
+ }
+
+ @Override
+ public void mousePressed(MouseEvent e) {
+ // close the list window if the user has clicked outside of the window
+ if (!(e.getSource() instanceof JList)) {
+ tabPanel.closeTabList();
+ }
+
+ if (e.isPopupTrigger()) {
+ return; // allow popup triggers to show actions without changing tabs
+ }
+
+ if (e.getSource() == closeLabel) {
+ tabPanel.closeTab(value);
+ return;
+ }
+ if (!selected) {
+ tabPanel.selectTab(value);
+ }
+ }
+ }
+
+}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tab/GTabBorder.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tab/GTabBorder.java
new file mode 100644
index 0000000000..ecff87abff
--- /dev/null
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tab/GTabBorder.java
@@ -0,0 +1,88 @@
+/* ###
+ * 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 docking.widgets.tab;
+
+import java.awt.*;
+
+import javax.swing.border.EmptyBorder;
+
+/**
+ * Custom border for the {@link GTab}. For non selected tabs, it basically draws a variation of
+ * a bevel border that is offset from the top by 2 pixels from the selected tab. Selected tabs
+ * are drawn at the very top of the component and doesn't draw the bottom border so that it appears
+ * to connect to the border of the overall tab panel.
+ */
+class GTabBorder extends EmptyBorder {
+ private static int LEFT_MARGIN = 7; // 2 for drawn border and 5 pixels for a left margin
+ private static int TOP_MARGIN = 4; // 2 for border and 2 to play with offset on non-selected
+ private static int RIGHT_MARGIN = 2; // 2 for border. Close Icon adds enough of a visual margin
+ private static int BOTTOM_MARGIN = 2; // 2 for border
+ private int offset = 0;
+
+ private boolean selected;
+
+ GTabBorder(boolean selected) {
+ super(TOP_MARGIN, LEFT_MARGIN, BOTTOM_MARGIN, RIGHT_MARGIN);
+ this.selected = selected;
+
+ // paint non-selected tabs a bit lower
+ if (!selected) {
+ offset = 2;
+ }
+ }
+
+ /**
+ * Paints the border, and also a bottom shadow border that isn't part of the insets, so that
+ * the area that doesn't have tabs, still paints a bottom border
+ */
+ @Override
+ public void paintBorder(Component c, Graphics g, int x, int y, int w, int h) {
+ Color oldColor = g.getColor();
+ g.translate(x, y);
+
+ Color innerHighlight = c.getBackground().brighter();
+ Color outerHighlight = innerHighlight.brighter();
+ Color innerShadow = c.getBackground().darker();
+ Color outerShadow = innerShadow.darker();
+
+ // upper
+ g.setColor(outerHighlight);
+ g.drawLine(1, offset, w - 3, offset); // upper outer
+ g.setColor(innerHighlight);
+ g.drawLine(2, offset + 1, w - 3, offset + 1); // upper inner
+
+ // left
+ g.setColor(outerShadow);
+ g.drawLine(0, offset + 1, 0, h - 1); // left outer
+ g.setColor(innerHighlight);
+ g.drawLine(1, offset + 1, 1, h - 2); // left inner
+
+ // right
+ g.setColor(innerShadow);
+ g.drawLine(w - 2, offset + 1, w - 2, h); // right inner
+ g.setColor(outerShadow);
+ g.drawLine(w - 1, offset + 1, w - 1, h - 2); // right outer
+
+ if (!selected) {
+ g.setColor(outerHighlight);
+ g.drawLine(0, h - 1, w - 1, h - 1); // bottom
+ }
+
+ g.translate(-x, -y);
+ g.setColor(oldColor);
+ }
+
+}
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
new file mode 100644
index 0000000000..a86e20ec9a
--- /dev/null
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tab/GTabPanel.java
@@ -0,0 +1,612 @@
+/* ###
+ * 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 docking.widgets.tab;
+
+import java.awt.event.*;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.Collectors;
+
+import javax.swing.*;
+
+import ghidra.util.layout.HorizontalLayout;
+import utility.function.Dummy;
+
+/**
+ * Component for displaying a list of items as a series of horizontal tabs where exactly one tab
+ * is selected.
+ *
+ * If there are too many tabs to display horizontally, a "hidden tabs" control will be
+ * displayed that when activated, will display a popup dialog with a scrollable list of all
+ * possible values.
+ *
+ * It also supports the idea of a highlighted tab which represents a value that is not selected,
+ * but is a candidate to be selected. For example, when the tab panel has focus, using the left
+ * and right arrows will highlight different tabs. Then pressing enter will cause the highlighted
+ * tab to be selected.
+ *
+ * The clients of this component can also supply functions for customizing the name, icon, and
+ * tooltip for values. They can also add consumers for when the selected value changes or a value
+ * is removed from the tab panel. Clients can also install a predicate for the close tab action so
+ * they can process it before the value is removed and possibly veto the remove.
+ *
+ * @param The type of values in the tab panel.
+ */
+public class GTabPanel extends JPanel {
+
+ private T selectedValue;
+ private T highlightedValue;
+ private boolean ignoreFocusLost;
+ private TabListPopup tabList;
+ private String tabTypeName;
+
+ private Set allValues = new LinkedHashSet<>();
+ private List> allTabs = new ArrayList<>();
+ private HiddenValuesButton hiddenValuesControl = new HiddenValuesButton(this);
+ 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();
+
+ /**
+ * Constructor
+ * @param tabTypeName the name of the type of values in the tab panel. This will be used to
+ * set accessible descriptions.
+ */
+ public GTabPanel(String tabTypeName) {
+ this.tabTypeName = tabTypeName;
+ setLayout(new HorizontalLayout(0));
+ setFocusable(true);
+ setBorder(new GTabPanelBorder());
+ getAccessibleContext().setAccessibleDescription(
+ "Use left and right arrows to highlight other tabs and press enter to select " +
+ "the highlighted tab");
+
+ addComponentListener(new ComponentAdapter() {
+ @Override
+ public void componentResized(ComponentEvent e) {
+ closeTabList();
+ rebuildTabs();
+ }
+ });
+
+ addKeyListener(new KeyAdapter() {
+ @Override
+ public void keyPressed(KeyEvent e) {
+ int keyCode = e.getKeyCode();
+ switch (keyCode) {
+ case KeyEvent.VK_SPACE:
+ case KeyEvent.VK_ENTER:
+ selectHighlightedValue();
+ break;
+ case KeyEvent.VK_LEFT:
+ highlightNextTab(false);
+ break;
+ case KeyEvent.VK_RIGHT:
+ highlightNextTab(true);
+ break;
+ }
+ }
+
+ });
+ addFocusListener(new FocusListener() {
+ @Override
+ public void focusGained(FocusEvent e) {
+ updateTabColors();
+ }
+
+ @Override
+ public void focusLost(FocusEvent e) {
+ highlightedValue = null;
+ updateAccessibleName();
+ updateTabColors();
+ }
+ });
+ }
+
+ /**
+ * Add a new tab to the panel for the given value.
+ * @param value the value for the new tab
+ */
+ public void addTab(T value) {
+ doAddValue(value);
+ rebuildTabs();
+ }
+
+ /**
+ * Add tabs for each value in the given list.
+ * @param values the values to add tabs for
+ */
+ public void addTabs(List values) {
+ for (T t : values) {
+ doAddValue(t);
+ }
+ rebuildTabs();
+ }
+
+ /**
+ * Removes the tab with the given value.
+ * @param value the value for which to remove its tab
+ */
+ public void removeTab(T value) {
+ allValues.remove(value);
+ highlightedValue = null;
+ // ensure there is a valid selected value
+ if (value == selectedValue) {
+ selectedValue = allValues.isEmpty() ? null : allValues.iterator().next();
+ }
+
+ rebuildTabs();
+ removedTabConsumer.accept(value);
+ }
+
+ /**
+ * Remove tabs for all values in the given list.
+ * @param values the values to remove from the tab panel
+ */
+ 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();
+ }
+
+ rebuildTabs();
+ }
+
+ /**
+ * Returns the currently selected tab. If the panel is not empty, there will always be a
+ * selected tab.
+ * @return the currently selected tab or null if the panel is empty
+ */
+ public T getSelectedTabValue() {
+ return selectedValue;
+ }
+
+ /**
+ * Returns the currently highlighted tab if a tab is highlighted. Note: the selected tab can
+ * never be highlighted.
+ * @return the currently highlighted tab or null if no tab is highligted
+ */
+ public T getHighlightedTabValue() {
+ return highlightedValue;
+ }
+
+ /**
+ * Makes the tab for the given value be the selected tab.
+ * @param value the value whose tab is to be selected
+ */
+ public void selectTab(T value) {
+ if (value == null) {
+ return;
+ }
+ if (!allValues.contains(value)) {
+ throw new IllegalArgumentException(
+ "Attempted to set selected value to non added value");
+ }
+ closeTabList();
+ highlightedValue = null;
+ selectedValue = value;
+ rebuildTabs();
+ selectedTabConsumer.accept(value);
+ }
+
+ /**
+ * Returns a list of values for all the tabs in the panel.
+ * @return a list of values for all the tabs in the panel
+ */
+ public List getTabValues() {
+ return new ArrayList<>(allValues);
+ }
+
+ /**
+ * Returns true if the tab for the given value is visible on the tab panel.
+ * @param value the value to test if visible
+ * @return true if the tab for the given value is visible on the tab panel
+ */
+ public boolean isVisibleTab(T value) {
+ for (GTab gTab : allTabs) {
+ if (gTab.getValue().equals(value)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the total number of tabs both visible and hidden.
+ * @return the total number of tabs both visible and hidden.
+ */
+ public int getTabCount() {
+ return allValues.size();
+ }
+
+ /**
+ * Sets the tab for the given value to be highlighted. If the value is selected, then the
+ * highlighted tab will be set to null.
+ * @param value the value to highlight its tab
+ */
+ public void highlightTab(T value) {
+ highlightedValue = value == selectedValue ? null : value;
+ updateTabColors();
+ updateAccessibleName();
+ }
+
+ /**
+ * Returns true if not all tabs are visible in the tab panel.
+ * @return true if not all tabs are visible in the tab panel
+ */
+ public boolean hasHiddenTabs() {
+ return allTabs.size() < allValues.size();
+ }
+
+ /**
+ * Returns a list of all tab values that are not visible.
+ * @return a list of all tab values that are not visible
+ */
+ public List getHiddenTabs() {
+ Set hiddenValues = new LinkedHashSet(allValues);
+ hiddenValues.removeAll(getVisibleTabs());
+ return new ArrayList<>(hiddenValues);
+ }
+
+ /**
+ * Returns a list of all tab values that are visible.
+ * @return a list of all tab values that are visible
+ */
+ public List getVisibleTabs() {
+ return allTabs.stream().map(t -> t.getValue()).collect(Collectors.toList());
+ }
+
+ /**
+ * Shows a popup dialog window with a filterable and scrollable list of all tab values.
+ * @param show true to show the popup list, false to close the popup list
+ */
+ public void showTabList(boolean show) {
+ if (show) {
+ showTabList();
+ }
+ else {
+ closeTabList();
+ }
+ }
+
+ /**
+ * Moves the highlight 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) {
+ if (allValues.size() < 2) {
+ return;
+ }
+ T current = highlightedValue == null ? selectedValue : highlightedValue;
+ if (isShowingTabList()) {
+ current = null;
+ closeTabList();
+ }
+ T next = forward ? getTabbedValueAfter(current) : getTabbedValueBefore(current);
+ highlightTab(next);
+ if (next == null) {
+ showTabList(true);
+ }
+ }
+
+ /**
+ * Informs the tab panel that some displayable property about the value has changed and the
+ * tabs label, icon, and tooltip need to be updated.
+ * @param value the value that has changed
+ */
+ public void refreshTab(T value) {
+ int tabIndex = getTabIndex(value);
+ if (tabIndex >= 0) {
+ allTabs.get(tabIndex).refresh();
+ }
+ }
+
+ /**
+ * Sets a function to be used to generated a display name for a given value. The display name
+ * is used in the tab, the filter, and the accessible description.
+ * @param nameFunction the function to generate display names for values
+ */
+ public void setNameFunction(Function nameFunction) {
+ this.nameFunction = nameFunction;
+ }
+
+ /**
+ * Sets a function to be used to generated an icon for a given value.
+ * @param iconFunction the function to generate icons for values
+ */
+ public void setIconFunction(Function iconFunction) {
+ this.iconFunction = iconFunction;
+ }
+
+ /**
+ * Sets a function to be used to generated an tooltip for a given value.
+ * @param toolTipFunction the function to generate tooltips for values
+ */
+ public void setToolTipFunction(Function toolTipFunction) {
+ this.toolTipFunction = toolTipFunction;
+ }
+
+ /**
+ * 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
+ */
+ 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;
+ }
+
+ /**
+ * Sets the consumer to be notified when the selected tab changes.
+ * @param selectedTabConsumer the consumer to be notified when the selected tab changes
+ */
+ public void setSelectedTabConsumer(Consumer selectedTabConsumer) {
+ this.selectedTabConsumer = selectedTabConsumer;
+ }
+
+ /**
+ * Returns true if the popup tab list is showing.
+ * @return true if the popup tab list is showing
+ */
+ public boolean isShowingTabList() {
+ return tabList != null;
+ }
+
+ void showTabList() {
+ if (tabList != null) {
+ return;
+ }
+ JComponent c = hasHiddenTabs() ? hiddenValuesControl : allTabs.get(allTabs.size() - 1);
+ tabList = new TabListPopup(this, c, tabTypeName);
+ tabList.setVisible(true);
+ }
+
+ void closeTab(T value) {
+ if (removeTabPredicate.test(value)) {
+ removeTab(value);
+ }
+ }
+
+ private void selectHighlightedValue() {
+ if (highlightedValue != null) {
+ selectTab(highlightedValue);
+ }
+ }
+
+ void highlightFromTabList(boolean forward) {
+ closeTabList();
+ int highlightIndex = forward ? 0 : allTabs.size() - 1;
+ highlightTab(allTabs.get(highlightIndex).getValue());
+ requestFocus();
+ }
+
+ private T getTabbedValueAfter(T current) {
+ if (current == null) {
+ return allTabs.get(0).getValue();
+ }
+ int tabIndex = getTabIndex(current);
+ if (tabIndex >= 0 && tabIndex < allTabs.size() - 1) {
+ return allTabs.get(tabIndex + 1).getValue();
+ }
+ if (hasHiddenTabs()) {
+ return null;
+ }
+ return allTabs.get(0).getValue();
+ }
+
+ private T getTabbedValueBefore(T current) {
+ if (current == null) {
+ return allTabs.get(allTabs.size() - 1).getValue();
+ }
+ int tabIndex = getTabIndex(current);
+ if (tabIndex >= 1) {
+ return allTabs.get(tabIndex - 1).getValue();
+ }
+ if (hasHiddenTabs()) {
+ return null;
+ }
+ return allTabs.get(allTabs.size() - 1).getValue();
+ }
+
+ private int getTabIndex(T value) {
+ for (int i = 0; i < allTabs.size(); i++) {
+ if (allTabs.get(i).getValue().equals(value)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private void updateTabColors() {
+ boolean tabPanelHasFocus = hasFocus();
+ for (GTab tab : allTabs) {
+ T value = tab.getValue();
+ tab.setHighlight(shouldHighlight(value, tabPanelHasFocus));
+ }
+ }
+
+ private boolean shouldHighlight(T value, boolean tabPanelHasFocus) {
+ if (value.equals(highlightedValue)) {
+ return true;
+ }
+ if (tabPanelHasFocus && highlightedValue == null) {
+ return value.equals(selectedValue);
+ }
+ return false;
+ }
+
+ 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();
+ repaint();
+ return;
+ }
+
+ GTab selectedTab = new GTab(this, selectedValue, true);
+ int availableWidth = getPanelWidth() - getTabWidth(selectedTab);
+
+ createNonSelectedTabsForWidth(availableWidth);
+
+ // a negative available width means there wasn't even enough room for the selected value tab
+ if (availableWidth >= 0) {
+ allTabs.add(getIndexToInsertSelectedValue(allTabs.size()), selectedTab);
+ }
+
+ // add tabs to this panel
+ for (GTab gTab : allTabs) {
+ add(gTab);
+ }
+
+ // if there are hidden tabs add hidden value control to this panel
+ if (hasHiddenTabs()) {
+ hiddenValuesControl.setHiddenCount(allValues.size() - allTabs.size());
+ add(hiddenValuesControl);
+ }
+ updateTabColors();
+ updateAccessibleName();
+ validate();
+ repaint();
+ }
+
+ private void updateAccessibleName() {
+ getAccessibleContext().setAccessibleName(getAccessibleName());
+ }
+
+ private String getAccessibleName() {
+ String panelName = tabTypeName + "Tab Panel";
+ if (allValues.isEmpty()) {
+ return panelName + ": No Tabs";
+ }
+ String accessibleName = panelName + ": " + getDisplayName(selectedValue) + "Selected";
+ if (highlightedValue != null) {
+ accessibleName += ": " + getDisplayName(highlightedValue) + " highlighted";
+ }
+ return accessibleName;
+ }
+
+ private int getIndexToInsertSelectedValue(int maxIndex) {
+ Iterator it = allValues.iterator();
+ for (int i = 0; i < maxIndex; i++) {
+ T t = it.next();
+ if (t == selectedValue) {
+ return i;
+ }
+ }
+ return maxIndex;
+ }
+
+ private void createNonSelectedTabsForWidth(int availableWidth) {
+ for (T value : allValues) {
+ if (value == selectedValue) {
+ continue;
+ }
+ GTab tab = new GTab(this, value, false);
+
+ int tabWidth = getTabWidth(tab);
+ if (tabWidth > availableWidth) {
+ break;
+ }
+
+ allTabs.add(tab);
+ availableWidth -= tabWidth;
+ }
+
+ // remove last tab if there isn't room for hidden values control
+ if (hasHiddenTabs() && availableWidth < hiddenValuesControl.getPreferredWidth()) {
+ if (!allTabs.isEmpty()) {
+ allTabs.remove(allTabs.size() - 1);
+ }
+ }
+ }
+
+ private int getTabWidth(GTab tab) {
+ return tab.getPreferredSize().width;
+ }
+
+ private int getPanelWidth() {
+ return getSize().width;
+ }
+
+ boolean isListWindowShowing() {
+ return tabList != null;
+ }
+
+ String getDisplayName(T t) {
+ return nameFunction.apply(t);
+ }
+
+ Icon getValueIcon(T value) {
+ return iconFunction.apply(value);
+ }
+
+ String getValueToolTip(T value) {
+ return toolTipFunction.apply(value);
+ }
+
+ void tabListFocusLost() {
+ if (!ignoreFocusLost) {
+ closeTabList();
+ }
+ }
+
+ void closeTabList() {
+ if (tabList != null) {
+ tabList.close();
+ tabList = null;
+ }
+ }
+
+ /*testing*/public void setIgnoreFocus(boolean ignoreFocusLost) {
+ this.ignoreFocusLost = ignoreFocusLost;
+ }
+
+ /*testing*/public JPanel getTab(T value) {
+ for (GTab tab : allTabs) {
+ if (tab.getValue().equals(value)) {
+ return tab;
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tab/GTabPanelBorder.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tab/GTabPanelBorder.java
new file mode 100644
index 0000000000..6d40ffdbd5
--- /dev/null
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tab/GTabPanelBorder.java
@@ -0,0 +1,55 @@
+/* ###
+ * 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 docking.widgets.tab;
+
+import java.awt.*;
+
+import javax.swing.border.EmptyBorder;
+
+/**
+ * Custom border for the {@link GTab}.
+ */
+public class GTabPanelBorder extends EmptyBorder {
+ public static final int MARGIN_SIZE = 2;
+ public static final int BOTTOM_SOLID_COLOR_SIZE = 3;
+
+ public GTabPanelBorder() {
+ super(0, 0, BOTTOM_SOLID_COLOR_SIZE, 0);
+ }
+
+ /**
+ * Paints the border, and also a bottom shadow border that isn't part of the insets, so that
+ * the area that doesn't have tabs, still paints a bottom border
+ */
+ @Override
+ public void paintBorder(Component c, Graphics g, int x, int y, int w, int h) {
+ Insets insets = getBorderInsets(c);
+ Color oldColor = g.getColor();
+ g.translate(x, y);
+
+ Color highlight = GTab.TAB_BG_COLOR.brighter().brighter();
+
+ g.setColor(GTab.SELECTED_TAB_BG_COLOR);
+ g.fillRect(insets.left, h - insets.bottom, w - insets.right - 1, insets.bottom);
+
+ g.setColor(highlight);
+ g.drawLine(insets.left, h - insets.bottom - 1, w - insets.right - 1, h - insets.bottom - 1);
+
+ g.translate(-x, -y);
+ g.setColor(oldColor);
+ }
+
+}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tab/HiddenValuesButton.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tab/HiddenValuesButton.java
new file mode 100644
index 0000000000..a0634d651e
--- /dev/null
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tab/HiddenValuesButton.java
@@ -0,0 +1,93 @@
+/* ###
+ * 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 docking.widgets.tab;
+
+import java.awt.Color;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+
+import javax.swing.*;
+import javax.swing.border.BevelBorder;
+import javax.swing.border.Border;
+
+import docking.widgets.label.GDLabel;
+import generic.theme.*;
+
+/**
+ * Component displayed when not all tabs fit on the tab panel and is used to display a popup
+ * list of all tabs.
+ */
+public class HiddenValuesButton extends GDLabel {
+ //@formatter:off
+ private static final String FONT_TABS_LIST_ID = "font.widget.tabs.list";
+ private static final Icon LIST_ICON = new GIcon("icon.widget.tabs.list");
+ private static final Color BG_COLOR_MORE_TABS_HOVER = new GColor("color.bg.widget.tabs.more.tabs.hover");
+ private static final String DEFAULT_HIDDEN_COUNT_STR = "99";
+ //@formatter:on
+
+ private Border defaultListLabelBorder;
+
+ HiddenValuesButton(GTabPanel> tabPanel) {
+ super(DEFAULT_HIDDEN_COUNT_STR, LIST_ICON, SwingConstants.LEFT);
+ setName("Hidden Values Control");
+ setIconTextGap(2);
+ Gui.registerFont(this, FONT_TABS_LIST_ID);
+ setBorder(BorderFactory.createEmptyBorder(4, 4, 0, 4));
+ setToolTipText("Show Tab List");
+ getAccessibleContext().setAccessibleName("Show Hidden Values List");
+ setBackground(BG_COLOR_MORE_TABS_HOVER);
+
+ defaultListLabelBorder = getBorder();
+ Border hoverBorder = BorderFactory.createBevelBorder(BevelBorder.RAISED);
+ addMouseListener(new MouseAdapter() {
+ @Override
+ public void mousePressed(MouseEvent e) {
+ if (tabPanel.isListWindowShowing()) {
+ tabPanel.closeTabList();
+ return;
+ }
+ tabPanel.showTabList(true);
+ }
+
+ @Override
+ public void mouseEntered(MouseEvent e) {
+ // show a raised border, like a button (if the window is not already visible)
+ if (tabPanel.isListWindowShowing()) {
+ return;
+ }
+
+ setBorder(hoverBorder);
+ setOpaque(true);
+ }
+
+ @Override
+ public void mouseExited(MouseEvent e) {
+ setBorder(defaultListLabelBorder);
+ setOpaque(false);
+ }
+ });
+
+ setPreferredSize(getPreferredSize());
+ }
+
+ void setHiddenCount(int count) {
+ setText(Integer.toString(count));
+ }
+
+ int getPreferredWidth() {
+ return getPreferredSize().width;
+ }
+}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/tab/TabListPopup.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tab/TabListPopup.java
new file mode 100644
index 0000000000..ebf3e4b058
--- /dev/null
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/tab/TabListPopup.java
@@ -0,0 +1,170 @@
+/* ###
+ * 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 docking.widgets.tab;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.List;
+
+import javax.swing.*;
+
+import docking.widgets.list.GListCellRenderer;
+import docking.widgets.searchlist.*;
+import generic.util.WindowUtilities;
+
+/**
+ * Undecorated dialog for showing a popup window displaying a filterable, scrollable list of tabs
+ * in a {@link GTabPanel}.
+ *
+ * @param the value types
+ */
+public class TabListPopup extends JDialog {
+ private static final String HIDDEN = "Hidden";
+ private static final String VISIBLE = "Visible";
+ private GTabPanel panel;
+ private SearchList searchList;
+
+ TabListPopup(GTabPanel panel, JComponent positioningComponent, String typeName) {
+ super(WindowUtilities.windowForComponent(panel));
+ setTitle("Popup Window Showing All " + typeName + " Tabs");
+ this.panel = panel;
+ setUndecorated(true);
+ getAccessibleContext().setAccessibleDescription("Use up down arrows to move between " +
+ typeName + "tab choices and press enter to select tab. Type text to filter choices. " +
+ "Left right arrows to close popup and return focus to visible tabs");
+
+ SearchListModel tabListModel = createTabListModel();
+ searchList = new SearchList(tabListModel, (T, C) -> panel.selectTab(T));
+ searchList.setItemRenderer(new TabListRenderer());
+ searchList.setShowCategories(false);
+ searchList.setSingleClickMode(true);
+ searchList.setMouseHoverSelection();
+ searchList.setDisplayNameFunction((t, c) -> panel.getDisplayName(t));
+ add(searchList);
+
+ addWindowFocusListener(new WindowFocusListener() {
+
+ @Override
+ public void windowGainedFocus(WindowEvent e) {
+ // don't care
+ }
+
+ @Override
+ public void windowLostFocus(WindowEvent e) {
+ panel.tabListFocusLost();
+ }
+
+ });
+
+ KeyAdapter keyListener = new KeyAdapter() {
+ @Override
+ public void keyPressed(KeyEvent e) {
+ int keyCode = e.getKeyCode();
+ switch (keyCode) {
+ case KeyEvent.VK_LEFT:
+ panel.highlightFromTabList(false);
+ break;
+ case KeyEvent.VK_RIGHT:
+ panel.highlightFromTabList(true);
+ break;
+ }
+ }
+ };
+ installKeyListener(this, keyListener);
+ pack();
+ positionRelativeTo(positioningComponent);
+ }
+
+ void close() {
+ setVisible(false);
+ dispose();
+ }
+
+ private SearchListModel createTabListModel() {
+ DefaultSearchListModel model = new DefaultSearchListModel();
+
+ List visibleValues = panel.getVisibleTabs();
+
+ model.add(HIDDEN, panel.getHiddenTabs());
+ model.add(VISIBLE, visibleValues);
+
+ return model;
+ }
+
+ private void positionRelativeTo(JComponent component) {
+
+ Rectangle bounds = getBounds();
+
+ // no label implies we are launched from a keyboard event
+ if (component == null) {
+
+ Point centerPoint = WindowUtilities.centerOnComponent(getParent(), this);
+ bounds.setLocation(centerPoint);
+ WindowUtilities.ensureEntirelyOnScreen(getParent(), bounds);
+ setBounds(bounds);
+ return;
+ }
+
+ // show the window just below the label that launched it
+ Point p = component.getLocationOnScreen();
+ int x = p.x;
+ int y = p.y + component.getHeight() + 3;
+ bounds.setLocation(x, y);
+
+ // fixes problem where popup gets clipped when going across screens
+ WindowUtilities.ensureOnScreen(component, bounds);
+ setBounds(bounds);
+ }
+
+ private class TabListRenderer extends GListCellRenderer> {
+
+ public TabListRenderer() {
+ setShouldAlternateRowBackgroundColors(false);
+ }
+
+ @Override
+ protected String getItemText(SearchListEntry value) {
+ return panel.getDisplayName(value.value());
+ }
+
+ @Override
+ public Component getListCellRendererComponent(JList extends SearchListEntry> list,
+ SearchListEntry value, int index, boolean isSelected, boolean hasFocus) {
+ super.getListCellRendererComponent(list, value, index, isSelected, hasFocus);
+
+ if (value.category().equals(HIDDEN)) {
+ setBold();
+ }
+ return this;
+ }
+
+ }
+
+ private void installKeyListener(Container c, KeyListener listener) {
+
+ c.addKeyListener(listener);
+ Component[] children = c.getComponents();
+ for (Component element : children) {
+ if (element instanceof Container) {
+ installKeyListener((Container) element, listener);
+ }
+ else {
+ element.addKeyListener(listener);
+ }
+ }
+ }
+
+}
diff --git a/Ghidra/Features/Base/src/main/resources/images/VCRFastForward.gif b/Ghidra/Framework/Docking/src/main/resources/images/VCRFastForward.gif
similarity index 100%
rename from Ghidra/Features/Base/src/main/resources/images/VCRFastForward.gif
rename to Ghidra/Framework/Docking/src/main/resources/images/VCRFastForward.gif
diff --git a/Ghidra/Features/Base/src/main/resources/images/pinkX.gif b/Ghidra/Framework/Docking/src/main/resources/images/pinkX.gif
similarity index 100%
rename from Ghidra/Features/Base/src/main/resources/images/pinkX.gif
rename to Ghidra/Framework/Docking/src/main/resources/images/pinkX.gif
diff --git a/Ghidra/Features/Base/src/main/resources/images/x.gif b/Ghidra/Framework/Docking/src/main/resources/images/x.gif
similarity index 100%
rename from Ghidra/Features/Base/src/main/resources/images/x.gif
rename to Ghidra/Framework/Docking/src/main/resources/images/x.gif
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
new file mode 100644
index 0000000000..ef0d708816
--- /dev/null
+++ b/Ghidra/Framework/Docking/src/test/java/docking/widgets/tab/GTabPanelTest.java
@@ -0,0 +1,228 @@
+/* ###
+ * 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 docking.widgets.tab;
+
+import static org.junit.Assert.*;
+
+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.*;
+
+import org.junit.*;
+
+import docking.test.AbstractDockingTest;
+
+public class GTabPanelTest extends AbstractDockingTest {
+
+ private GTabPanel gTabPanel;
+ private JFrame parentFrame;
+
+ @Before
+ public void setUp() throws Exception {
+
+ runSwing(() -> {
+ gTabPanel = new GTabPanel("Test");
+ gTabPanel.addTab("One");
+ gTabPanel.addTab("Two");
+ gTabPanel.addTab("Three Three Three");
+
+ JPanel panel = new JPanel();
+ panel.setLayout(new BorderLayout());
+ panel.add(gTabPanel, BorderLayout.NORTH);
+
+ JTextArea textArea = new JTextArea(20, 100);
+ panel.add(textArea, BorderLayout.CENTER);
+
+ parentFrame = new JFrame(GTabPanel.class.getName());
+ parentFrame.getContentPane().add(panel);
+ parentFrame.pack();
+ parentFrame.setVisible(true);
+ parentFrame.setLocation(1000, 200);
+ });
+ }
+
+ @After
+ public void tearDown() {
+ parentFrame.setVisible(false);
+ }
+
+ @Test
+ public void testFirstTabIsSelectedByDefault() {
+ assertEquals("One", getSelectedValue());
+ }
+
+ @Test
+ public void testAddValue() {
+ assertEquals(3, getTabCount());
+ assertEquals("One", getSelectedValue());
+ addValue("Four");
+ assertEquals(4, getTabCount());
+ assertEquals("One", getSelectedValue());
+ assertEquals("Four", getValue(3));
+ }
+
+ @Test
+ public void testSwitchSelected() {
+ setSelectedValue("Two");
+ assertEquals("Two", getSelectedValue());
+ }
+
+ @Test
+ public void testSwitchToInvalidValue() {
+ try {
+ gTabPanel.selectTab("Four");
+ fail("expected exception");
+ }
+ catch (IllegalArgumentException e) {
+ //expected
+ }
+ assertEquals("One", getSelectedValue());
+
+ }
+
+ @Test
+ public void testCloseSelected() {
+ assertEquals(3, getTabCount());
+ assertEquals("One", getSelectedValue());
+ removeTab("One");
+ assertEquals(2, getTabCount());
+ assertEquals("Two", getSelectedValue());
+ }
+
+ @Test
+ public void testSelectedTabIsVisible() {
+ addValue("asdfasfasfdasfasfasfasfasfasfasfasfasfasfasfasfsaasasfassafsasf");
+ addValue("ABCDEFGHIJK");
+ assertFalse(isVisibleTab("ABCDEFGHIJK"));
+ setSelectedValue("ABCDEFGHIJK");
+ assertTrue(isVisibleTab("ABCDEFGHIJK"));
+ setSelectedValue("One");
+ assertFalse(isVisibleTab("ABCDEFGHIJK"));
+ }
+
+ @Test
+ public void testGetHiddenTabs() {
+ List hiddenTabs = getHiddenTabs();
+ assertTrue(hiddenTabs.isEmpty());
+ addValue("asdfasfasfdasfasfasfasfasfasfasfasfasfasfasfasfsaasasfassafsasf");
+ addValue("ABCDEFGHIJK");
+ hiddenTabs = getHiddenTabs();
+ assertEquals(2, hiddenTabs.size());
+ assertTrue(hiddenTabs.contains("ABCDEFGHIJK"));
+ }
+
+ @Test
+ public void testHighlightTab() {
+ assertNull(gTabPanel.getHighlightedTabValue());
+ gTabPanel.highlightTab("Two");
+ assertEquals("Two", gTabPanel.getHighlightedTabValue());
+ }
+
+ @Test
+ public void testSelectedConsumer() {
+ AtomicReference selectedValue = new AtomicReference();
+ Consumer c = s -> selectedValue.set(s);
+ runSwing(() -> gTabPanel.setSelectedTabConsumer(c));
+ setSelectedValue("Two");
+ assertEquals("Two", selectedValue.get());
+ setSelectedValue("One");
+ assertEquals("One", selectedValue.get());
+ }
+
+ @Test
+ public void testRemovedConsumer() {
+ AtomicReference removedValue = new AtomicReference();
+ Consumer c = s -> removedValue.set(s);
+ runSwing(() -> gTabPanel.setRemovedTabConsumer(c));
+ runSwing(() -> gTabPanel.closeTab("Two"));
+ assertEquals("Two", removedValue.get());
+ }
+
+ @Test
+ public void testSetRemoveTabPredicateAcceptsRemove() {
+ AtomicReference removePredicateCallValue = new AtomicReference();
+ Predicate p = s -> {
+ removePredicateCallValue.set(s);
+ return true;
+ };
+ runSwing(() -> gTabPanel.setRemoveTabActionPredicate(p));
+ runSwing(() -> gTabPanel.closeTab("Two"));
+ assertEquals("Two", removePredicateCallValue.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());
+ }
+
+ @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());
+ }
+
+ private List getHiddenTabs() {
+ return runSwing(() -> gTabPanel.getHiddenTabs());
+ }
+
+ private boolean isVisibleTab(String value) {
+ return runSwing(() -> gTabPanel.isVisibleTab(value));
+ }
+
+ private void addValue(String value) {
+ runSwing(() -> gTabPanel.addTab(value));
+ }
+
+ private void setSelectedValue(String value) {
+ runSwing(() -> gTabPanel.selectTab(value));
+ }
+
+ private void removeTab(String value) {
+ runSwing(() -> gTabPanel.removeTab(value));
+ }
+
+ private int getTabCount() {
+ return runSwing(() -> gTabPanel.getTabCount());
+ }
+
+ private String getSelectedValue() {
+ return runSwing(() -> gTabPanel.getSelectedTabValue());
+ }
+
+ private String getValue(int i) {
+ return runSwing(() -> gTabPanel.getTabValues().get(i));
+ }
+}