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