mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-03 17:59:46 +02:00
GP-4379 Created generic Tab Panel component that is accessible and changed the program multitab plugin to use it.
This commit is contained in:
parent
18b7b8ba42
commit
60edf70859
25 changed files with 1740 additions and 1560 deletions
|
@ -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
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Before Width: | Height: | Size: 138 B After Width: | Height: | Size: 138 B |
Before Width: | Height: | Size: 88 B After Width: | Height: | Size: 88 B |
Before Width: | Height: | Size: 88 B After Width: | Height: | Size: 88 B |
|
@ -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));
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue