GP-4379 Created generic Tab Panel component that is accessible and changed the program multitab plugin to use it.

This commit is contained in:
ghidragon 2024-03-27 11:40:04 -04:00
parent 18b7b8ba42
commit 60edf70859
25 changed files with 1740 additions and 1560 deletions

View file

@ -350,7 +350,11 @@ public abstract class AbstractCodeBrowserPlugin<P extends CodeViewerProvider> ex
@Override
public void setNorthComponent(JComponent comp) {
connectedProvider.setNorthComponent(comp);
}
@Override
public void requestFocus() {
connectedProvider.requestFocus();
}
@Override

View file

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

View file

@ -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.
* <p>
* 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<Program> hiddenList;
private List<Program> shownList;
private JList<Program> programList;
private MultiTabPlugin multiTabPlugin;
private DefaultListModel<Program> 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<Program> hiddenList, List<Program> 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<Program> hiddenList, List<Program> shownList) {
this.hiddenList = hiddenList;
this.shownList = shownList;
initListModel();
programList.clearSelection();
}
JList<Program> 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<Program> allDataList = new ArrayList<>();
allDataList.addAll(hiddenList);
allDataList.addAll(shownList);
boolean hasFilter = filterText.trim().length() != 0;
if (hasFilter) {
String lowerCaseFilterText = filterText.toLowerCase();
for (Iterator<Program> 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<Program> {
@Override
protected String getItemText(Program program) {
return multiTabPlugin.getStringUsedInList(program);
}
@Override
public Component getListCellRendererComponent(JList<? extends Program> list, Program value,
int index, boolean isSelected, boolean hasFocus) {
super.getListCellRendererComponent(list, value, index, isSelected, hasFocus);
if (hiddenList.contains(value)) {
setBold();
}
if (isSelected) {
setBackground(list.getSelectionBackground());
setForeground(list.getSelectionForeground());
}
else {
setBackground(list.getBackground());
setForeground(list.getForeground());
}
return this;
}
}
}

View file

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

View file

@ -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<Program> 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<Program> list = findComponent(tabListPopup, SearchList.class);
assertNotNull(list);
ListModel<?> model = list.getModel();
SearchListModel<Program> 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<Program> 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<JPanel> 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;
}
}

View file

@ -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<Program> 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<Program> panel = getTabPanel();
assertEquals(true, isDiffing());
assertEquals(true, isShowingDiff());
@ -850,6 +850,10 @@ public class DiffTest extends DiffTestAdapter {
//==================================================================================================
// Private Methods
//==================================================================================================
@SuppressWarnings("unchecked")
private GTabPanel<Program> 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<Program> panel, Program pgm) {
runSwing(() -> panel.selectTab(pgm));
waitForSwing();
}

View file

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

View file

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

View file

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

View file

@ -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
@ -639,6 +640,15 @@ 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
@ -1096,6 +1106,9 @@ public class DialogComponentProvider
void setDialog(DockingDialog dialog) {
this.dialog = dialog;
if (dialog != null) {
dialog.getAccessibleContext().setAccessibleDescription(accessibleDescription);
}
}
DockingDialog getDialog() {

View file

@ -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<DockingActionIf> localActions,
Set<DockingActionIf> globalActions,
ActionContext context) {
Set<DockingActionIf> 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<DockingActionIf>(model, (a, c) -> actionChosen(a)) {
@Override
protected BiPredicate<DockingActionIf, String> 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);
}

View file

@ -127,14 +127,19 @@ public class DefaultSearchListModel<T> extends AbstractListModel<SearchListEntry
private List<SearchListEntry<T>> getFilteredEntries(BiPredicate<T, String> filter) {
List<SearchListEntry<T>> entries = new ArrayList<>();
for (String category : dataMap.keySet()) {
Iterator<String> it = dataMap.keySet().iterator();
while (it.hasNext()) {
String category = it.next();
List<T> 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<T>(value, category, isFirst, isLast));
boolean isLastInCateogry = list.get(list.size() - 1) == value;
boolean isLastCategory = !it.hasNext();
boolean showSeparator = isLastInCateogry && !isLastCategory;
entries.add(new SearchListEntry<T>(value, category, isFirst, showSeparator));
}
}
return entries;
}

View file

@ -43,6 +43,9 @@ public class SearchList<T> extends JPanel {
private Consumer<T> selectedConsumer = Dummy.consumer();
private ListCellRenderer<SearchListEntry<T>> itemRenderer = new DefaultItemRenderer();
private String currentFilterText;
private boolean showCategories = true;
private boolean singleClickMode = false;
private BiFunction<T, String, String> displayNameFunction = (t, c) -> t.toString();
/**
* Construct a new SearchList given a model and an chosen item callback.
@ -55,8 +58,8 @@ public class SearchList<T> 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<T> extends JPanel {
return textField.getText();
}
/**
* Returns the search list model.
* @return the model
*/
public SearchListModel<T> 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<T> extends JPanel {
return null;
}
public void setSelectedItem(T t) {
ListModel<SearchListEntry<T>> listModel = jList.getModel();
for (int i = 0; i < listModel.getSize(); i++) {
SearchListEntry<T> 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<T> 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<T, String, String> 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<T> 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<SearchListEntry<T>>(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<T> 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<T> extends JPanel {
return width + 10;
}
private void chooseItem() {
public void chooseItem() {
SearchListEntry<T> selectedValue = jList.getSelectedValue();
if (selectedValue != null) {
chosenItemCallback.accept(selectedValue.value(), selectedValue.category());
@ -236,30 +300,32 @@ public class SearchList<T> 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();
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<T> extends JPanel {
@Override
public Component getListCellRendererComponent(JList<? extends SearchListEntry<T>> list,
SearchListEntry<T> value, int index,
boolean isSelected, boolean hasFocus) {
SearchListEntry<T> 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<T> entry = value;
T t = entry.value();
label.setText(t.toString());
@ -308,23 +373,27 @@ public class SearchList<T> 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();
}
}
}
private class SearchListDocumentListener implements DocumentListener {
@ -354,8 +423,12 @@ public class SearchList<T> 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;
}
}

View file

@ -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 <T> the type of list items
*/
public record SearchListEntry<T>(T value, String category, boolean isFirst, boolean isLast) {
public record SearchListEntry<T>(T value, String category, boolean showCategory,
boolean drawSeparator) {
}

View file

@ -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 <T> the type of the tab values
*/
class GTab<T> 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<T> tabPanel;
private T value;
private boolean selected;
private JLabel closeLabel;
private JLabel nameLabel;
GTab(GTabPanel<T> 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);
}
}
}
}

View file

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

View file

@ -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.
* <P>
* 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.
* <P>
* 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.
* <P>
* 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 <T> The type of values in the tab panel.
*/
public class GTabPanel<T> extends JPanel {
private T selectedValue;
private T highlightedValue;
private boolean ignoreFocusLost;
private TabListPopup<T> tabList;
private String tabTypeName;
private Set<T> allValues = new LinkedHashSet<>();
private List<GTab<T>> allTabs = new ArrayList<>();
private HiddenValuesButton hiddenValuesControl = new HiddenValuesButton(this);
private Function<T, String> nameFunction = v -> v.toString();
private Function<T, Icon> iconFunction = Dummy.function();
private Function<T, String> toolTipFunction = Dummy.function();
private Predicate<T> removeTabPredicate = Dummy.predicate();
private Consumer<T> selectedTabConsumer = Dummy.consumer();
private Consumer<T> removedTabConsumer = Dummy.consumer();
/**
* 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<T> 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<T> 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<T> 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<T> 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<T> getHiddenTabs() {
Set<T> hiddenValues = new LinkedHashSet<T>(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<T> 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<T, String> 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<T, Icon> 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<T, String> 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<T> removeTabPredicate) {
this.removeTabPredicate = removeTabPredicate;
}
/**
* Sets the consumer to be notified if a tab value is removed.
* @param removedTabConsumer the consumer to be notified when tab values are removed
*/
public void setRemovedTabConsumer(Consumer<T> removedTabConsumer) {
this.removedTabConsumer = removedTabConsumer;
}
/**
* 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<T> 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<T>(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<T> 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<T> selectedTab = new GTab<T>(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<T> 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<T> 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<T> tab = new GTab<T>(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<T> 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<T> tab : allTabs) {
if (tab.getValue().equals(value)) {
return tab;
}
}
return null;
}
}

View file

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

View file

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

View file

@ -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 <T> the value types
*/
public class TabListPopup<T> extends JDialog {
private static final String HIDDEN = "Hidden";
private static final String VISIBLE = "Visible";
private GTabPanel<T> panel;
private SearchList<T> searchList;
TabListPopup(GTabPanel<T> 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<T> tabListModel = createTabListModel();
searchList = new SearchList<T>(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<T> createTabListModel() {
DefaultSearchListModel<T> model = new DefaultSearchListModel<T>();
List<T> 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<SearchListEntry<T>> {
public TabListRenderer() {
setShouldAlternateRowBackgroundColors(false);
}
@Override
protected String getItemText(SearchListEntry<T> value) {
return panel.getDisplayName(value.value());
}
@Override
public Component getListCellRendererComponent(JList<? extends SearchListEntry<T>> list,
SearchListEntry<T> 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);
}
}
}
}

View file

Before

Width:  |  Height:  |  Size: 138 B

After

Width:  |  Height:  |  Size: 138 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 88 B

After

Width:  |  Height:  |  Size: 88 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 88 B

After

Width:  |  Height:  |  Size: 88 B

Before After
Before After

View file

@ -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<String> gTabPanel;
private JFrame parentFrame;
@Before
public void setUp() throws Exception {
runSwing(() -> {
gTabPanel = new GTabPanel<String>("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<String> 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<String> selectedValue = new AtomicReference<String>();
Consumer<String> 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<String> removedValue = new AtomicReference<String>();
Consumer<String> c = s -> removedValue.set(s);
runSwing(() -> gTabPanel.setRemovedTabConsumer(c));
runSwing(() -> gTabPanel.closeTab("Two"));
assertEquals("Two", removedValue.get());
}
@Test
public void testSetRemoveTabPredicateAcceptsRemove() {
AtomicReference<String> removePredicateCallValue = new AtomicReference<String>();
Predicate<String> 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<String> removePredicateCallValue = new AtomicReference<String>();
Predicate<String> p = s -> {
removePredicateCallValue.set(s);
return false;
};
runSwing(() -> gTabPanel.setRemoveTabActionPredicate(p));
runSwing(() -> gTabPanel.closeTab("Two"));
assertEquals("Two", removePredicateCallValue.get());
assertEquals(3, getTabCount());
}
@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<String> 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));
}
}