mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-04 18:29:37 +02:00
GP-4227 fixed several focus traversal issues
This commit is contained in:
parent
59972bb9f1
commit
3d333c071b
18 changed files with 1448 additions and 150 deletions
|
@ -365,6 +365,7 @@ src/main/help/help/topics/Intro/images/Empty_ghidra.png||GHIDRA||||END|
|
|||
src/main/help/help/topics/Intro/images/Err_Dialog.png||GHIDRA||||END|
|
||||
src/main/help/help/topics/Intro/images/Open_ghidra.png||GHIDRA||||END|
|
||||
src/main/help/help/topics/Intro/images/Simple_err_dialog.png||GHIDRA||||END|
|
||||
src/main/help/help/topics/KeyboardNavigation/KeyboardNavigation.htm||GHIDRA||||END|
|
||||
src/main/help/help/topics/LabelMgrPlugin/FieldNames.htm||GHIDRA||||END|
|
||||
src/main/help/help/topics/LabelMgrPlugin/Labels.htm||GHIDRA||||END|
|
||||
src/main/help/help/topics/LabelMgrPlugin/images/AddLabel.png||GHIDRA||||END|
|
||||
|
|
|
@ -375,6 +375,7 @@
|
|||
|
||||
</tocdef> <!-- End Ghidra Support -->
|
||||
|
||||
<tocdef id="Keyboard Navigation" sortgroup="gg" text="Keyboard Navigation" target="help/topics/KeyboardNavigation/KeyboardNavigation.htm"/>
|
||||
<tocdef id="Undo/Redo" sortgroup="h" text="Undo/Redo" target="help/topics/Tool/Undo_Redo.htm" />
|
||||
<tocdef id="Glossary" sortgroup="i" text="Glossary" target="help/topics/Glossary/glossary.htm" />
|
||||
<tocdef id="What's New" sortgroup="j" text="What's New" target="docs/WhatsNew.html" />
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
|
||||
|
||||
<HTML>
|
||||
<HEAD>
|
||||
<META name="generator" content=
|
||||
"HTML Tidy for Java (vers. 2009-12-01), see jtidy.sourceforge.net">
|
||||
<META http-equiv="Content-Language" content="en-us">
|
||||
<META http-equiv="Content-Type" content="text/html; charset=windows-1252">
|
||||
|
||||
<TITLE>Keyboard Navigation</TITLE>
|
||||
<LINK rel="stylesheet" type="text/css" href="help/shared/DefaultStyle.css">
|
||||
</HEAD>
|
||||
|
||||
<BODY>
|
||||
<BLOCKQUOTE>
|
||||
<H1>Keyboard Navigation</H1>
|
||||
<BLOCKQUOTE>
|
||||
<H2>Component Traversal</H2>
|
||||
<P>
|
||||
Ghidra supports standard keyboard navigation using for traversing component focus cycles within
|
||||
each window. In general, TAB and <CTRL> TAB will transfer focus to the next component. And
|
||||
<SHIFT> TAB and <CTRL><SHIFT>TAB will transfer focus to the previous component
|
||||
in the cycle. TAB and <SHIFT>TAB do not always work as they are sometimes used by
|
||||
individual components such as text components, but the <CTRL> versions should work
|
||||
universally.</P>
|
||||
<P>
|
||||
</P>Ghidra also provides some handy shortcut keys for navigation:
|
||||
<UL>
|
||||
<LI><CTRL>F3 - Transfer focus to the next window or dialog.
|
||||
<LI><CTRL><SHIFT>F3 - Transfer focus to the previous window or dialog.
|
||||
<LI><CTRL>J - Transfer focus (Jump) to the next component provider (titled component).
|
||||
<LI><CTRL><SHIFT>J - Transfer focus (Jump) to the previous component provider.
|
||||
</UL>
|
||||
<H2>Actions</H2>
|
||||
<P>Global menus can be reached using the various accelerator keys such as <ALT>F to access
|
||||
the File menu.</P>
|
||||
<P>
|
||||
Context menus can be invoked using the <SHIFT>F10 key or the dedicated context menu key
|
||||
available on some keyboards.</P>
|
||||
<P>
|
||||
Toolbar actions currently can't be accessed via the keyboard unless a keybinding is assigned to
|
||||
them.
|
||||
|
||||
</P>
|
||||
</BLOCKQUOTE>
|
||||
</BODY>
|
||||
</HTML>
|
|
@ -151,7 +151,8 @@ public class SearchTextPlugin extends ProgramPlugin implements OptionsChangeList
|
|||
if (result == null) {
|
||||
searchDialog.setStatusText("Not found");
|
||||
}
|
||||
else if (result.programLocation().equals(currentLocation)) {
|
||||
else if (result.programLocation()
|
||||
.equals(currentLocation)) {
|
||||
searchNext(searchTask.getProgram(), searchNavigatable, textSearcher);
|
||||
}
|
||||
else {
|
||||
|
@ -396,7 +397,6 @@ public class SearchTextPlugin extends ProgramPlugin implements OptionsChangeList
|
|||
new ActionBuilder("Repeat Text Search", getName())
|
||||
.menuPath("&Search", "Repeat Text Search")
|
||||
.menuGroup("search", subGroup)
|
||||
.keyBinding("ctrl shift F3")
|
||||
.description(DESCRIPTION)
|
||||
.helpLocation(new HelpLocation(HelpTopics.SEARCH, "Repeat Text Search"))
|
||||
.withContext(NavigatableActionContext.class, true)
|
||||
|
@ -468,7 +468,8 @@ public class SearchTextPlugin extends ProgramPlugin implements OptionsChangeList
|
|||
String textSelection = navigatable.getTextSelection();
|
||||
ProgramLocation location = navigatable.getLocation();
|
||||
Address address = location.getAddress();
|
||||
Listing listing = context.getProgram().getListing();
|
||||
Listing listing = context.getProgram()
|
||||
.getListing();
|
||||
CodeUnit codeUnit = listing.getCodeUnitAt(address);
|
||||
boolean isInstruction = false;
|
||||
if (textSelection != null) {
|
||||
|
@ -582,7 +583,8 @@ public class SearchTextPlugin extends ProgramPlugin implements OptionsChangeList
|
|||
|
||||
// must have completed too fast for the provider to be set; try something cute
|
||||
Component focusOwner =
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||
.getFocusOwner();
|
||||
return focusOwner; // assume this IS the provider
|
||||
}
|
||||
|
||||
|
@ -627,7 +629,8 @@ public class SearchTextPlugin extends ProgramPlugin implements OptionsChangeList
|
|||
@Override
|
||||
public Highlight[] createHighlights(String text, ListingField field, int cursorTextOffset) {
|
||||
|
||||
Class<? extends FieldFactory> fieldFactoryClass = field.getFieldFactory().getClass();
|
||||
Class<? extends FieldFactory> fieldFactoryClass = field.getFieldFactory()
|
||||
.getClass();
|
||||
|
||||
if (!doHighlight) {
|
||||
return NO_HIGHLIGHTS;
|
||||
|
@ -650,7 +653,8 @@ public class SearchTextPlugin extends ProgramPlugin implements OptionsChangeList
|
|||
return getAllHighlights(text, cursorTextOffset);
|
||||
}
|
||||
|
||||
Address address = searchResult.programLocation().getAddress();
|
||||
Address address = searchResult.programLocation()
|
||||
.getAddress();
|
||||
ProxyObj<?> proxy = field.getProxy();
|
||||
if (proxy.contains(address)) {
|
||||
return getSingleSearchHighlight(text, field, cursorTextOffset);
|
||||
|
@ -742,7 +746,8 @@ public class SearchTextPlugin extends ProgramPlugin implements OptionsChangeList
|
|||
return true;
|
||||
}
|
||||
|
||||
Class<? extends FieldFactory> factoryClass = field.getFieldFactory().getClass();
|
||||
Class<? extends FieldFactory> factoryClass = field.getFieldFactory()
|
||||
.getClass();
|
||||
if (searchOptions.searchComments()) {
|
||||
if (factoryClass == PreCommentFieldFactory.class ||
|
||||
factoryClass == PlateFieldFactory.class ||
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/* ###
|
||||
* 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.debug;
|
||||
|
||||
import docking.action.DockingAction;
|
||||
import docking.action.builder.ActionBuilder;
|
||||
import ghidra.app.DeveloperPluginPackage;
|
||||
import ghidra.app.plugin.PluginCategoryNames;
|
||||
import ghidra.docking.util.ComponentInfoDialog;
|
||||
import ghidra.framework.plugintool.*;
|
||||
import ghidra.framework.plugintool.util.PluginStatus;
|
||||
import help.Help;
|
||||
import help.HelpService;
|
||||
|
||||
/**
|
||||
* Plugin to display information about components in the application
|
||||
*/
|
||||
|
||||
//@formatter:off
|
||||
@PluginInfo(
|
||||
status = PluginStatus.RELEASED,
|
||||
packageName = DeveloperPluginPackage.NAME,
|
||||
category = PluginCategoryNames.TESTING,
|
||||
shortDescription = "Show component information",
|
||||
description = "This plugin is a debug aid that displays " +
|
||||
"a table of information about the components in the application."
|
||||
)
|
||||
//@formatter:on
|
||||
public class ComponentInfoPlugin extends Plugin {
|
||||
|
||||
public ComponentInfoPlugin(PluginTool tool) {
|
||||
super(tool);
|
||||
|
||||
//@formatter:off
|
||||
DockingAction action =
|
||||
new ActionBuilder("Component Display", getName())
|
||||
.menuPath("Help", "Show Diagnostic Component Information")
|
||||
.onAction(e -> showComponentDialog())
|
||||
.buildAndInstall(tool);
|
||||
//@formatter:on
|
||||
|
||||
// Note: this plugin is in the 'Developer' category and as such does not need help
|
||||
HelpService helpService = Help.getHelpService();
|
||||
helpService.excludeFromHelp(action);
|
||||
}
|
||||
|
||||
private void showComponentDialog() {
|
||||
tool.showDialog(new ComponentInfoDialog());
|
||||
}
|
||||
}
|
|
@ -20,10 +20,10 @@ import java.awt.event.*;
|
|||
import java.util.*;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.ChangeListener;
|
||||
|
||||
import org.jdom.Element;
|
||||
|
||||
import docking.actions.KeyBindingUtils;
|
||||
import docking.widgets.OptionDialog;
|
||||
import docking.widgets.tabbedpane.DockingTabRenderer;
|
||||
import ghidra.util.*;
|
||||
|
@ -42,59 +42,47 @@ class ComponentNode extends Node {
|
|||
private JComponent comp;
|
||||
private boolean isDisposed;
|
||||
|
||||
// keep track of top ComponentWindowingPlaceholder
|
||||
private ChangeListener tabbedPaneChangeListener = e -> {
|
||||
Component selectedComponent = ((JTabbedPane) comp).getSelectedComponent();
|
||||
for (ComponentPlaceholder placeholder : windowPlaceholders) {
|
||||
if (placeholder.getComponent() == selectedComponent) {
|
||||
top = placeholder;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Swing.runLater(() -> {
|
||||
if (top != null) {
|
||||
top.requestFocus();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructs a new component node with the given docking windows manager.
|
||||
* @param mgr the docking windows manager that this node belongs to.
|
||||
* @param windowManager the docking windows manager that this node belongs to.
|
||||
*/
|
||||
ComponentNode(DockingWindowManager mgr) {
|
||||
super(mgr);
|
||||
ComponentNode(DockingWindowManager windowManager) {
|
||||
super(windowManager);
|
||||
windowPlaceholders = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new component node from the given xml element.
|
||||
* @param elem the xml element describing the configuration of this node.
|
||||
* @param mgr the docking windows manager
|
||||
* @param element the xml element describing the configuration of this node.
|
||||
* @param windowManager the docking windows manager
|
||||
* @param parent the parent node for this node.
|
||||
* @param restoredPlaceholders the list into which any restored placeholders will be placed
|
||||
*/
|
||||
ComponentNode(Element elem, DockingWindowManager mgr, Node parent,
|
||||
ComponentNode(Element element, DockingWindowManager windowManager, Node parent,
|
||||
List<ComponentPlaceholder> restoredPlaceholders) {
|
||||
super(mgr);
|
||||
super(windowManager);
|
||||
|
||||
this.parent = parent;
|
||||
windowPlaceholders = new ArrayList<>();
|
||||
|
||||
int topIndex = Integer.parseInt(elem.getAttributeValue("TOP_INFO"));
|
||||
int topIndex = Integer.parseInt(element.getAttributeValue("TOP_INFO"));
|
||||
|
||||
List<?> children = element.getChildren();
|
||||
Iterator<?> it = children.iterator();
|
||||
|
||||
Iterator<?> it = elem.getChildren().iterator();
|
||||
while (it.hasNext()) {
|
||||
Element e = (Element) it.next();
|
||||
String name = e.getAttributeValue("NAME");
|
||||
String owner = e.getAttributeValue("OWNER");
|
||||
String title = e.getAttributeValue("TITLE");
|
||||
String group = e.getAttributeValue("GROUP");
|
||||
if (group == null || group.trim().isEmpty()) {
|
||||
if (group == null || group.trim()
|
||||
.isEmpty()) {
|
||||
group = ComponentProvider.DEFAULT_WINDOW_GROUP;
|
||||
}
|
||||
|
||||
boolean isActive = Boolean.valueOf(e.getAttributeValue("ACTIVE")).booleanValue();
|
||||
boolean isActive = Boolean.valueOf(e.getAttributeValue("ACTIVE"))
|
||||
.booleanValue();
|
||||
|
||||
long uniqueID = getUniqueID(e, 0);
|
||||
|
||||
|
@ -117,6 +105,19 @@ class ComponentNode extends Node {
|
|||
}
|
||||
}
|
||||
|
||||
private void focusComponent(Component component) {
|
||||
if (component == null) {
|
||||
return;
|
||||
}
|
||||
ComponentPlaceholder placeholder = getPlaceHolderForComponent(component);
|
||||
if (placeholder == null) {
|
||||
return;
|
||||
}
|
||||
Swing.runLater(() -> {
|
||||
placeholder.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
private boolean containsPlaceholder(ComponentPlaceholder placeholder) {
|
||||
// Note: we purposely didn't override equals here, as other code here relies on the default
|
||||
// equals() implementation to locate placeholders
|
||||
|
@ -130,10 +131,14 @@ class ComponentNode extends Node {
|
|||
String name = placeholder.getName();
|
||||
String title = placeholder.getTitle();
|
||||
for (ComponentPlaceholder existingPlaceholder : windowPlaceholders) {
|
||||
if (existingPlaceholder.getOwner().equals(owner) &&
|
||||
existingPlaceholder.getName().equals(name) &&
|
||||
existingPlaceholder.getGroup().equals(group) &&
|
||||
existingPlaceholder.getTitle().equals(title)) {
|
||||
if (existingPlaceholder.getOwner()
|
||||
.equals(owner) &&
|
||||
existingPlaceholder.getName()
|
||||
.equals(name) &&
|
||||
existingPlaceholder.getGroup()
|
||||
.equals(group) &&
|
||||
existingPlaceholder.getTitle()
|
||||
.equals(title)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -250,8 +255,7 @@ class ComponentNode extends Node {
|
|||
JComponent getComponent() {
|
||||
|
||||
if (isDisposed) {
|
||||
throw new AssertException(
|
||||
"Attempted to reuse a disposed component window node");
|
||||
throw new AssertException("Attempted to reuse a disposed component window node");
|
||||
}
|
||||
|
||||
if (!invalid) {
|
||||
|
@ -259,7 +263,6 @@ class ComponentNode extends Node {
|
|||
}
|
||||
|
||||
if (comp instanceof JTabbedPane) {
|
||||
((JTabbedPane) comp).removeChangeListener(tabbedPaneChangeListener);
|
||||
comp.removeAll();
|
||||
}
|
||||
comp = null;
|
||||
|
@ -287,10 +290,26 @@ class ComponentNode extends Node {
|
|||
installRenameMenu(top, null);
|
||||
}
|
||||
else if (count > 1) {
|
||||
JTabbedPane pane =
|
||||
JTabbedPane tabbedPane =
|
||||
new JTabbedPane(SwingConstants.BOTTOM, JTabbedPane.SCROLL_TAB_LAYOUT);
|
||||
comp = pane;
|
||||
int topIndex = 0;
|
||||
setupFocusUpdateListeners(tabbedPane);
|
||||
comp = tabbedPane;
|
||||
|
||||
int activeIndex = addComponentsToTabbedPane(activeComponents, tabbedPane);
|
||||
|
||||
DockableComponent activeComp =
|
||||
(DockableComponent) tabbedPane.getComponentAt(activeIndex);
|
||||
top = activeComp.getComponentWindowingPlaceholder();
|
||||
tabbedPane.setSelectedComponent(activeComp);
|
||||
}
|
||||
invalid = false;
|
||||
return comp;
|
||||
}
|
||||
|
||||
private int addComponentsToTabbedPane(List<ComponentPlaceholder> activeComponents,
|
||||
JTabbedPane tabbedPane) {
|
||||
int count = activeComponents.size();
|
||||
int activeIndex = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
ComponentPlaceholder placeholder = activeComponents.get(i);
|
||||
DockableComponent c = placeholder.getComponent();
|
||||
|
@ -299,30 +318,58 @@ class ComponentNode extends Node {
|
|||
String tabText = placeholder.getTabText();
|
||||
|
||||
final DockableComponent component = placeholder.getComponent();
|
||||
pane.add(component, title);
|
||||
tabbedPane.add(component, title);
|
||||
|
||||
DockingTabRenderer tabRenderer =
|
||||
createTabRenderer(pane, placeholder, title, tabText, component);
|
||||
createTabRenderer(tabbedPane, placeholder, title, tabText, component);
|
||||
|
||||
c.installDragDropTarget(pane);
|
||||
c.installDragDropTarget(tabbedPane);
|
||||
|
||||
pane.setTabComponentAt(i, tabRenderer);
|
||||
tabbedPane.setTabComponentAt(i, tabRenderer);
|
||||
Icon icon = placeholder.getIcon();
|
||||
if (icon != null) {
|
||||
tabRenderer.setIcon(icon);
|
||||
}
|
||||
|
||||
if (placeholder == top) {
|
||||
topIndex = i;
|
||||
activeIndex = i;
|
||||
}
|
||||
}
|
||||
DockableComponent activeComp = (DockableComponent) pane.getComponentAt(topIndex);
|
||||
top = activeComp.getComponentWindowingPlaceholder();
|
||||
pane.setSelectedComponent(activeComp);
|
||||
pane.addChangeListener(tabbedPaneChangeListener);
|
||||
return activeIndex;
|
||||
}
|
||||
invalid = false;
|
||||
return comp;
|
||||
|
||||
private void setupFocusUpdateListeners(JTabbedPane tabbedPane) {
|
||||
registerSpacebarActionToTransferFocusToTabbedComponent(tabbedPane);
|
||||
tabbedPane.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
int index = tabbedPane.indexAtLocation(e.getX(), e.getY());
|
||||
Component selectedComponent = tabbedPane.getComponentAt(index);
|
||||
focusComponent(selectedComponent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a keybinding that binds VK_SPACE to an action that transfers focus the
|
||||
* component represented by the currently focussed tab of a JTabbedPane. This is so that
|
||||
* when using keyboard focus traversal and the JTabbedPane get focus such that using arrow
|
||||
* keys allows the user to switch tabs, pressing the spacebar will transfer control to the
|
||||
* currently focused tab.
|
||||
* @param tabbedPane the JTabbedPane to add this spacebar keybinding.
|
||||
*/
|
||||
private void registerSpacebarActionToTransferFocusToTabbedComponent(JTabbedPane tabbedPane) {
|
||||
Action focusAction = new AbstractAction("Focus") {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent ev) {
|
||||
Component selectedComponent = tabbedPane.getSelectedComponent();
|
||||
focusComponent(selectedComponent);
|
||||
}
|
||||
};
|
||||
|
||||
KeyBindingUtils.registerAction(tabbedPane, KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0),
|
||||
focusAction, JComponent.WHEN_FOCUSED);
|
||||
|
||||
}
|
||||
|
||||
private DockingTabRenderer createTabRenderer(JTabbedPane pane, ComponentPlaceholder placeholder,
|
||||
|
|
|
@ -616,7 +616,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
* @param component the component for which to find a provider
|
||||
* @return the provider; null if the component is not the child of a provider
|
||||
*/
|
||||
private ComponentProvider getComponentProvider(Component component) {
|
||||
public ComponentProvider getComponentProvider(Component component) {
|
||||
Set<ComponentProvider> providers = placeholderManager.getActiveProviders();
|
||||
for (ComponentProvider provider : providers) {
|
||||
JComponent providerComponent = provider.getComponent();
|
||||
|
@ -1189,7 +1189,8 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
return;
|
||||
}
|
||||
|
||||
tool.getToolActions().removeActions(DOCKING_WINDOWS_OWNER);
|
||||
tool.getToolActions()
|
||||
.removeActions(DOCKING_WINDOWS_OWNER);
|
||||
|
||||
Map<String, List<ComponentPlaceholder>> permanentMap =
|
||||
LazyMap.lazyMap(new HashMap<>(), menuName -> new ArrayList<>());
|
||||
|
@ -1205,10 +1206,12 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
|
||||
String subMenuName = provider.getWindowSubMenuName();
|
||||
if (provider.isTransient() && !provider.isSnapshot()) {
|
||||
transientMap.get(subMenuName).add(placeholder);
|
||||
transientMap.get(subMenuName)
|
||||
.add(placeholder);
|
||||
}
|
||||
else {
|
||||
permanentMap.get(subMenuName).add(placeholder);
|
||||
permanentMap.get(subMenuName)
|
||||
.add(placeholder);
|
||||
}
|
||||
}
|
||||
promoteSingleMenuGroups(permanentMap);
|
||||
|
@ -1222,7 +1225,8 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
}
|
||||
|
||||
private boolean isWindowMenuShowing() {
|
||||
MenuElement[] selectedPath = MenuSelectionManager.defaultManager().getSelectedPath();
|
||||
MenuElement[] selectedPath = MenuSelectionManager.defaultManager()
|
||||
.getSelectedPath();
|
||||
if (selectedPath == null || selectedPath.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
@ -1278,7 +1282,8 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
|
||||
List<ComponentPlaceholder> list = lazyMap.get(key);
|
||||
if (list.size() == 1) {
|
||||
lazyMap.get(null /*submenu name*/).add(list.get(0));
|
||||
lazyMap.get(null /*submenu name*/)
|
||||
.add(list.get(0));
|
||||
lazyMap.remove(key);
|
||||
}
|
||||
}
|
||||
|
@ -1417,7 +1422,10 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
for (Entry<ComponentProvider, ComponentPlaceholder> entry : entrySet) {
|
||||
ComponentProvider provider = entry.getKey();
|
||||
ComponentPlaceholder placeholder = entry.getValue();
|
||||
if (provider.getOwner().equals(focusOwner) && provider.getName().equals(focusName)) {
|
||||
if (provider.getOwner()
|
||||
.equals(focusOwner) &&
|
||||
provider.getName()
|
||||
.equals(focusName)) {
|
||||
focusReplacement = placeholder;
|
||||
break; // found one!
|
||||
}
|
||||
|
@ -1473,6 +1481,10 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
return getActivePlaceholder(defaultProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the docking window manager's notion of which component placeholder is focused. This
|
||||
* is used when a component is removed or component placeholders are rebuilt.
|
||||
*/
|
||||
private void clearFocusedComponent() {
|
||||
if (focusedPlaceholder != null) {
|
||||
lastFocusedPlaceholders.remove(focusedPlaceholder);
|
||||
|
@ -1488,6 +1500,20 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
setNextFocusPlaceholder(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the docking window manager's notion of the active provider. This is used
|
||||
* when a component that is not contained within a dockable component gets focus
|
||||
* (e.g., JTabbedPanes for stacked components).
|
||||
*/
|
||||
private void deactivateFocusedComponent() {
|
||||
if (focusedPlaceholder != null) {
|
||||
focusedPlaceholder.setSelected(false);
|
||||
focusedPlaceholder = null;
|
||||
}
|
||||
// also clear any pending focus transfers
|
||||
setNextFocusPlaceholder(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked by associated docking windows when they become active or inactive
|
||||
*
|
||||
|
@ -1526,7 +1552,8 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
@Override
|
||||
public void propertyChange(PropertyChangeEvent evt) {
|
||||
|
||||
Window win = KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow();
|
||||
Window win = KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||
.getActiveWindow();
|
||||
if (!isMyWindow(win)) {
|
||||
return;
|
||||
}
|
||||
|
@ -1535,6 +1562,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
|
||||
// adjust the focus if no component within the window has focus
|
||||
Component newFocusComponent = (Component) evt.getNewValue();
|
||||
|
||||
if (newFocusComponent == null) {
|
||||
return; // we'll get called again with the correct value
|
||||
}
|
||||
|
@ -1574,7 +1602,19 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
return false;
|
||||
}
|
||||
|
||||
// Transfer focus to one of our component providers when a component gets focus that is
|
||||
// not contained in a dockable component provider. This keeps unexpected components
|
||||
// from getting focus as the user navigates the application from the keyboard.
|
||||
if (!SwingUtilities.isDescendingFrom(newFocusComponent, dockableComponent)) {
|
||||
|
||||
// We make an exception for JTabbedPane as that is the component we use to stack
|
||||
// components and users need to be able to select and activate tabs when using the
|
||||
// keyboard focus traversal
|
||||
if (newFocusComponent instanceof JTabbedPane) {
|
||||
deactivateFocusedComponent();
|
||||
return false;
|
||||
}
|
||||
|
||||
dockableComponent.requestFocus();
|
||||
return false;
|
||||
}
|
||||
|
@ -1653,7 +1693,8 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
toolPreferencesElement.getChildren(PreferenceState.PREFERENCE_STATE_NAME);
|
||||
for (Object name : children) {
|
||||
Element preferencesElement = (Element) name;
|
||||
preferenceStateMap.put(preferencesElement.getAttribute("NAME").getValue(),
|
||||
preferenceStateMap.put(preferencesElement.getAttribute("NAME")
|
||||
.getValue(),
|
||||
new PreferenceState(preferencesElement));
|
||||
}
|
||||
}
|
||||
|
@ -2142,7 +2183,8 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
|
||||
setStatusText(text);
|
||||
if (beep) {
|
||||
Toolkit.getDefaultToolkit().beep();
|
||||
Toolkit.getDefaultToolkit()
|
||||
.beep();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2159,7 +2201,8 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
* A convenience method to make an attention-grabbing noise to the user
|
||||
*/
|
||||
public static void beep() {
|
||||
Toolkit.getDefaultToolkit().beep();
|
||||
Toolkit.getDefaultToolkit()
|
||||
.beep();
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -2231,7 +2274,8 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
if (includeMain) {
|
||||
winList.add(root.getMainWindow());
|
||||
}
|
||||
Iterator<DetachedWindowNode> it = root.getDetachedWindows().iterator();
|
||||
Iterator<DetachedWindowNode> it = root.getDetachedWindows()
|
||||
.iterator();
|
||||
while (it.hasNext()) {
|
||||
DetachedWindowNode node = it.next();
|
||||
Window win = node.getWindow();
|
||||
|
@ -2406,7 +2450,8 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
|
|||
defaultContextProviderMap.entrySet();
|
||||
|
||||
for (Entry<Class<? extends ActionContext>, ActionContextProvider> entry : entrySet) {
|
||||
contextMap.put(entry.getKey(), entry.getValue().getActionContext(null));
|
||||
contextMap.put(entry.getKey(), entry.getValue()
|
||||
.getActionContext(null));
|
||||
}
|
||||
return contextMap;
|
||||
}
|
||||
|
|
|
@ -46,8 +46,8 @@ public class StatusBar extends JPanel {
|
|||
private static final Border STATUS_BORDER = BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createLoweredBevelBorder(), BorderFactory.createEmptyBorder(1, 2, 1, 2));
|
||||
|
||||
private static final Border STATUS_ITEM_BORDER = BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createEmptyBorder(0, 3, 0, 0), STATUS_BORDER);
|
||||
private static final Border STATUS_ITEM_BORDER = BorderFactory
|
||||
.createCompoundBorder(BorderFactory.createEmptyBorder(0, 3, 0, 0), STATUS_BORDER);
|
||||
|
||||
private static final int STATUS_BAR_GAP = 3;
|
||||
private static final int MESSAGE_QUEUE_MAX_SIZE = 10;
|
||||
|
@ -124,6 +124,14 @@ public class StatusBar extends JPanel {
|
|||
button.addActionListener(e -> callback.run());
|
||||
button.setToolTipText("Press to show the primary application window");
|
||||
|
||||
// We currently don't support components outside of DockingComponents (Except for
|
||||
// JTabbedPanes) getting focus. If this button were to get focus via keyboard traversal, the
|
||||
// DockingWindowManager would "fix" the focus and put it back to the last DockingComponent,
|
||||
// effectively creating a dead end for keyboard traversal.
|
||||
// Also, the button being focusable could possibly be confusing since this button is about
|
||||
// changing focus to the front-end.
|
||||
button.setFocusable(false);
|
||||
|
||||
homeButtonPanel.add(button);
|
||||
}
|
||||
|
||||
|
@ -256,7 +264,8 @@ public class StatusBar extends JPanel {
|
|||
}
|
||||
|
||||
private void addMessageToQueue(String message) {
|
||||
if (message != null && message.trim().length() != 0) {
|
||||
if (message != null && message.trim()
|
||||
.length() != 0) {
|
||||
if (message.endsWith("\n")) {
|
||||
message = message.substring(0, message.length() - 1);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
/* ###
|
||||
* 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.action;
|
||||
|
||||
import java.awt.KeyboardFocusManager;
|
||||
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import docking.ActionContext;
|
||||
import docking.DockingWindowManager;
|
||||
|
||||
/**
|
||||
* Action for global focus traversal.
|
||||
* <P>
|
||||
* The Java focus system suggests that both TAB and <CTRL> TAB move the focus to the next
|
||||
* component in the focus traversal cycle. It also suggests that both <SHIFT> TAB and
|
||||
* <CTRL><SHIFT> TAB move the focus to the previous component in the focus traversal
|
||||
* cycle.
|
||||
* <P>
|
||||
* However, the implementation across Look And Feels and some components within those Look and
|
||||
* Feels are inconsistent with regards the <CTRL> version of these keys. Rather than try
|
||||
* and find and fix all the inconsistencies across all components
|
||||
* and Look And Feels, we process the <CTRL> version of focus traversal using global
|
||||
* reserved actions. We can't take the same approach for the base TAB and <SHIFT> TAB because
|
||||
* these really do need to be component specific as some components use these keys for some other
|
||||
* purpose other than focus traversal.
|
||||
* <P>
|
||||
* This global processing of <CTRL> TAB and <CTRL><SHIFT> TAB can be disabled by
|
||||
* setting the system property {@link #GLOBAL_FOCUS_TRAVERSAL_PROPERTY} to "false"
|
||||
*/
|
||||
|
||||
public class GlobalFocusTraversalAction extends DockingAction {
|
||||
private static final String GLOBAL_FOCUS_TRAVERSAL_PROPERTY =
|
||||
"docking.global.focus.traversal.key.enabled";
|
||||
|
||||
private boolean forward;
|
||||
|
||||
public GlobalFocusTraversalAction(KeyStroke keybinding, boolean forward) {
|
||||
super(forward ? "Next Component" : "Previous Component",
|
||||
DockingWindowManager.DOCKING_WINDOWS_OWNER);
|
||||
|
||||
this.forward = forward;
|
||||
createReservedKeyBinding(keybinding);
|
||||
setEnabled(isGlobalFocusTraversalEnabled());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionContext context) {
|
||||
KeyboardFocusManager focusManager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
|
||||
if (forward) {
|
||||
focusManager.focusNextComponent();
|
||||
}
|
||||
else {
|
||||
focusManager.focusPreviousComponent();
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isGlobalFocusTraversalEnabled() {
|
||||
String property =
|
||||
System.getProperty(GLOBAL_FOCUS_TRAVERSAL_PROPERTY, Boolean.TRUE.toString());
|
||||
return Boolean.parseBoolean(property);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
/* ###
|
||||
* 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.action;
|
||||
|
||||
import java.awt.KeyboardFocusManager;
|
||||
import java.awt.Window;
|
||||
|
||||
import javax.swing.KeyStroke;
|
||||
|
||||
import docking.ActionContext;
|
||||
import docking.DockingWindowManager;
|
||||
|
||||
/**
|
||||
* Action for transferring focus to the next or previous visible window in the application.
|
||||
*/
|
||||
public class NextPreviousWindowAction extends DockingAction {
|
||||
|
||||
private boolean forward;
|
||||
|
||||
public NextPreviousWindowAction(KeyStroke keybinding, boolean forward) {
|
||||
super(forward ? "Next Window" : "Previous Window",
|
||||
DockingWindowManager.DOCKING_WINDOWS_OWNER);
|
||||
|
||||
this.forward = forward;
|
||||
createReservedKeyBinding(keybinding);
|
||||
setEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionContext context) {
|
||||
Window[] windows = Window.getWindows();
|
||||
int currentIndex = getIndexForCurrentWindow(windows);
|
||||
Window nextWindow = findNextValidWindow(windows, currentIndex);
|
||||
if (nextWindow != null) {
|
||||
nextWindow.toFront();
|
||||
}
|
||||
}
|
||||
|
||||
private int getIndexForCurrentWindow(Window[] windows) {
|
||||
Window window = KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow();
|
||||
for (int i = 0; i < windows.length; i++) {
|
||||
if (window == windows[i]) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0; // this shouldn't happen
|
||||
}
|
||||
|
||||
private Window findNextValidWindow(Window[] windows, int currentIndex) {
|
||||
int candidateIndex = nextIndex(windows, currentIndex);
|
||||
while (candidateIndex != currentIndex) {
|
||||
if (isValid(windows[candidateIndex])) {
|
||||
return windows[candidateIndex];
|
||||
}
|
||||
candidateIndex = nextIndex(windows, candidateIndex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isValid(Window window) {
|
||||
if (!window.isVisible()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private int nextIndex(Window[] windows, int index) {
|
||||
if (forward) {
|
||||
int next = index + 1;
|
||||
return next >= windows.length ? 0 : next;
|
||||
}
|
||||
int previous = index - 1;
|
||||
return previous < 0 ? windows.length - 1 : previous;
|
||||
}
|
||||
|
||||
}
|
|
@ -89,13 +89,24 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
|
||||
keyBindingsManager.addReservedAction(new HelpAction(false, ReservedKeyBindings.HELP_KEY1));
|
||||
keyBindingsManager.addReservedAction(new HelpAction(false, ReservedKeyBindings.HELP_KEY2));
|
||||
keyBindingsManager.addReservedAction(
|
||||
new HelpAction(true, ReservedKeyBindings.HELP_INFO_KEY));
|
||||
keyBindingsManager
|
||||
.addReservedAction(new HelpAction(true, ReservedKeyBindings.HELP_INFO_KEY));
|
||||
keyBindingsManager.addReservedAction(
|
||||
new ShowContextMenuAction(ReservedKeyBindings.CONTEXT_MENU_KEY1));
|
||||
keyBindingsManager.addReservedAction(
|
||||
new ShowContextMenuAction(ReservedKeyBindings.CONTEXT_MENU_KEY2));
|
||||
|
||||
keyBindingsManager.addReservedAction(
|
||||
new NextPreviousWindowAction(ReservedKeyBindings.FOCUS_NEXT_WINDOW_KEY, true));
|
||||
keyBindingsManager.addReservedAction(
|
||||
new NextPreviousWindowAction(ReservedKeyBindings.FOCUS_PREVIOUS_WINDOW_KEY, false));
|
||||
|
||||
keyBindingsManager.addReservedAction(
|
||||
new GlobalFocusTraversalAction(ReservedKeyBindings.FOCUS_NEXT_COMPONENT_KEY, true));
|
||||
keyBindingsManager.addReservedAction(
|
||||
new GlobalFocusTraversalAction(ReservedKeyBindings.FOCUS_PREVIOUS_COMPONENT_KEY,
|
||||
false));
|
||||
|
||||
// helpful debugging actions
|
||||
keyBindingsManager.addReservedAction(new ShowFocusInfoAction());
|
||||
keyBindingsManager.addReservedAction(new ShowFocusCycleAction());
|
||||
|
@ -283,12 +294,16 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
private Iterator<DockingActionIf> getAllActionsIterator() {
|
||||
// chain all items together, rather than copy the data
|
||||
// Note: do not use Apache's IteratorUtils.chainedIterator. It degrades exponentially
|
||||
return Stream.concat(
|
||||
return Stream
|
||||
.concat(
|
||||
actionsByNameByOwner.values()
|
||||
.stream()
|
||||
.flatMap(actionsByName -> actionsByName.values().stream())
|
||||
.flatMap(actionsByName -> actionsByName.values()
|
||||
.stream())
|
||||
.flatMap(actions -> actions.stream()),
|
||||
sharedActionMap.values().stream()).iterator();
|
||||
sharedActionMap.values()
|
||||
.stream())
|
||||
.iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -344,7 +359,8 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
keyBindingsManager.removeAction(action);
|
||||
|
||||
getActionStorage(action).remove(action);
|
||||
if (!action.getKeyBindingType().isShared()) {
|
||||
if (!action.getKeyBindingType()
|
||||
.isShared()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -357,7 +373,8 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
private Set<DockingActionIf> getActionStorage(DockingActionIf action) {
|
||||
String owner = action.getOwner();
|
||||
String name = action.getName();
|
||||
return actionsByNameByOwner.get(owner).get(name);
|
||||
return actionsByNameByOwner.get(owner)
|
||||
.get(name);
|
||||
}
|
||||
|
||||
private void updateKeyBindingsFromOptions(ToolOptions options, String optionName,
|
||||
|
@ -370,7 +387,8 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
String name = matcher.group(1);
|
||||
String owner = matcher.group(2);
|
||||
|
||||
Set<DockingActionIf> actions = actionsByNameByOwner.get(owner).get(name);
|
||||
Set<DockingActionIf> actions = actionsByNameByOwner.get(owner)
|
||||
.get(name);
|
||||
for (DockingActionIf action : actions) {
|
||||
KeyStroke oldKs = action.getKeyBinding();
|
||||
if (Objects.equals(oldKs, newKs)) {
|
||||
|
@ -382,12 +400,14 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
|
||||
@Override
|
||||
public void propertyChange(PropertyChangeEvent evt) {
|
||||
if (!evt.getPropertyName().equals(DockingActionIf.KEYBINDING_DATA_PROPERTY)) {
|
||||
if (!evt.getPropertyName()
|
||||
.equals(DockingActionIf.KEYBINDING_DATA_PROPERTY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
DockingActionIf action = (DockingActionIf) evt.getSource();
|
||||
if (!action.getKeyBindingType().isManaged()) {
|
||||
if (!action.getKeyBindingType()
|
||||
.isManaged()) {
|
||||
// this reads unusually, but we need to notify the tool to rebuild its 'Window' menu
|
||||
// in the case that this action is one of the tool's special actions
|
||||
keyBindingsChanged();
|
||||
|
@ -419,7 +439,8 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
|
|||
Iterator<DockingActionIf> it = actionGuiHelper.getComponentActions(provider);
|
||||
while (it.hasNext()) {
|
||||
DockingActionIf action = it.next();
|
||||
if (action.getName().equals(actionName)) {
|
||||
if (action.getName()
|
||||
.equals(actionName)) {
|
||||
return action;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,8 +15,7 @@
|
|||
*/
|
||||
package docking.widgets;
|
||||
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.*;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.Border;
|
||||
|
@ -36,6 +35,8 @@ public class EmptyBorderButton extends JButton {
|
|||
|
||||
private ButtonStateListener emptyBorderButtonChangeListener;
|
||||
|
||||
private ButtonFocusListener emptyBorderButtonFocusListener;
|
||||
|
||||
/**
|
||||
* A raised beveled border.
|
||||
*/
|
||||
|
@ -105,7 +106,10 @@ public class EmptyBorderButton extends JButton {
|
|||
installLookAndFeelFix();
|
||||
clearBorder();
|
||||
emptyBorderButtonChangeListener = new ButtonStateListener();
|
||||
emptyBorderButtonFocusListener = new ButtonFocusListener();
|
||||
|
||||
addChangeListener(emptyBorderButtonChangeListener);
|
||||
addFocusListener(emptyBorderButtonFocusListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -164,6 +168,9 @@ public class EmptyBorderButton extends JButton {
|
|||
else if (rollover) {
|
||||
setBorder(getRaisedBorder());
|
||||
}
|
||||
else if (isFocusOwner()) {
|
||||
setBorder(getRaisedBorder());
|
||||
}
|
||||
else {
|
||||
setBorder(NO_BUTTON_BORDER);
|
||||
}
|
||||
|
@ -179,6 +186,7 @@ public class EmptyBorderButton extends JButton {
|
|||
|
||||
public void removeListeners() {
|
||||
removeChangeListener(emptyBorderButtonChangeListener);
|
||||
removeFocusListener(emptyBorderButtonFocusListener);
|
||||
}
|
||||
|
||||
private class ButtonStateListener implements ChangeListener {
|
||||
|
@ -187,4 +195,18 @@ public class EmptyBorderButton extends JButton {
|
|||
updateBorderBasedOnState();
|
||||
}
|
||||
}
|
||||
|
||||
private class ButtonFocusListener implements FocusListener {
|
||||
|
||||
@Override
|
||||
public void focusGained(FocusEvent e) {
|
||||
updateBorderBasedOnState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void focusLost(FocusEvent e) {
|
||||
updateBorderBasedOnState();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,762 @@
|
|||
/* ###
|
||||
* 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.docking.util;
|
||||
|
||||
import java.awt.*;
|
||||
import java.beans.PropertyChangeEvent;
|
||||
import java.beans.PropertyChangeListener;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
import docking.*;
|
||||
import docking.action.DockingAction;
|
||||
import docking.action.ToggleDockingAction;
|
||||
import docking.action.builder.ActionBuilder;
|
||||
import docking.action.builder.ToggleActionBuilder;
|
||||
import docking.widgets.table.*;
|
||||
import generic.theme.GThemeDefaults.Ids.Fonts;
|
||||
import generic.theme.Gui;
|
||||
import generic.util.WindowUtilities;
|
||||
import ghidra.docking.settings.Settings;
|
||||
import ghidra.framework.plugintool.ServiceProvider;
|
||||
import ghidra.framework.plugintool.ServiceProviderStub;
|
||||
import ghidra.util.bean.GGlassPane;
|
||||
import ghidra.util.layout.PairLayout;
|
||||
import resources.Icons;
|
||||
|
||||
/**
|
||||
* Diagnostic dialog for display information about the components in a window and related focus
|
||||
* information.
|
||||
*/
|
||||
public class ComponentInfoDialog extends DialogComponentProvider implements PropertyChangeListener {
|
||||
private static final String ACTION_OWNER = "Component Info";
|
||||
private Container rootComponentForTable;
|
||||
private List<ComponentInfo> infos = new ArrayList<>();
|
||||
private Map<Component, ComponentInfo> infoMap = new HashMap<>();
|
||||
|
||||
private GFilterTable<ComponentInfo> filterTable;
|
||||
private ComponentTableModel model;
|
||||
private ToggleDockingAction filterAction;
|
||||
private JTextField oldWindowTextField;
|
||||
private JTextField newWindowTextField;
|
||||
private JTextField oldProviderTextField;
|
||||
private JTextField newProviderTextField;
|
||||
private JTextField oldComponentTextField;
|
||||
private JTextField newComponentTextField;
|
||||
private Window currentWindow;
|
||||
private ComponentProvider currentProvider;
|
||||
private Component currentComponent;
|
||||
private EventDisplayPanel eventDisplay;
|
||||
private JSplitPane splitPane;
|
||||
private ToggleDockingAction eventAction;
|
||||
|
||||
public ComponentInfoDialog() {
|
||||
super("Component Inspector", false);
|
||||
|
||||
addWorkPanel(buildMainPanel());
|
||||
addDismissButton();
|
||||
addOKButton();
|
||||
setOkButtonText("Arm");
|
||||
|
||||
setPreferredSize(1200, 600);
|
||||
eventDisplay = new EventDisplayPanel();
|
||||
createActions();
|
||||
|
||||
KeyboardFocusManager km = KeyboardFocusManager.getCurrentKeyboardFocusManager();
|
||||
km.addPropertyChangeListener("permanentFocusOwner", this);
|
||||
arm();
|
||||
}
|
||||
|
||||
private void createActions() {
|
||||
// we don't automatically refresh the list of components in a window if it changes
|
||||
DockingAction refreshAction = new ActionBuilder("Refresh", ACTION_OWNER)
|
||||
.toolBarIcon(Icons.REFRESH_ICON)
|
||||
.onAction(c -> refreshModelData())
|
||||
.build();
|
||||
addAction(refreshAction);
|
||||
|
||||
// this action also just calls the refreshModelData() method, but since it is a toggle
|
||||
// action, the refresh model data will rebuild with the filter option toggled.
|
||||
filterAction = new ToggleActionBuilder("Filter", ACTION_OWNER)
|
||||
.toolBarIcon(Icons.CONFIGURE_FILTER_ICON)
|
||||
.description("Filters out most non-focusable components")
|
||||
.onAction(c -> refreshModelData())
|
||||
.selected(true)
|
||||
.build();
|
||||
addAction(filterAction);
|
||||
|
||||
eventAction = new ToggleActionBuilder("Show Events", ACTION_OWNER)
|
||||
.toolBarIcon(Icons.INFO_ICON)
|
||||
.description("Shows focus events")
|
||||
.onAction(c -> toggleShowEvents())
|
||||
.build();
|
||||
addAction(eventAction);
|
||||
}
|
||||
|
||||
private void toggleShowEvents() {
|
||||
if (eventAction.isSelected()) {
|
||||
splitPane.setBottomComponent(eventDisplay);
|
||||
splitPane.setDividerLocation(0.7);
|
||||
splitPane.setResizeWeight(0.7);
|
||||
}
|
||||
else {
|
||||
splitPane.setBottomComponent(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshModelData() {
|
||||
buildComponentModel();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void okCallback() {
|
||||
arm();
|
||||
}
|
||||
|
||||
// clear the current table data. The next component to get focus will repopulate the table data.
|
||||
private void arm() {
|
||||
setRootContainer(null);
|
||||
}
|
||||
|
||||
private ComponentInfo getComponentInfo(Component comp) {
|
||||
return infoMap.get(comp);
|
||||
}
|
||||
|
||||
private JComponent buildMainPanel() {
|
||||
JPanel panel = new JPanel(new BorderLayout());
|
||||
panel.add(buildCenterPanel(), BorderLayout.CENTER);
|
||||
panel.add(buildInfoPanel(), BorderLayout.SOUTH);
|
||||
return panel;
|
||||
|
||||
}
|
||||
|
||||
private JComponent buildCenterPanel() {
|
||||
splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
|
||||
splitPane.setTopComponent(buildTablePanel());
|
||||
return splitPane;
|
||||
}
|
||||
|
||||
private JComponent buildTablePanel() {
|
||||
model = new ComponentTableModel();
|
||||
filterTable = new GFilterTable<ComponentInfo>(model);
|
||||
return filterTable;
|
||||
}
|
||||
|
||||
private JComponent buildInfoPanel() {
|
||||
JPanel panel = new JPanel(new PairLayout(4, 5));
|
||||
panel.setBorder(BorderFactory.createEmptyBorder(4, 10, 20, 10));
|
||||
oldWindowTextField = new JTextField(50);
|
||||
newWindowTextField = new JTextField(50);
|
||||
oldProviderTextField = new JTextField(50);
|
||||
newProviderTextField = new JTextField(50);
|
||||
oldComponentTextField = new JTextField(50);
|
||||
newComponentTextField = new JTextField(50);
|
||||
oldWindowTextField.setFocusable(false);
|
||||
newWindowTextField.setFocusable(false);
|
||||
oldProviderTextField.setFocusable(false);
|
||||
newProviderTextField.setFocusable(false);
|
||||
oldComponentTextField.setFocusable(false);
|
||||
newComponentTextField.setFocusable(false);
|
||||
panel.add(new JLabel("Focused Window (new/old): ", SwingConstants.RIGHT));
|
||||
panel.add(buildTextPair(newWindowTextField, oldWindowTextField));
|
||||
panel.add(new JLabel("Focused Provider (new/old): ", SwingConstants.RIGHT));
|
||||
panel.add(buildTextPair(newProviderTextField, oldProviderTextField));
|
||||
panel.add(new JLabel("Focused Component (new/old): ", SwingConstants.RIGHT));
|
||||
panel.add(buildTextPair(newComponentTextField, oldComponentTextField));
|
||||
return panel;
|
||||
}
|
||||
|
||||
private Component buildTextPair(JComponent comp1, JComponent comp2) {
|
||||
JPanel panel = new JPanel(new GridLayout(1, 2, 10, 10));
|
||||
panel.add(comp1);
|
||||
panel.add(comp2);
|
||||
return panel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void propertyChange(PropertyChangeEvent evt) {
|
||||
eventDisplay.report(getDisplayString(evt));
|
||||
filterTable.setSelectedRowObject(null);
|
||||
Component newFocusComponent = (Component) evt.getNewValue();
|
||||
selectFocusedComponentInTable(newFocusComponent);
|
||||
updateFocusInfo(newFocusComponent);
|
||||
|
||||
}
|
||||
|
||||
private String getDisplayString(PropertyChangeEvent evt) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(evt.getPropertyName());
|
||||
builder.append(": OLD = ");
|
||||
builder.append(getName((Component) evt.getOldValue()));
|
||||
builder.append(", NEW = ");
|
||||
builder.append(getName((Component) evt.getNewValue()));
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private void updateFocusInfo(Component newFocusComponent) {
|
||||
if (newFocusComponent == null) {
|
||||
return;
|
||||
}
|
||||
if (newFocusComponent == currentComponent) {
|
||||
return;
|
||||
}
|
||||
oldComponentTextField.setText(newComponentTextField.getText());
|
||||
currentComponent = newFocusComponent;
|
||||
newComponentTextField.setText(getName(currentComponent));
|
||||
|
||||
oldProviderTextField.setText(newProviderTextField.getText());
|
||||
currentProvider = DockingWindowManager.getActiveInstance()
|
||||
.getProvider(newFocusComponent);
|
||||
newProviderTextField.setText(currentProvider == null ? "" : currentProvider.getName());
|
||||
|
||||
oldWindowTextField.setText(newWindowTextField.getText());
|
||||
currentWindow = SwingUtilities.windowForComponent(newFocusComponent);
|
||||
newWindowTextField.setText(WindowUtilities.getTitle(currentWindow));
|
||||
|
||||
}
|
||||
|
||||
private String getName(Component comp) {
|
||||
if (comp == null) {
|
||||
return null;
|
||||
}
|
||||
String name = comp.getName();
|
||||
StringBuilder buf = new StringBuilder(name == null ? "" : name);
|
||||
buf.append(" (");
|
||||
buf.append(comp.getClass()
|
||||
.getSimpleName());
|
||||
buf.append(")");
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
private void selectFocusedComponentInTable(Component newFocusComponent) {
|
||||
if (infos.isEmpty()) {
|
||||
if (newFocusComponent == null) {
|
||||
return;
|
||||
}
|
||||
setRootContainer(findRoot(newFocusComponent));
|
||||
}
|
||||
ComponentInfo focusedInfo = getComponentInfo(newFocusComponent);
|
||||
filterTable.setSelectedRowObject(focusedInfo);
|
||||
}
|
||||
|
||||
void setRootContainer(Container container) {
|
||||
rootComponentForTable = container;
|
||||
buildComponentModel();
|
||||
}
|
||||
|
||||
void buildComponentModel() {
|
||||
ComponentInfo.resetIds();
|
||||
infos = findComponents(rootComponentForTable);
|
||||
buildInfoMap();
|
||||
model.setModelData(filterAction.isSelected() ? filterInfos() : infos);
|
||||
}
|
||||
|
||||
private List<ComponentInfo> filterInfos() {
|
||||
List<ComponentInfo> result = new ArrayList<>();
|
||||
for (ComponentInfo info : infos) {
|
||||
if (shouldInclude(info)) {
|
||||
result.add(info);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void buildInfoMap() {
|
||||
for (ComponentInfo info : infos) {
|
||||
infoMap.put(info.getComponent(), info);
|
||||
}
|
||||
}
|
||||
|
||||
private List<ComponentInfo> findComponents(Container root) {
|
||||
List<ComponentInfo> infoList = new ArrayList<>();
|
||||
if (root == null) {
|
||||
return infoList;
|
||||
}
|
||||
ComponentInfo rootInfo = new ComponentInfo(null, root, 0);
|
||||
infoList.add(rootInfo);
|
||||
for (int i = 0; i < root.getComponentCount(); i++) {
|
||||
Component comp = root.getComponent(i);
|
||||
ComponentInfo info = new ComponentInfo(rootInfo, comp, i);
|
||||
infoList.add(info);
|
||||
addChildInfos(infoList, info);
|
||||
}
|
||||
|
||||
return infoList;
|
||||
}
|
||||
|
||||
private void addChildInfos(List<ComponentInfo> infoList, ComponentInfo parentInfo) {
|
||||
if (parentInfo.getComponent() instanceof Container container) {
|
||||
for (int i = 0; i < container.getComponentCount(); i++) {
|
||||
Component comp = container.getComponent(i);
|
||||
ComponentInfo info = new ComponentInfo(parentInfo, comp, i);
|
||||
infoList.add(info);
|
||||
addChildInfos(infoList, info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldInclude(ComponentInfo info) {
|
||||
Component component = info.getComponent();
|
||||
if (!component.isFocusable()) {
|
||||
return false;
|
||||
}
|
||||
if (component instanceof JPanel jPanel) {
|
||||
if (info.getCycleRootIndex() == null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (component instanceof GGlassPane) {
|
||||
return false;
|
||||
}
|
||||
if (component instanceof JRootPane) {
|
||||
return false;
|
||||
}
|
||||
if (component instanceof JViewport) {
|
||||
return false;
|
||||
}
|
||||
if (component instanceof JLayeredPane) {
|
||||
return false;
|
||||
}
|
||||
if (component instanceof JLabel) {
|
||||
return false;
|
||||
}
|
||||
if (component instanceof JMenu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private Container findRoot(Component component) {
|
||||
if (component.getParent() == null) {
|
||||
return component instanceof Container ? (Container) component : null;
|
||||
}
|
||||
if (component instanceof Window window) {
|
||||
return window;
|
||||
}
|
||||
return findRoot(component.getParent());
|
||||
}
|
||||
|
||||
class ComponentTableModel extends GDynamicColumnTableModel<ComponentInfo, Object> {
|
||||
private List<ComponentInfo> modelData = new ArrayList<>();
|
||||
|
||||
public ComponentTableModel() {
|
||||
super(new ServiceProviderStub());
|
||||
}
|
||||
|
||||
void setModelData(List<ComponentInfo> data) {
|
||||
this.modelData = new ArrayList<>(data);
|
||||
fireTableDataChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return ACTION_OWNER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ComponentInfo> getModelData() {
|
||||
return modelData;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TableColumnDescriptor<ComponentInfo> createTableColumnDescriptor() {
|
||||
TableColumnDescriptor<ComponentInfo> descriptor = new TableColumnDescriptor<>();
|
||||
descriptor.addVisibleColumn(new ComponentIdColumn(), 1, true);
|
||||
descriptor.addVisibleColumn(new ParentIdColumn());
|
||||
descriptor.addVisibleColumn(new CycleIndexColumn());
|
||||
descriptor.addVisibleColumn(new ComponentNameColumn());
|
||||
descriptor.addVisibleColumn(new ComponentClassColumn());
|
||||
descriptor.addVisibleColumn(new ToolTipColumn());
|
||||
descriptor.addHiddenColumn(new FocusableColumn());
|
||||
descriptor.addHiddenColumn(new IsFocusCycleRootColumn());
|
||||
descriptor.addHiddenColumn(new focusCycleRootColumn());
|
||||
descriptor.addVisibleColumn(new TraversalKeysColumn());
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getDataSource() {
|
||||
return null;
|
||||
}
|
||||
|
||||
private class ComponentNameColumn
|
||||
extends AbstractDynamicTableColumn<ComponentInfo, String, Object> {
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return "Name";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue(ComponentInfo info, Settings settings, Object data,
|
||||
ServiceProvider provider) throws IllegalArgumentException {
|
||||
return info.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnPreferredWidth() {
|
||||
return 200;
|
||||
}
|
||||
}
|
||||
|
||||
private class ToolTipColumn
|
||||
extends AbstractDynamicTableColumn<ComponentInfo, String, Object> {
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return "Tool Tip";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue(ComponentInfo info, Settings settings, Object data,
|
||||
ServiceProvider provider) throws IllegalArgumentException {
|
||||
return info.getToolTip();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnPreferredWidth() {
|
||||
return 200;
|
||||
}
|
||||
}
|
||||
|
||||
private class ComponentClassColumn
|
||||
extends AbstractDynamicTableColumn<ComponentInfo, String, Object> {
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return "Class";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue(ComponentInfo info, Settings settings, Object data,
|
||||
ServiceProvider provider) throws IllegalArgumentException {
|
||||
return info.getClassSimpleName();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnPreferredWidth() {
|
||||
return 300;
|
||||
}
|
||||
}
|
||||
|
||||
private class ComponentIdColumn
|
||||
extends AbstractDynamicTableColumn<ComponentInfo, Integer, Object> {
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return "Id";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getValue(ComponentInfo info, Settings settings, Object data,
|
||||
ServiceProvider provider) throws IllegalArgumentException {
|
||||
return info.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnPreferredWidth() {
|
||||
return 50;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class ParentIdColumn
|
||||
extends AbstractDynamicTableColumn<ComponentInfo, String, Object> {
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return "Parent Id";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue(ComponentInfo info, Settings settings, Object data,
|
||||
ServiceProvider provider) throws IllegalArgumentException {
|
||||
ComponentInfo parent = info.getParent();
|
||||
return parent == null ? null : parent.getNameAndId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnPreferredWidth() {
|
||||
return 50;
|
||||
}
|
||||
}
|
||||
|
||||
private class TraversalKeysColumn
|
||||
extends AbstractDynamicTableColumn<ComponentInfo, String, Object> {
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return "Traversal Keys";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue(ComponentInfo info, Settings settings, Object data,
|
||||
ServiceProvider provider) throws IllegalArgumentException {
|
||||
Set<AWTKeyStroke> keys = info.getComponent()
|
||||
.getFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS);
|
||||
return keys == null ? "" : keys.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnPreferredWidth() {
|
||||
return 50;
|
||||
}
|
||||
}
|
||||
|
||||
private class FocusableColumn
|
||||
extends AbstractDynamicTableColumn<ComponentInfo, Boolean, Object> {
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return "Focusable";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean getValue(ComponentInfo info, Settings settings, Object data,
|
||||
ServiceProvider provider) throws IllegalArgumentException {
|
||||
return info.isFocusable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnPreferredWidth() {
|
||||
return 50;
|
||||
}
|
||||
}
|
||||
|
||||
private class IsFocusCycleRootColumn
|
||||
extends AbstractDynamicTableColumn<ComponentInfo, Boolean, Object> {
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return "Is Cycle Root";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean getValue(ComponentInfo info, Settings settings, Object data,
|
||||
ServiceProvider provider) throws IllegalArgumentException {
|
||||
return info.isCycleRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnPreferredWidth() {
|
||||
return 200;
|
||||
}
|
||||
}
|
||||
|
||||
private class focusCycleRootColumn
|
||||
extends AbstractDynamicTableColumn<ComponentInfo, String, Object> {
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return "Cycle Root";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue(ComponentInfo info, Settings settings, Object data,
|
||||
ServiceProvider provider) throws IllegalArgumentException {
|
||||
Component component = info.getComponent();
|
||||
Container cycleRoot = component.getFocusCycleRootAncestor();
|
||||
ComponentInfo cycleRootInfo = getComponentInfo(cycleRoot);
|
||||
if (cycleRootInfo != null) {
|
||||
return cycleRootInfo.getNameAndId();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnPreferredWidth() {
|
||||
return 200;
|
||||
}
|
||||
}
|
||||
|
||||
private class CycleIndexColumn
|
||||
extends AbstractDynamicTableColumn<ComponentInfo, Integer, Object> {
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return "Cycle Index";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getValue(ComponentInfo info, Settings settings, Object data,
|
||||
ServiceProvider provider) throws IllegalArgumentException {
|
||||
|
||||
return info.getCycleRootIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnPreferredWidth() {
|
||||
return 200;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ComponentInfo {
|
||||
private static int nextId = 0;
|
||||
private ComponentInfo parent;
|
||||
private Component component;
|
||||
private int id = ++nextId;
|
||||
private int depth;
|
||||
private String nameAndId;
|
||||
private boolean isCycleRoot;
|
||||
private List<Component> traversalComps;
|
||||
private Integer cycleRootIndex;
|
||||
|
||||
ComponentInfo(ComponentInfo parent, Component component, int indexInParent) {
|
||||
this.parent = parent;
|
||||
this.component = component;
|
||||
this.depth = parent == null ? 0 : parent.depth + 1;
|
||||
this.nameAndId = component.getName() + " (" + id + ")";
|
||||
this.isCycleRoot = checkIsCycleRoot();
|
||||
if (isCycleRoot) {
|
||||
this.traversalComps = computeTraversalComps();
|
||||
}
|
||||
}
|
||||
|
||||
public String getToolTip() {
|
||||
if (component instanceof JComponent jComp) {
|
||||
return jComp.getToolTipText();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Integer computeCycleRootIndex() {
|
||||
Container cycleRoot = component.getFocusCycleRootAncestor();
|
||||
if (cycleRoot == null) {
|
||||
return -1;
|
||||
}
|
||||
ComponentInfo cycleRootInfo = getComponentInfo(cycleRoot);
|
||||
List<Component> rootTraversalComps = cycleRootInfo.getTraversalComps();
|
||||
return rootTraversalComps.indexOf(component);
|
||||
}
|
||||
|
||||
private List<Component> computeTraversalComps() {
|
||||
List<Component> traversals = new ArrayList<>();
|
||||
Container container = (Container) component;
|
||||
FocusTraversalPolicy policy = container.getFocusTraversalPolicy();
|
||||
Component comp = policy.getFirstComponent(container);
|
||||
while (comp != null && !traversals.contains(comp)) {
|
||||
traversals.add(comp);
|
||||
comp = policy.getComponentAfter(container, comp);
|
||||
}
|
||||
return traversals;
|
||||
}
|
||||
|
||||
public Integer getCycleRootIndex() {
|
||||
if (cycleRootIndex == null) {
|
||||
cycleRootIndex = computeCycleRootIndex();
|
||||
}
|
||||
return cycleRootIndex < 0 ? null : cycleRootIndex;
|
||||
}
|
||||
|
||||
public List<Component> getTraversalComps() {
|
||||
return traversalComps;
|
||||
}
|
||||
|
||||
private boolean checkIsCycleRoot() {
|
||||
if (component instanceof Container container) {
|
||||
return component.isFocusCycleRoot(container);
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
public boolean isCycleRoot() {
|
||||
return isCycleRoot;
|
||||
}
|
||||
|
||||
public String getNameAndId() {
|
||||
return nameAndId;
|
||||
}
|
||||
|
||||
public String getClassSimpleName() {
|
||||
String name = component.getClass()
|
||||
.getName();
|
||||
int lastIndexOf = name.lastIndexOf(".");
|
||||
if (lastIndexOf < 0) {
|
||||
return name;
|
||||
}
|
||||
return name.substring(lastIndexOf + 1);
|
||||
}
|
||||
|
||||
public Boolean isFocusable() {
|
||||
return component.isFocusable();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return component.getName();
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Component getComponent() {
|
||||
return component;
|
||||
}
|
||||
|
||||
public ComponentInfo getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
public int getDepth() {
|
||||
return depth;
|
||||
}
|
||||
|
||||
public static void resetIds() {
|
||||
nextId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
class EventDisplayPanel extends JPanel {
|
||||
private static int NUM_MESSAGES = 20;
|
||||
private JTextArea text;
|
||||
private List<String> messages = new LinkedList<>();
|
||||
|
||||
EventDisplayPanel() {
|
||||
super(new BorderLayout());
|
||||
setBorder(BorderFactory.createEmptyBorder(0, 10, 2, 10));
|
||||
text = new JTextArea(5, 100);
|
||||
text.setFont(Gui.getFont(Fonts.MONOSPACED));
|
||||
text.setEditable(false);
|
||||
JScrollPane scroll = new JScrollPane(text);
|
||||
scroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
|
||||
add(scroll, BorderLayout.CENTER);
|
||||
}
|
||||
|
||||
void report(String message) {
|
||||
messages.add(message);
|
||||
if (messages.size() > NUM_MESSAGES) {
|
||||
messages.remove(0);
|
||||
}
|
||||
text.setText(buildText());
|
||||
}
|
||||
|
||||
private String buildText() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (String message : messages) {
|
||||
builder.append(message);
|
||||
builder.append("\n");
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -24,7 +24,8 @@ import javax.swing.KeyStroke;
|
|||
public class ReservedKeyBindings {
|
||||
|
||||
private static final int CONTROL_KEY_MODIFIER_MASK =
|
||||
Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
|
||||
Toolkit.getDefaultToolkit()
|
||||
.getMenuShortcutKeyMaskEx();
|
||||
|
||||
private ReservedKeyBindings() {
|
||||
// utils class
|
||||
|
@ -40,6 +41,18 @@ public class ReservedKeyBindings {
|
|||
public static final KeyStroke CONTEXT_MENU_KEY2 =
|
||||
KeyStroke.getKeyStroke(KeyEvent.VK_CONTEXT_MENU, 0);
|
||||
|
||||
public static final KeyStroke FOCUS_NEXT_WINDOW_KEY =
|
||||
KeyStroke.getKeyStroke(KeyEvent.VK_F3, InputEvent.CTRL_DOWN_MASK);
|
||||
public static final KeyStroke FOCUS_PREVIOUS_WINDOW_KEY =
|
||||
KeyStroke.getKeyStroke(KeyEvent.VK_F3,
|
||||
InputEvent.SHIFT_DOWN_MASK | InputEvent.CTRL_DOWN_MASK);
|
||||
|
||||
public static final KeyStroke FOCUS_NEXT_COMPONENT_KEY =
|
||||
KeyStroke.getKeyStroke(KeyEvent.VK_TAB, InputEvent.CTRL_DOWN_MASK);
|
||||
public static final KeyStroke FOCUS_PREVIOUS_COMPONENT_KEY =
|
||||
KeyStroke.getKeyStroke(KeyEvent.VK_TAB,
|
||||
InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK);
|
||||
|
||||
public static final KeyStroke FOCUS_INFO_KEY = KeyStroke.getKeyStroke(KeyEvent.VK_F2,
|
||||
CONTROL_KEY_MODIFIER_MASK | InputEvent.ALT_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK);
|
||||
public static final KeyStroke FOCUS_CYCLE_INFO_KEY = KeyStroke.getKeyStroke(KeyEvent.VK_F3,
|
||||
|
@ -51,17 +64,30 @@ public class ReservedKeyBindings {
|
|||
public static final KeyStroke COMPONENT_THEME_INFO_KEY = KeyStroke.getKeyStroke(KeyEvent.VK_F9,
|
||||
CONTROL_KEY_MODIFIER_MASK | InputEvent.ALT_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK);
|
||||
|
||||
// @formatter:off
|
||||
public static boolean isReservedKeystroke(KeyStroke keyStroke) {
|
||||
int code = keyStroke.getKeyCode();
|
||||
if (code == KeyEvent.VK_SHIFT || code == KeyEvent.VK_ALT || code == KeyEvent.VK_CONTROL ||
|
||||
code == KeyEvent.VK_CAPS_LOCK || code == KeyEvent.VK_TAB ||
|
||||
HELP_KEY1.equals(keyStroke) || HELP_KEY2.equals(keyStroke) ||
|
||||
HELP_INFO_KEY.equals(keyStroke) || UPDATE_KEY_BINDINGS_KEY.equals(keyStroke) ||
|
||||
FOCUS_INFO_KEY.equals(keyStroke) || FOCUS_CYCLE_INFO_KEY.equals(keyStroke) ||
|
||||
COMPONENT_THEME_INFO_KEY.equals(keyStroke) || CONTEXT_MENU_KEY1.equals(keyStroke) ||
|
||||
CONTEXT_MENU_KEY2.equals(keyStroke)) {
|
||||
if (code == KeyEvent.VK_SHIFT ||
|
||||
code == KeyEvent.VK_ALT ||
|
||||
code == KeyEvent.VK_CONTROL ||
|
||||
code == KeyEvent.VK_CAPS_LOCK ||
|
||||
code == KeyEvent.VK_TAB ||
|
||||
HELP_KEY1.equals(keyStroke) ||
|
||||
HELP_KEY2.equals(keyStroke) ||
|
||||
HELP_INFO_KEY.equals(keyStroke) ||
|
||||
UPDATE_KEY_BINDINGS_KEY.equals(keyStroke) ||
|
||||
FOCUS_INFO_KEY.equals(keyStroke) ||
|
||||
FOCUS_CYCLE_INFO_KEY.equals(keyStroke) ||
|
||||
COMPONENT_THEME_INFO_KEY.equals(keyStroke) ||
|
||||
CONTEXT_MENU_KEY1.equals(keyStroke) ||
|
||||
CONTEXT_MENU_KEY2.equals(keyStroke) ||
|
||||
FOCUS_NEXT_WINDOW_KEY.equals(keyStroke) ||
|
||||
FOCUS_PREVIOUS_WINDOW_KEY.equals(keyStroke) ||
|
||||
FOCUS_NEXT_COMPONENT_KEY.equals(keyStroke) ||
|
||||
FOCUS_PREVIOUS_COMPONENT_KEY.equals(keyStroke)) {
|
||||
return true;
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
* REVIEWED: YES
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
|
@ -26,6 +26,7 @@ public interface PluginCategoryNames {
|
|||
String SEARCH = "Search";
|
||||
String TREE = "Program Tree";
|
||||
String TESTING = GenericPluginCategoryNames.TESTING;
|
||||
String DIAGNOSTIC = "Diagnostic";
|
||||
String DIFF = "Code Difference";
|
||||
String MISC = GenericPluginCategoryNames.MISC;
|
||||
String USER_ANNOTATION = "User Annotation";
|
||||
|
|
|
@ -501,7 +501,8 @@ public abstract class PluginTool extends AbstractDockingTool {
|
|||
pluginMgr.close();
|
||||
if (project != null) {
|
||||
if (project.getToolManager() != null) {
|
||||
project.getToolManager().disconnectTool(this);
|
||||
project.getToolManager()
|
||||
.disconnectTool(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -720,7 +721,8 @@ public abstract class PluginTool extends AbstractDockingTool {
|
|||
*/
|
||||
public boolean threadIsBackgroundTaskThread() {
|
||||
ThreadGroup taskGroup = taskMgr.getTaskThreadGroup();
|
||||
ThreadGroup group = Thread.currentThread().getThreadGroup();
|
||||
ThreadGroup group = Thread.currentThread()
|
||||
.getThreadGroup();
|
||||
while (group != null && group != taskGroup) {
|
||||
group = group.getParent();
|
||||
}
|
||||
|
@ -1050,8 +1052,28 @@ public abstract class PluginTool extends AbstractDockingTool {
|
|||
addAction(saveAsAction);
|
||||
}
|
||||
|
||||
protected void addExportToolAction() {
|
||||
/**
|
||||
* Adds actions to the tool for transferring focus to the first component in the next
|
||||
* or previous dockable component provider.
|
||||
*/
|
||||
protected void addNextPreviousProviderActions() {
|
||||
// @formatter:off
|
||||
new ActionBuilder("Jump to Next Dockable Provider", ToolConstants.TOOL_OWNER)
|
||||
.keyBinding(KeyStroke.getKeyStroke("control J"))
|
||||
.description("Transfer focus to the next major component in this windows")
|
||||
.onAction(e -> nextDockableComponent(true))
|
||||
.buildAndInstall(this);
|
||||
|
||||
new ActionBuilder("Jump to Previous Dockable Provider", ToolConstants.TOOL_OWNER)
|
||||
.keyBinding("shift control J")
|
||||
.description("Transfer focus to the previous major component in this windows")
|
||||
.onAction(e -> nextDockableComponent(false))
|
||||
.buildAndInstall(this);
|
||||
// @formatter:on
|
||||
|
||||
}
|
||||
|
||||
protected void addExportToolAction() {
|
||||
String menuGroup = "Tool";
|
||||
String exportPullright = "Export";
|
||||
setMenuGroup(new String[] { ToolConstants.MENU_FILE, exportPullright }, menuGroup);
|
||||
|
@ -1310,7 +1332,8 @@ public abstract class PluginTool extends AbstractDockingTool {
|
|||
* @param height height in pixels
|
||||
*/
|
||||
public void setSize(int width, int height) {
|
||||
winMgr.getMainWindow().setSize(new Dimension(width, height));
|
||||
winMgr.getMainWindow()
|
||||
.setSize(new Dimension(width, height));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1318,7 +1341,8 @@ public abstract class PluginTool extends AbstractDockingTool {
|
|||
* @return dimension of this tool's frame
|
||||
*/
|
||||
public Dimension getSize() {
|
||||
return winMgr.getMainWindow().getSize();
|
||||
return winMgr.getMainWindow()
|
||||
.getSize();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1327,7 +1351,8 @@ public abstract class PluginTool extends AbstractDockingTool {
|
|||
* @param y screen y coordinate
|
||||
*/
|
||||
public void setLocation(int x, int y) {
|
||||
winMgr.getMainWindow().setLocation(x, y);
|
||||
winMgr.getMainWindow()
|
||||
.setLocation(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1335,7 +1360,8 @@ public abstract class PluginTool extends AbstractDockingTool {
|
|||
* @return location of this tool's frame
|
||||
*/
|
||||
public Point getLocation() {
|
||||
return winMgr.getMainWindow().getLocation();
|
||||
return winMgr.getMainWindow()
|
||||
.getLocation();
|
||||
}
|
||||
|
||||
private void updateTitle() {
|
||||
|
@ -1527,6 +1553,63 @@ public abstract class PluginTool extends AbstractDockingTool {
|
|||
return restoringDataState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfers focus to the first component in the next/previous dockable component provider.
|
||||
* @param forward true to go to next provider, false to go to previous provider
|
||||
*/
|
||||
private void nextDockableComponent(boolean forward) {
|
||||
KeyboardFocusManager focusManager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
|
||||
Component focusOwner = focusManager.getPermanentFocusOwner();
|
||||
Component next = findNextProviderComponent(focusOwner, forward);
|
||||
|
||||
// If going backwards, go back one more provider, then go forward to get the first
|
||||
// component in the resulting provider. This makes it so that when going backwards, you
|
||||
// still get the first component in the component provider and not the last.
|
||||
if (!forward) {
|
||||
next = findNextProviderComponent(next, false);
|
||||
next = findNextProviderComponent(next, true);
|
||||
}
|
||||
if (next != null) {
|
||||
next.requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
private Component findNextProviderComponent(Component component, boolean forward) {
|
||||
if (component == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
DockingWindowManager windowManager = getWindowManager();
|
||||
ComponentProvider startingProvider = windowManager.getComponentProvider(component);
|
||||
|
||||
Component next = getNext(component, forward);
|
||||
while (next != null && next != component) {
|
||||
// Skip JTabbedPanes. Assume the user prefers that the component inside the tabbed
|
||||
// pane gets focus, not the tabbed pane itself so the user does not have to navigate
|
||||
// twice to get the internal component.
|
||||
if (next instanceof JTabbedPane) {
|
||||
next = getNext(next, forward);
|
||||
continue;
|
||||
}
|
||||
ComponentProvider nextProvider = windowManager.getComponentProvider(next);
|
||||
if (nextProvider != startingProvider) {
|
||||
return next;
|
||||
}
|
||||
next = getNext(next, forward);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Component getNext(Component component, boolean forward) {
|
||||
KeyboardFocusManager focusManager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
|
||||
Window window = focusManager.getFocusedWindow();
|
||||
FocusTraversalPolicy policy = window.getFocusTraversalPolicy();
|
||||
if (forward) {
|
||||
return policy.getComponentAfter(window, component);
|
||||
}
|
||||
return policy.getComponentBefore(window, component);
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
// Inner Classes
|
||||
//==================================================================================================
|
||||
|
|
|
@ -95,6 +95,7 @@ public class GhidraTool extends PluginTool {
|
|||
addManagePluginsAction();
|
||||
addOptionsAction();
|
||||
addHelpActions();
|
||||
addNextPreviousProviderActions();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue