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();
addApplyButton();
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');
setOkEnabled(true);
setPreferredSize(1000, 600);

View File

@ -227,8 +227,12 @@ class ComponentNode extends Node {
}
}
@Override
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

View File

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

View File

@ -27,13 +27,14 @@ import org.jdesktop.animation.timing.Animator;
import org.jdesktop.animation.timing.TimingTargetAdapter;
import docking.action.*;
import docking.actions.KeyBindingUtils;
import docking.action.builder.ActionBuilder;
import docking.event.mouse.GMouseListenerAdapter;
import docking.menu.DialogToolbarButton;
import docking.util.AnimationUtils;
import docking.widgets.label.GDHtmlLabel;
import generic.theme.GColor;
import generic.theme.GThemeDefaults.Colors.Messages;
import generic.util.WindowUtilities;
import ghidra.util.*;
import ghidra.util.exception.AssertException;
import ghidra.util.task.*;
@ -81,6 +82,7 @@ public class DialogComponentProvider
private TaskMonitorComponent taskMonitorComponent;
private static final KeyStroke ESC_KEYSTROKE = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
private DockingAction closeAction;
private CardLayout progressCardLayout;
private JButton defaultButton;
@ -181,15 +183,31 @@ public class DialogComponentProvider
}
private void installEscapeAction() {
Action escAction = new AbstractAction("ESCAPE") {
@Override
public void actionPerformed(ActionEvent ev) {
escapeCallback();
}
};
KeyBindingUtils.registerAction(rootPanel, ESC_KEYSTROKE, escAction,
JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
closeAction = new ActionBuilder("Close Dialog", title)
.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 */
@ -197,6 +215,16 @@ public class DialogComponentProvider
// 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() {
return id;
}

View File

@ -75,12 +75,15 @@ class DockableToolBarManager {
ToolBarCloseAction closeAction = new ToolBarCloseAction(owner);
closeButtonManager = new ToolBarItemManager(closeAction, winMgr);
CloseLastProviderAction closeLastProviderAction = new CloseLastProviderAction(owner);
ToolBarMenuAction dropDownAction = new ToolBarMenuAction(owner);
menuButtonManager = new ToolBarItemManager(dropDownAction, winMgr);
// we need to add this action to the tool in order to use key bindings
Tool tool = winMgr.getTool();
tool.addLocalAction(provider, closeAction);
tool.addLocalAction(provider, closeLastProviderAction);
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.
*/

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
* running tool.
*
* @param componentProvider the dialog content for this dialog
* @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
@ -118,7 +117,7 @@ public class DockingDialog extends JDialog implements HelpDescriptor {
}
private DockingDialog(DialogComponentProvider comp, Component centeredOnComponent) {
super(createHiddenParentFrame(comp), comp.getTitle(), comp.isModal());
super(createHiddenParentFrame(), comp.getTitle(), comp.isModal());
init(comp);
initializeLocationAndSize(centeredOnComponent);
}
@ -299,6 +298,15 @@ public class DockingDialog extends JDialog implements HelpDescriptor {
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.
* @param c the component to center over.

View File

@ -461,6 +461,23 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
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.
*
@ -944,7 +961,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
if (visibleState) {
movePlaceholderToFront(placeholder, false);
if (placeholder.getNode() == null) {
root.add(placeholder);
root.addToNewWindow(placeholder);
}
if (requestFocus) {
setNextFocusPlaceholder(placeholder);
@ -1142,7 +1159,7 @@ public class DockingWindowManager implements PropertyChangeListener, Placeholder
void movePlaceholder(ComponentPlaceholder source, Point p) {
ComponentNode sourceNode = source.getNode();
sourceNode.remove(source);
root.add(source, p);
root.addToNewWindow(source, p);
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
// for modal dialogs. This can be changed in the future if needed.
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
}
@ -275,6 +284,15 @@ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher
// 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
// unless they are modified with the 'Alt'/'Ctrl'/etc keys, unless they directly used
// by the text component

View File

@ -52,6 +52,12 @@ abstract class Node {
*/
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.
*/

View File

@ -213,19 +213,19 @@ class RootNode extends WindowNode {
}
}
void add(ComponentPlaceholder info) {
add(info, (Point) null);
void addToNewWindow(ComponentPlaceholder placeholder) {
addToNewWindow(placeholder, (Point) null);
}
/**
* 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.
*/
void add(ComponentPlaceholder info, Point loc) {
void addToNewWindow(ComponentPlaceholder placeholder, Point loc) {
ComponentNode node = new ComponentNode(winMgr);
info.setNode(node);
placeholder.setNode(node);
node.parent = this;
DetachedWindowNode windowNode =
new DetachedWindowNode(winMgr, this, node, dropTargetFactory);
@ -233,18 +233,18 @@ class RootNode extends WindowNode {
windowNode.setInitialLocation(loc.x, loc.y);
}
detachedWindows.add(windowNode);
info.getNode().add(info);
info.requestFocusWhenReady();
placeholder.getNode().add(placeholder);
placeholder.requestFocusWhenReady();
notifyWindowAdded(windowNode);
}
void add(ComponentPlaceholder info, WindowPosition initialPosition) {
void add(ComponentPlaceholder placeholder, WindowPosition initialPosition) {
if (initialPosition == WindowPosition.WINDOW) {
add(info);
addToNewWindow(placeholder);
return;
}
ComponentNode node = new ComponentNode(winMgr);
info.setNode(node);
placeholder.setNode(node);
if (child == null) {
node.parent = this;
child = node;
@ -266,7 +266,7 @@ class RootNode extends WindowNode {
}
child.parent = this;
}
info.getNode().add(info);
placeholder.getNode().add(placeholder);
}
/**
@ -635,6 +635,11 @@ class RootNode extends WindowNode {
return windowWrapper.getWindow();
}
@Override
int getComponentCount() {
return child.getComponentCount();
}
//==================================================================================================
// Inner Classes
//==================================================================================================

View File

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