diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisOptionsDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisOptionsDialog.java index 1ed2d6c751..2d24a9664b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisOptionsDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisOptionsDialog.java @@ -4,9 +4,9 @@ * 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. @@ -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); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/ComponentNode.java b/Ghidra/Framework/Docking/src/main/java/docking/ComponentNode.java index 89b415fb3c..653420cf09 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/ComponentNode.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/ComponentNode.java @@ -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 activeComponents = new ArrayList<>(); + populateActiveComponents(activeComponents); + return activeComponents.size(); } @Override diff --git a/Ghidra/Framework/Docking/src/main/java/docking/DetachedWindowNode.java b/Ghidra/Framework/Docking/src/main/java/docking/DetachedWindowNode.java index e15055d5cd..4d130f1dd1 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/DetachedWindowNode.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/DetachedWindowNode.java @@ -4,9 +4,9 @@ * 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. @@ -101,6 +101,11 @@ class DetachedWindowNode extends WindowNode { } } + @Override + int getComponentCount() { + return child.getComponentCount(); + } + @Override String getTitle() { if (window instanceof JDialog) { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/DialogComponentProvider.java b/Ghidra/Framework/Docking/src/main/java/docking/DialogComponentProvider.java index c9dc664a81..eb0f03c284 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/DialogComponentProvider.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/DialogComponentProvider.java @@ -4,9 +4,9 @@ * 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. @@ -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; } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/DockableToolBarManager.java b/Ghidra/Framework/Docking/src/main/java/docking/DockableToolBarManager.java index f47569818b..5ced712253 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/DockableToolBarManager.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/DockableToolBarManager.java @@ -4,9 +4,9 @@ * 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. @@ -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. */ diff --git a/Ghidra/Framework/Docking/src/main/java/docking/DockingDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/DockingDialog.java index 928e7a804f..df26c0ddf2 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/DockingDialog.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/DockingDialog.java @@ -4,9 +4,9 @@ * 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. @@ -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. diff --git a/Ghidra/Framework/Docking/src/main/java/docking/DockingWindowManager.java b/Ghidra/Framework/Docking/src/main/java/docking/DockingWindowManager.java index e5e6e011c2..da9522a005 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/DockingWindowManager.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/DockingWindowManager.java @@ -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(); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/KeyBindingOverrideKeyEventDispatcher.java b/Ghidra/Framework/Docking/src/main/java/docking/KeyBindingOverrideKeyEventDispatcher.java index bb2d4c9ec3..44a1df76b5 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/KeyBindingOverrideKeyEventDispatcher.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/KeyBindingOverrideKeyEventDispatcher.java @@ -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 diff --git a/Ghidra/Framework/Docking/src/main/java/docking/Node.java b/Ghidra/Framework/Docking/src/main/java/docking/Node.java index 212336cdb2..dabcc015df 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/Node.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/Node.java @@ -4,9 +4,9 @@ * 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. @@ -52,6 +52,12 @@ abstract class Node { */ abstract List 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. */ diff --git a/Ghidra/Framework/Docking/src/main/java/docking/RootNode.java b/Ghidra/Framework/Docking/src/main/java/docking/RootNode.java index 7d960cb544..cd73733aea 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/RootNode.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/RootNode.java @@ -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 //================================================================================================== diff --git a/Ghidra/Framework/Docking/src/main/java/docking/SplitNode.java b/Ghidra/Framework/Docking/src/main/java/docking/SplitNode.java index 12415d6474..54d3132435 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/SplitNode.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/SplitNode.java @@ -4,9 +4,9 @@ * 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. @@ -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();