GP-5114 - Updated component providers to close on Escape when they are the only provider in a window

This commit is contained in:
dragonmacher 2024-11-14 17:32:01 -05:00
parent 7e0a96e477
commit aaf23cf096
11 changed files with 187 additions and 41 deletions

View File

@ -62,6 +62,10 @@ public class AnalysisOptionsDialog extends DialogComponentProvider
addCancelButton(); addCancelButton();
addApplyButton(); addApplyButton();
setOkButtonText("Analyze"); setOkButtonText("Analyze");
// This allows user to press Enter to launch analysis when the dialog is shown. Without
// this, the table takes focus, which consumes Enter key presses.
setFocusComponent(okButton);
okButton.setMnemonic('A'); okButton.setMnemonic('A');
setOkEnabled(true); setOkEnabled(true);
setPreferredSize(1000, 600); setPreferredSize(1000, 600);

View File

@ -227,8 +227,12 @@ class ComponentNode extends Node {
} }
} }
@Override
int getComponentCount() { int getComponentCount() {
return windowPlaceholders.size(); // we may be a single component or in a tabbed pane of components
List<ComponentPlaceholder> activeComponents = new ArrayList<>();
populateActiveComponents(activeComponents);
return activeComponents.size();
} }
@Override @Override

View File

@ -101,6 +101,11 @@ class DetachedWindowNode extends WindowNode {
} }
} }
@Override
int getComponentCount() {
return child.getComponentCount();
}
@Override @Override
String getTitle() { String getTitle() {
if (window instanceof JDialog) { if (window instanceof JDialog) {

View File

@ -27,13 +27,14 @@ import org.jdesktop.animation.timing.Animator;
import org.jdesktop.animation.timing.TimingTargetAdapter; import org.jdesktop.animation.timing.TimingTargetAdapter;
import docking.action.*; import docking.action.*;
import docking.actions.KeyBindingUtils; import docking.action.builder.ActionBuilder;
import docking.event.mouse.GMouseListenerAdapter; import docking.event.mouse.GMouseListenerAdapter;
import docking.menu.DialogToolbarButton; import docking.menu.DialogToolbarButton;
import docking.util.AnimationUtils; import docking.util.AnimationUtils;
import docking.widgets.label.GDHtmlLabel; import docking.widgets.label.GDHtmlLabel;
import generic.theme.GColor; import generic.theme.GColor;
import generic.theme.GThemeDefaults.Colors.Messages; import generic.theme.GThemeDefaults.Colors.Messages;
import generic.util.WindowUtilities;
import ghidra.util.*; import ghidra.util.*;
import ghidra.util.exception.AssertException; import ghidra.util.exception.AssertException;
import ghidra.util.task.*; import ghidra.util.task.*;
@ -81,6 +82,7 @@ public class DialogComponentProvider
private TaskMonitorComponent taskMonitorComponent; private TaskMonitorComponent taskMonitorComponent;
private static final KeyStroke ESC_KEYSTROKE = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); private static final KeyStroke ESC_KEYSTROKE = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
private DockingAction closeAction;
private CardLayout progressCardLayout; private CardLayout progressCardLayout;
private JButton defaultButton; private JButton defaultButton;
@ -181,15 +183,31 @@ public class DialogComponentProvider
} }
private void installEscapeAction() { private void installEscapeAction() {
Action escAction = new AbstractAction("ESCAPE") {
@Override
public void actionPerformed(ActionEvent ev) {
escapeCallback();
}
};
KeyBindingUtils.registerAction(rootPanel, ESC_KEYSTROKE, escAction, closeAction = new ActionBuilder("Close Dialog", title)
JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); .sharedKeyBinding()
.keyBinding(ESC_KEYSTROKE)
.enabledWhen(this::isMyDialog)
.onAction(c -> escapeCallback())
.build();
addAction(closeAction);
}
private boolean isMyDialog(ActionContext c) {
//
// Each dialog registers a shared action bound to Escape. If all dialog actions are
// enabled, then the user will get prompted to pick which dialog to close when pressing
// Escape. Thus, we limit the enablement of each action to be the dialog that contains the
// focused component. We use the action context to find out if this dialog is the active
// dialog.
//
Window window = WindowUtilities.windowForComponent(c.getSourceComponent());
if (!(window instanceof DockingDialog dockingDialog)) {
return false;
}
return dockingDialog.containsProvider(DialogComponentProvider.this);
} }
/** a callback mechanism for children to do work */ /** a callback mechanism for children to do work */
@ -197,6 +215,16 @@ public class DialogComponentProvider
// may be overridden by subclasses // may be overridden by subclasses
} }
/**
* Returns true if the given keystroke is the trigger for this dialog's close action.
* @param ks the keystroke
* @return true if the given keystroke is the trigger for this dialog's close action
*/
public boolean isCloseKeyStroke(KeyStroke ks) {
KeyStroke currentCloseKs = closeAction.getKeyBinding();
return Objects.equals(ks, currentCloseKs);
}
public int getId() { public int getId() {
return id; return id;
} }

View File

@ -75,12 +75,15 @@ class DockableToolBarManager {
ToolBarCloseAction closeAction = new ToolBarCloseAction(owner); ToolBarCloseAction closeAction = new ToolBarCloseAction(owner);
closeButtonManager = new ToolBarItemManager(closeAction, winMgr); closeButtonManager = new ToolBarItemManager(closeAction, winMgr);
CloseLastProviderAction closeLastProviderAction = new CloseLastProviderAction(owner);
ToolBarMenuAction dropDownAction = new ToolBarMenuAction(owner); ToolBarMenuAction dropDownAction = new ToolBarMenuAction(owner);
menuButtonManager = new ToolBarItemManager(dropDownAction, winMgr); menuButtonManager = new ToolBarItemManager(dropDownAction, winMgr);
// we need to add this action to the tool in order to use key bindings // we need to add this action to the tool in order to use key bindings
Tool tool = winMgr.getTool(); Tool tool = winMgr.getTool();
tool.addLocalAction(provider, closeAction); tool.addLocalAction(provider, closeAction);
tool.addLocalAction(provider, closeLastProviderAction);
tool.addLocalAction(provider, dropDownAction); tool.addLocalAction(provider, dropDownAction);
} }
@ -216,6 +219,42 @@ class DockableToolBarManager {
} }
} }
/**
* An action to close the provider on Escape if the provider is the last in the window. This
* allows users to close transient providers (like search results) easily.
*/
private class CloseLastProviderAction extends DockingAction {
CloseLastProviderAction(String owner) {
super("Close Window for Last Provider", owner, KeyBindingType.SHARED);
setKeyBindingData(new KeyBindingData("ESCAPE"));
setDescription("Close the window if this provider is the last provider in the window");
markHelpUnnecessary();
}
@Override
public void actionPerformed(ActionContext context) {
ComponentPlaceholder placeholder = dockableComponent.getComponentWindowingPlaceholder();
if (placeholder != null) {
placeholder.close();
}
}
@Override
public boolean isEnabledForContext(ActionContext context) {
DockingWindowManager dwm = DockingWindowManager.getActiveInstance();
ComponentProvider provider = context.getComponentProvider();
if (provider == null) {
// Some context providers do not specify the provider when creating a contexts
provider = dwm.getActiveComponentProvider();
}
if (provider != dockableComponent.getComponentProvider()) {
return false; // not my provider
}
return dwm.isLastProviderInDetachedWindow(provider);
}
}
/** /**
* Actions added to toolbar for displaying the drop-down menu. * Actions added to toolbar for displaying the drop-down menu.
*/ */

View File

@ -62,10 +62,9 @@ public class DockingDialog extends JDialog implements HelpDescriptor {
* only happens during tests and one-off main methods that are not part of a * only happens during tests and one-off main methods that are not part of a
* running tool. * running tool.
* *
* @param componentProvider the dialog content for this dialog
* @return the hidden frame * @return the hidden frame
*/ */
private static JFrame createHiddenParentFrame(DialogComponentProvider componentProvider) { private static JFrame createHiddenParentFrame() {
// //
// Note: we expect to only get here when there is no parent window found. This usually // Note: we expect to only get here when there is no parent window found. This usually
@ -118,7 +117,7 @@ public class DockingDialog extends JDialog implements HelpDescriptor {
} }
private DockingDialog(DialogComponentProvider comp, Component centeredOnComponent) { private DockingDialog(DialogComponentProvider comp, Component centeredOnComponent) {
super(createHiddenParentFrame(comp), comp.getTitle(), comp.isModal()); super(createHiddenParentFrame(), comp.getTitle(), comp.isModal());
init(comp); init(comp);
initializeLocationAndSize(centeredOnComponent); initializeLocationAndSize(centeredOnComponent);
} }
@ -299,6 +298,15 @@ public class DockingDialog extends JDialog implements HelpDescriptor {
return component; return component;
} }
/**
* Returns true if the given provider is the provider owned by this dialog.
* @param dcp the provider to check
* @return true if the given provider is the provider owned by this dialog
*/
public boolean containsProvider(DialogComponentProvider dcp) {
return component == dcp;
}
/** /**
* Centers the dialog on the given component. * Centers the dialog on the given component.
* @param c the component to center over. * @param c the component to center over.

View File

@ -461,6 +461,23 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
return isActiveWindowManager && isFocusedProvider; return isActiveWindowManager && isFocusedProvider;
} }
/**
* Returns true if the given provider is in a non-main window (a {@link DetachedWindowNode})
* and is the last component provider in that window.
* @param provider the provider
* @return true if the last provider in a non-main window
*/
public boolean isLastProviderInDetachedWindow(ComponentProvider provider) {
Window providerWindow = getProviderWindow(provider);
WindowNode providerNode = root.getNodeForWindow(providerWindow);
if (!(providerNode instanceof DetachedWindowNode windowNode)) {
return false;
}
return windowNode.getComponentCount() == 1;
}
/** /**
* Sets the visible state of the set of docking windows. * Sets the visible state of the set of docking windows.
* *
@ -944,7 +961,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
if (visibleState) { if (visibleState) {
movePlaceholderToFront(placeholder, false); movePlaceholderToFront(placeholder, false);
if (placeholder.getNode() == null) { if (placeholder.getNode() == null) {
root.add(placeholder); root.addToNewWindow(placeholder);
} }
if (requestFocus) { if (requestFocus) {
setNextFocusPlaceholder(placeholder); setNextFocusPlaceholder(placeholder);
@ -1142,7 +1159,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
void movePlaceholder(ComponentPlaceholder source, Point p) { void movePlaceholder(ComponentPlaceholder source, Point p) {
ComponentNode sourceNode = source.getNode(); ComponentNode sourceNode = source.getNode();
sourceNode.remove(source); sourceNode.remove(source);
root.add(source, p); root.addToNewWindow(source, p);
scheduleUpdate(); scheduleUpdate();
} }

View File

@ -240,7 +240,16 @@ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher
// processed with modal dialogs open. For now, do not let key bindings get processed // processed with modal dialogs open. For now, do not let key bindings get processed
// for modal dialogs. This can be changed in the future if needed. // for modal dialogs. This can be changed in the future if needed.
DockingDialog dialog = (DockingDialog) activeWindow; DockingDialog dialog = (DockingDialog) activeWindow;
return !dialog.isModal(); if (!dialog.isModal()) {
return true;
}
// Allow modal dialogs to process close keystrokes (e.g., ESCAPE) so they can be closed
DialogComponentProvider provider = dialog.getComponent();
if (provider.isCloseKeyStroke(keyStroke)) {
return true;
}
return false; // modal dialog; non-escape key
} }
return true; // default case; allow it through return true; // default case; allow it through
} }
@ -275,6 +284,15 @@ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher
// return false; // return false;
// } // }
// Special Case: We allow Escape to go through. This doesn't seem useful to text widgets
// but does allow for closing of windows. If we find text widgets that need Escape, then
// we will have to update how we make this decision, such as by having the concerned text
// widgets register actions for Escape and then check for that action.
int code = event.getKeyCode();
if (code == KeyEvent.VK_ESCAPE) {
return false;
}
// We've made the executive decision to allow all keys to go through to the text component // We've made the executive decision to allow all keys to go through to the text component
// unless they are modified with the 'Alt'/'Ctrl'/etc keys, unless they directly used // unless they are modified with the 'Alt'/'Ctrl'/etc keys, unless they directly used
// by the text component // by the text component

View File

@ -52,6 +52,12 @@ abstract class Node {
*/ */
abstract List<Node> getChildren(); abstract List<Node> getChildren();
/**
* Returns the number of visible components in this node.
* @return the number of visible components in this node.
*/
abstract int getComponentCount();
/** /**
* Recursively closes all nodes. * Recursively closes all nodes.
*/ */

View File

@ -213,19 +213,19 @@ class RootNode extends WindowNode {
} }
} }
void add(ComponentPlaceholder info) { void addToNewWindow(ComponentPlaceholder placeholder) {
add(info, (Point) null); addToNewWindow(placeholder, (Point) null);
} }
/** /**
* Creates a new sub-window for the given component a positions it at the given location. * Creates a new sub-window for the given component a positions it at the given location.
* *
* @param info the component to be put in its own window. * @param placeholder the component to be put in its own window.
* @param loc the location for the new window. * @param loc the location for the new window.
*/ */
void add(ComponentPlaceholder info, Point loc) { void addToNewWindow(ComponentPlaceholder placeholder, Point loc) {
ComponentNode node = new ComponentNode(winMgr); ComponentNode node = new ComponentNode(winMgr);
info.setNode(node); placeholder.setNode(node);
node.parent = this; node.parent = this;
DetachedWindowNode windowNode = DetachedWindowNode windowNode =
new DetachedWindowNode(winMgr, this, node, dropTargetFactory); new DetachedWindowNode(winMgr, this, node, dropTargetFactory);
@ -233,18 +233,18 @@ class RootNode extends WindowNode {
windowNode.setInitialLocation(loc.x, loc.y); windowNode.setInitialLocation(loc.x, loc.y);
} }
detachedWindows.add(windowNode); detachedWindows.add(windowNode);
info.getNode().add(info); placeholder.getNode().add(placeholder);
info.requestFocusWhenReady(); placeholder.requestFocusWhenReady();
notifyWindowAdded(windowNode); notifyWindowAdded(windowNode);
} }
void add(ComponentPlaceholder info, WindowPosition initialPosition) { void add(ComponentPlaceholder placeholder, WindowPosition initialPosition) {
if (initialPosition == WindowPosition.WINDOW) { if (initialPosition == WindowPosition.WINDOW) {
add(info); addToNewWindow(placeholder);
return; return;
} }
ComponentNode node = new ComponentNode(winMgr); ComponentNode node = new ComponentNode(winMgr);
info.setNode(node); placeholder.setNode(node);
if (child == null) { if (child == null) {
node.parent = this; node.parent = this;
child = node; child = node;
@ -266,7 +266,7 @@ class RootNode extends WindowNode {
} }
child.parent = this; child.parent = this;
} }
info.getNode().add(info); placeholder.getNode().add(placeholder);
} }
/** /**
@ -635,6 +635,11 @@ class RootNode extends WindowNode {
return windowWrapper.getWindow(); return windowWrapper.getWindow();
} }
@Override
int getComponentCount() {
return child.getComponentCount();
}
//================================================================================================== //==================================================================================================
// Inner Classes // Inner Classes
//================================================================================================== //==================================================================================================

View File

@ -205,6 +205,18 @@ class SplitNode extends Node {
return list; return list;
} }
@Override
int getComponentCount() {
int n = 0;
if (child1 != null) {
n += child1.getComponentCount();
}
if (child2 != null) {
n += child2.getComponentCount();
}
return n;
}
@Override @Override
public String toString() { public String toString() {
return printTree(); return printTree();