Update key processing to have the notion of both valid and enabled

This commit is contained in:
dragonmacher 2025-04-15 19:08:17 -04:00
parent 13834fabaa
commit d59d6e7d92
14 changed files with 606 additions and 339 deletions

View file

@ -15,13 +15,15 @@
*/
package ghidra.app.plugin.core.datamgr;
import java.util.ArrayList;
import java.util.List;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.Transferable;
import java.util.*;
import javax.swing.tree.TreePath;
import docking.widgets.tree.GTree;
import docking.widgets.tree.GTreeNode;
import docking.widgets.tree.support.GTreeNodeTransferable;
import ghidra.app.context.ProgramActionContext;
import ghidra.app.plugin.core.datamgr.archive.BuiltInSourceArchive;
import ghidra.app.plugin.core.datamgr.archive.ProjectArchive;
@ -37,6 +39,8 @@ public class DataTypesActionContext extends ProgramActionContext implements Doma
private DataTypeArchiveGTree archiveGTree;
private List<DomainFile> domainFiles;
private List<GTreeNode> clipboardNodes;
public DataTypesActionContext(DataTypesProvider provider, Program program,
DataTypeArchiveGTree archiveGTree, GTreeNode clickedNode) {
this(provider, program, archiveGTree, clickedNode, false);
@ -44,13 +48,33 @@ public class DataTypesActionContext extends ProgramActionContext implements Doma
public DataTypesActionContext(DataTypesProvider provider, Program program,
DataTypeArchiveGTree archiveGTree, GTreeNode clickedNode, boolean isToolbarAction) {
super(provider, program, archiveGTree);
this.archiveGTree = archiveGTree;
this.clickedNode = clickedNode;
this.isToolbarAction = isToolbarAction;
}
public List<GTreeNode> getClipboardNodes() {
if (clipboardNodes != null) {
return clipboardNodes;
}
// cache, since this could be slow
DataTypesProvider dtProvider = (DataTypesProvider) getComponentProvider();
DataTypeManagerPlugin plugin = dtProvider.getPlugin();
Clipboard clipboard = plugin.getClipboard();
Transferable transferable = clipboard.getContents(this);
if (transferable instanceof GTreeNodeTransferable) {
GTreeNodeTransferable gtTransferable = (GTreeNodeTransferable) transferable;
clipboardNodes = gtTransferable.getAllData();
}
if (clipboardNodes == null) {
clipboardNodes = Collections.emptyList();
}
return clipboardNodes;
}
public boolean isToolbarAction() {
return isToolbarAction;
}

View file

@ -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.
@ -16,11 +15,7 @@
*/
package ghidra.app.plugin.core.datamgr.actions;
import ghidra.app.plugin.core.datamgr.DataTypeManagerPlugin;
import ghidra.app.plugin.core.datamgr.tree.DataTypeTreeNode;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.Transferable;
import java.awt.event.KeyEvent;
import java.util.List;
@ -28,7 +23,9 @@ import docking.ActionContext;
import docking.action.DockingAction;
import docking.action.KeyBindingData;
import docking.widgets.tree.GTreeNode;
import docking.widgets.tree.support.GTreeNodeTransferable;
import ghidra.app.plugin.core.datamgr.DataTypeManagerPlugin;
import ghidra.app.plugin.core.datamgr.DataTypesActionContext;
import ghidra.app.plugin.core.datamgr.tree.DataTypeTreeNode;
public class ClearCutAction extends DockingAction {
private Clipboard clipboard;
@ -42,26 +39,36 @@ public class ClearCutAction extends DockingAction {
setEnabled(true);
}
@Override
public boolean isValidContext(ActionContext context) {
//
// This action is particular about when it is valid. This is so that it does not interfere
// with Escape key presses for the parent window, except when this action has work to do.
//
if (!(context instanceof DataTypesActionContext dtc)) {
return false;
}
return !dtc.getClipboardNodes().isEmpty();
}
@Override
public boolean isEnabledForContext(ActionContext context) {
// If we are valid, then we are enabled (see isValidContext()). Most actions are always
// valid, but only sometimes enabled. We use the valid check to remove ourselves completely
// from the workflow. But, if we are valid, then we are also enabled.
return true;
}
@Override
public void actionPerformed(ActionContext context) {
Transferable transferable = clipboard.getContents(this);
if (transferable instanceof GTreeNodeTransferable) {
GTreeNodeTransferable gtTransferable = (GTreeNodeTransferable) transferable;
List<GTreeNode> nodeList = gtTransferable.getAllData();
if (nodeList.isEmpty()) {
return;
}
DataTypesActionContext dtc = (DataTypesActionContext) context;
List<GTreeNode> nodeList = dtc.getClipboardNodes();
DataTypeTreeNode node = (DataTypeTreeNode) nodeList.get(0);
if (node.isCut()) {
clipboard.setContents(null, null);
}
}
}
}

View file

@ -1327,7 +1327,7 @@ public class DialogComponentProvider
private void addKeyBindingAction(DockingActionIf action) {
DialogActionProxy proxy = new DialogActionProxy(action);
DialogActionProxy proxy = new DialogActionProxy(this, action);
keyBindingProxyActions.add(proxy);
// The tool will be null when clients add actions to this dialog before it has been shown.
@ -1481,8 +1481,11 @@ public class DialogComponentProvider
*/
private class DialogActionProxy extends DockingActionProxy {
public DialogActionProxy(DockingActionIf dockingAction) {
private DialogComponentProvider provider;
public DialogActionProxy(DialogComponentProvider provider, DockingActionIf dockingAction) {
super(dockingAction);
this.provider = provider;
}
@Override
@ -1494,5 +1497,18 @@ public class DialogComponentProvider
public ToolBarData getToolBarData() {
return null;
}
@Override
public boolean isEnabledForContext(ActionContext context) {
if (context instanceof DialogActionContext dialogContext) {
DialogComponentProvider contextProvider =
dialogContext.getDialogComponentProvider();
if (provider != contextProvider) {
return false;
}
return dockingAction.isEnabledForContext(context);
}
return false;
}
}
}

View file

@ -31,7 +31,7 @@ public class DockingActionProxy
implements ToggleDockingActionIf, MultiActionDockingActionIf, PropertyChangeListener {
private WeakSet<PropertyChangeListener> propertyListeners =
WeakDataStructureFactory.createSingleThreadAccessWeakSet();
private final DockingActionIf dockingAction;
protected final DockingActionIf dockingAction;
public DockingActionProxy(DockingActionIf dockingAction) {
this.dockingAction = dockingAction;

View file

@ -15,8 +15,7 @@
*/
package docking;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.Component;
import java.util.List;
import javax.swing.AbstractAction;
@ -47,33 +46,12 @@ public abstract class DockingKeyBindingAction extends AbstractAction {
return true; // always enable; this is a internal action that cannot be disabled
}
public void reportNotEnabled() {
String name = dockingAction.getName();
String ksText = KeyBindingUtils.parseKeyStroke(keyStroke);
String message = "Action '%s' (%s) not currently enabled".formatted(name, ksText);
tool.setStatusInfo(message, true);
Toolkit.getDefaultToolkit().beep();
}
public abstract KeyBindingPrecedence getKeyBindingPrecedence();
public abstract ExecutableAction getExecutableAction(Component focusOwner);
public boolean isSystemKeybindingPrecedence() {
return false;
}
@Override
public void actionPerformed(final ActionEvent e) {
tool.setStatusInfo("");
ComponentProvider provider = tool.getActiveComponentProvider();
ActionContext context = getLocalContext(provider);
context.setSourceObject(e.getSource());
dockingAction.actionPerformed(context);
}
public List<DockingActionIf> getValidActions(Object source) {
return getActions(); // the action for this class is always enabled and valid
}
protected ActionContext getLocalContext(ComponentProvider localProvider) {
if (localProvider == null) {
return new DefaultActionContext();
@ -90,4 +68,5 @@ public abstract class DockingKeyBindingAction extends AbstractAction {
public List<DockingActionIf> getActions() {
return List.of(dockingAction);
}
}

View file

@ -15,37 +15,24 @@
*/
package docking;
import docking.action.DockingActionIf;
import docking.action.ToggleDockingActionIf;
import generic.json.Json;
import java.awt.Component;
public class ExecutableAction {
/**
* A class used by the {@link KeyBindingOverrideKeyEventDispatcher}. It represents an action and
* the context in which that action should operate if {@link #execute()} is called. This class is
* created for each keystroke that maps to a tool action.
* <p>
* This is not meant to be used outside of this API.
*/
public interface ExecutableAction {
DockingActionIf action;
ActionContext context;
public boolean isValid();
public ExecutableAction(DockingActionIf action, ActionContext context) {
this.action = action;
this.context = context;
}
public boolean isEnabled();
public void execute() {
// Toggle actions do not toggle its state directly therefor we have to do it for
// them before we execute the action.
if (action instanceof ToggleDockingActionIf) {
ToggleDockingActionIf toggleAction = (ToggleDockingActionIf) action;
toggleAction.setSelected(!toggleAction.isSelected());
}
public void reportNotEnabled(Component focusOwner);
action.actionPerformed(context);
}
public KeyBindingPrecedence getKeyBindingPrecedence();
public DockingActionIf getAction() {
return action;
}
@Override
public String toString() {
return Json.toString(action);
}
public void execute();
}

View file

@ -0,0 +1,32 @@
/* ###
* 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;
/**
* A class to track an action's precedence and enablement
* @param precedence the precedence
* @param isValid true if valid
* @param isEnabled true if enabled
*/
public record KbEnabledState(KeyBindingPrecedence precedence, boolean isValid,
boolean isEnabled) {
public KbEnabledState {
if (!isValid && isEnabled) {
throw new IllegalArgumentException("Cannot be enable if not also valid");
}
}
}

View file

@ -20,12 +20,10 @@ import static docking.KeyBindingPrecedence.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.List;
import javax.swing.*;
import javax.swing.text.JTextComponent;
import docking.action.*;
import docking.actions.KeyBindingUtils;
import docking.menu.keys.MenuKeyProcessor;
import ghidra.util.bean.GGlassPane;
@ -57,7 +55,7 @@ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher
* <b>Posterity Note:</b> While debugging we will not get a KeyEvent.KEY_RELEASED event if
* the focus changes from the application to the debugger tool.
*/
private DockingKeyBindingAction inProgressAction;
private ExecutableAction inProgressAction;
/**
* Provides the current focus owner. This allows for dependency injection.
@ -119,75 +117,56 @@ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher
return true;
}
// Special case for when we use one of our built-in menu navigation actions. We ignore
// docking actions if a menu is open (this is done in action.getEnabledState()).
if (MenuKeyProcessor.processMenuKeyEvent(event)) {
return true;
}
DockingKeyBindingAction action = getDockingKeyBindingActionForEvent(event);
DockingKeyBindingAction action = getActionForEvent(event);
if (action == null) {
return false; // let the normal event flow continue
}
// *Special*, System key bindings--these can always be processed and are a higher priority
if (processSystemActionPrecedence(action, event)) {
Component focusOwner = focusProvider.getFocusOwner();
ExecutableAction executableAction = action.getExecutableAction(focusOwner);
if (processSystemActionPrecedence(executableAction, event)) {
return true;
}
// some known special cases that we don't wish to process
if (!isValidContextForAction(event, action)) {
return false;
}
if (willBeHandledByTextComponent(event)) {
return false;
}
// no actions valid at all
// return
// actions that are valid, but not enabled
// also, is applicable:
// isValidContext();
// is action for active local for provider;
// is focused
// actions that are enabled
KeyBindingPrecedence keyBindingPrecedence = getValidKeyBindingPrecedence(action);
if (keyBindingPrecedence == null) {
// Note: we used to return false here. Returning false allows Java to handle a given
// key stroke when our actions are disabled. We have decided it is simpler to
// always consume a given key stroke when we have actions registered. This
// prevents inconsistent action firing between Ghidra and Java, depending upon
// Ghidra's action enablement. If we find a case that is broken by this change,
// then we will need a more robust solution here.
// action.reportNotEnabled();
// return true;
if (!executableAction.isValid()) {
// The action is not currently valid for the given focus owner. Let all key strokes go
// to Java when we have no valid context. This allows keys like Escape to work on Java
// widgets.
return false;
}
if (!executableAction.isEnabled()) {
// The action is valid, but is not enabled. In this case, we do not want Java to get
// the event. Instead, let the user know that they cannot perform the requested action.
// We keep the action from Java in this case to keep the event processing consistent
// whether or not the action is enabled.
executableAction.reportNotEnabled(focusOwner);
return true;
}
// Process the key event in precedence order.
// If it processes the event at any given level, the short-circuit operator will kick out.
// Finally, if the exception statement is reached, then someone has added a new level
// of precedence that this algorithm has not taken into account!
// @formatter:off
return processKeyListenerPrecedence(action, keyBindingPrecedence, event) ||
processComponentActionMapPrecedence(action, keyBindingPrecedence, event) ||
processActionAtPrecedence(DefaultLevel, keyBindingPrecedence, action, event) ||
return processKeyListenerPrecedence(executableAction, event) ||
processComponentActionMapPrecedence(executableAction, event) ||
processActionAtPrecedence(DefaultLevel, executableAction, event) ||
throwAssertException();
// @formatter:on
}
private KeyBindingPrecedence getValidKeyBindingPrecedence(DockingKeyBindingAction action) {
if (action instanceof MultipleKeyAction) {
MultipleKeyAction multiAction = (MultipleKeyAction) action;
return multiAction.geValidKeyBindingPrecedence(focusProvider.getFocusOwner());
}
return action.getKeyBindingPrecedence();
}
/**
* Returns true if the given key event should be blocked (i.e., not processed by us or Java).
*/
@ -226,58 +205,15 @@ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher
private boolean actionInProgress(KeyEvent event) {
boolean wasInProgress = inProgressAction != null;
if (event.getID() == KeyEvent.KEY_RELEASED) {
DockingKeyBindingAction action = inProgressAction;
ExecutableAction action = inProgressAction;
inProgressAction = null;
KeyStroke keyStroke = KeyStroke.getKeyStrokeForEvent(event);
// note: this call has no effect if 'action' is null
Object source = event.getSource();
int modifiersEx = event.getModifiersEx();
SwingUtilities.notifyAction(action, keyStroke, event, source, modifiersEx);
if (action != null) {
action.execute();
}
}
return wasInProgress;
}
private boolean isValidContextForAction(KeyEvent event, DockingKeyBindingAction kbAction) {
Window activeWindow = focusProvider.getActiveWindow();
if (!(activeWindow instanceof DockingDialog dialog)) {
return true; // allow all non-dialog windows to process events
}
// The choice to ignore modal dialogs was made long ago. We cannot remember why the
// choice was made, but speculate that odd things can happen when keybindings are
// 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.
if (!dialog.isModal()) {
return true;
}
// Allow modal dialogs to process their own actions
DialogComponentProvider provider = dialog.getComponent();
List<DockingActionIf> actions = kbAction.getValidActions(event.getSource());
if (actions.isEmpty()) {
return false; // no actions; not a valid key stroke for this dialog
}
for (DockingActionIf action : actions) {
if (!isAllowedDialogAction(provider, action)) {
return false;
}
}
return true; // all actions belong to the active dialog; this is a valid action
}
private boolean isAllowedDialogAction(DialogComponentProvider provider,
DockingActionIf action) {
if (action instanceof ComponentBasedDockingAction) {
return true; // these actions work on low-level components, which may live in dialogs
}
return provider.isDialogKeyBindingAction(action);
}
private boolean isSettingKeyBindings(KeyEvent event) {
Component destination = event.getComponent();
if (destination == null) {
@ -370,7 +306,7 @@ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher
throw new AssertException("New precedence added to KeyBindingPrecedence?");
}
private boolean processSystemActionPrecedence(DockingKeyBindingAction action,
private boolean processSystemActionPrecedence(ExecutableAction executableAction,
KeyEvent event) {
if (isSettingKeyBindings(event)) {
@ -379,21 +315,17 @@ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher
return false;
}
if (!action.isSystemKeybindingPrecedence()) {
KeyBindingPrecedence precedence = executableAction.getKeyBindingPrecedence();
if (precedence != SystemActionsLevel) {
return false;
}
if (inProgressAction != null) {
inProgressAction = executableAction; // this will be handled on the release
return true;
}
inProgressAction = action; // this will be handled on the release
return true;
}
private boolean processKeyListenerPrecedence(DockingKeyBindingAction action,
KeyBindingPrecedence keyBindingPrecedence, KeyEvent e) {
if (processActionAtPrecedence(KeyBindingPrecedence.KeyListenerLevel, keyBindingPrecedence,
private boolean processKeyListenerPrecedence(ExecutableAction action, KeyEvent e) {
if (processActionAtPrecedence(KeyBindingPrecedence.KeyListenerLevel,
action, e)) {
return true;
}
@ -407,10 +339,8 @@ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher
return false;
}
private boolean processComponentActionMapPrecedence(DockingKeyBindingAction action,
KeyBindingPrecedence keyBindingPrecedence, KeyEvent event) {
if (processActionAtPrecedence(ActionMapLevel, keyBindingPrecedence, action, event)) {
private boolean processComponentActionMapPrecedence(ExecutableAction action, KeyEvent event) {
if (processActionAtPrecedence(ActionMapLevel, action, event)) {
return true;
}
@ -422,11 +352,11 @@ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher
return false;
}
private boolean processActionAtPrecedence(KeyBindingPrecedence precedence,
KeyBindingPrecedence keyBindingPrecedence, DockingKeyBindingAction action,
KeyEvent event) {
private boolean processActionAtPrecedence(KeyBindingPrecedence keyBindingPrecedence,
ExecutableAction action, KeyEvent event) {
if (keyBindingPrecedence != precedence) {
KeyBindingPrecedence actionPrecedence = action.getKeyBindingPrecedence();
if (keyBindingPrecedence != actionPrecedence) {
return false;
}
@ -506,7 +436,7 @@ public class KeyBindingOverrideKeyEventDispatcher implements KeyEventDispatcher
* @param event The key event to check.
* @return An action, if one is available for the given key event, in the current context.
*/
private DockingKeyBindingAction getDockingKeyBindingActionForEvent(KeyEvent event) {
private DockingKeyBindingAction getActionForEvent(KeyEvent event) {
DockingWindowManager activeManager = getActiveDockingWindowManager();
if (activeManager == null) {
return null;

View file

@ -22,6 +22,7 @@ import java.util.List;
import javax.swing.*;
import docking.action.DockingActionIf;
import docking.action.ToggleDockingActionIf;
import docking.event.mouse.GMouseListenerAdapter;
import docking.widgets.label.GIconLabel;
import docking.widgets.label.GLabel;
@ -33,25 +34,27 @@ import docking.widgets.label.GLabel;
public class MultiActionDialog extends DialogComponentProvider {
private String keystrokeName;
private List<ExecutableAction> list;
private JList<String> actionList;
private DefaultListModel<String> listModel;
private List<DockingActionIf> actions;
private ActionContext context;
/**
* Constructor
* @param keystrokeName keystroke name
* @param list list of actions
* @param actions list of actions
* @param context the context
*/
public MultiActionDialog(String keystrokeName, List<ExecutableAction> list) {
public MultiActionDialog(String keystrokeName, List<DockingActionIf> actions,
ActionContext context) {
super("Select Action", true);
this.keystrokeName = keystrokeName;
this.context = context;
init();
setActionList(list);
setActionList(actions);
}
/**
* The callback method for when the "OK" button is pressed.
*/
@Override
protected void okCallback() {
maybeDoAction();
@ -65,21 +68,23 @@ public class MultiActionDialog extends DialogComponentProvider {
close();
ExecutableAction actionProxy = list.get(index);
actionProxy.execute();
DockingActionIf action = actions.get(index);
// Toggle actions do not toggle its state directly therefor we have to do it for
// them before we execute the action.
if (action instanceof ToggleDockingActionIf) {
ToggleDockingActionIf toggleAction = (ToggleDockingActionIf) action;
toggleAction.setSelected(!toggleAction.isSelected());
}
/**
* Set the list of actions that are enabled
* @param list list of actions selected
*/
public void setActionList(List<ExecutableAction> list) {
action.actionPerformed(context);
}
private void setActionList(List<DockingActionIf> actions) {
okButton.setEnabled(false);
this.list = list;
this.actions = actions;
listModel.clear();
for (int i = 0; i < list.size(); i++) {
ExecutableAction actionProxy = list.get(i);
DockingActionIf action = actionProxy.getAction();
for (DockingActionIf action : actions) {
listModel.addElement(action.getName() + " (" + action.getOwnerDescription() + ")");
}
actionList.setSelectedIndex(0);

View file

@ -0,0 +1,71 @@
/* ###
* 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;
import java.awt.Component;
import javax.help.UnsupportedOperationException;
import docking.action.DockingActionIf;
import docking.action.ToggleDockingActionIf;
public class SystemExecutableAction implements ExecutableAction {
private DockingActionIf action;
private ActionContext context;
public SystemExecutableAction(DockingActionIf action, ActionContext context) {
this.action = action;
this.context = context;
}
@Override
public boolean isValid() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public void reportNotEnabled(Component focusOwner) {
// we are always enabled
throw new UnsupportedOperationException();
}
@Override
public KeyBindingPrecedence getKeyBindingPrecedence() {
return action.getKeyBindingData().getKeyBindingPrecedence();
}
@Override
public void execute() {
// Toggle actions do not toggle its state directly therefor we have to do it for
// them before we execute the action.
if (action instanceof ToggleDockingActionIf) {
ToggleDockingActionIf toggleAction = (ToggleDockingActionIf) action;
toggleAction.setSelected(!toggleAction.isSelected());
}
action.actionPerformed(context);
}
@Override
public String toString() {
return action.getFullName();
}
}

View file

@ -20,6 +20,7 @@ import java.awt.event.ActionEvent;
import java.util.*;
import java.util.List;
import javax.help.UnsupportedOperationException;
import javax.swing.*;
import docking.*;
@ -110,112 +111,115 @@ public class MultipleKeyAction extends DockingKeyBindingAction {
}
@Override
public List<DockingActionIf> getValidActions(Object source) {
if (ignoreActionWhileMenuShowing()) {
return List.of();
public void actionPerformed(ActionEvent event) {
// A vestige from when we used to send this class through the Swing API. Execution is now
// done on the ExecutableAction this class creates.
throw new UnsupportedOperationException();
}
List<DockingActionIf> validActions = new ArrayList<>();
List<ExecutableAction> proxyActions = getActionsForCurrentOrDefaultContext(source);
for (ExecutableAction proxy : proxyActions) {
DockingActionIf action = proxy.getAction();
validActions.add(action);
}
return validActions;
}
private boolean ignoreActionWhileMenuShowing(ExecutableAction action) {
@Override
public void actionPerformed(final ActionEvent event) {
// Build list of actions which are valid in current context
List<ExecutableAction> list = getActionsForCurrentOrDefaultContext(event.getSource());
// If menu active, disable all key bindings
if (ignoreActionWhileMenuShowing()) {
return;
}
// If more than one action, prompt user for selection
if (list.size() > 1) {
// popup dialog to show multiple actions
MultiActionDialog dialog =
new MultiActionDialog(KeyBindingUtils.parseKeyStroke(keyStroke), list);
// doing the show in an invoke later seems to fix a strange swing bug that lock up
// the program if you tried to invoke a new action too quickly after invoking
// it the first time
Swing.runLater(() -> DockingWindowManager.showDialog(dialog));
}
else if (list.size() == 1) {
ExecutableAction actionProxy = list.get(0);
tool.setStatusInfo("");
actionProxy.execute();
}
else {
String name = (String) getValue(Action.NAME);
tool.setStatusInfo("Action (" + name + ") not valid in this context!", true);
Toolkit.getDefaultToolkit().beep();
}
}
private boolean ignoreActionWhileMenuShowing() {
if (getKeyBindingPrecedence() == KeyBindingPrecedence.SystemActionsLevel) {
return false; // allow system bindings through "no matter what!"
KeyBindingPrecedence precedence = action.getKeyBindingPrecedence();
if (precedence == KeyBindingPrecedence.SystemActionsLevel) {
// Allow system bindings through. This allows actions like Help to work for menus.
return false;
}
MenuSelectionManager menuManager = MenuSelectionManager.defaultManager();
return menuManager.getSelectedPath().length != 0;
}
private List<ExecutableAction> getValidContextActions(ActionContext localContext,
private ExecutableAction createNonDialogExecutableAction(ActionContext localContext,
Map<Class<? extends ActionContext>, ActionContext> contextMap) {
List<ExecutableAction> list = new ArrayList<>();
boolean hasLocalActionsForKeyBinding = false;
MultiExecutableAction multiAction = new MultiExecutableAction();
//
// 1) Prefer local actions for the active provider
//
getLocalContextActions(localContext, multiAction);
if (multiAction.isValid()) {
// At this point, we have local docking actions that may or may not be enabled. Exit
// so that any component specific actions or global found below will not interfere with
// the provider's local actions
return multiAction;
}
//
// 2) Check for actions local to the source component (e.g., GTable and GTree)
//
getLocalComponentActions(localContext, multiAction);
if (multiAction.isValid()) {
// At this point, we have local component actions that may or may not be enabled. Exit
// so that any global actions found below will not interfere with these component
// actions.
return multiAction;
}
//
// 3) Check for global actions using the current context
//
getGlobalActions(localContext, multiAction);
if (multiAction.isValid()) {
// We have found global actions that are valid for the current local context. Do not
// also look for global actions that work for the default context.
return multiAction;
}
//
// 4) Check for global actions using the default context. This is a final fallback to allow
// global actions to work that are unrelated to the current active component's context.
//
getGlobalDefaultContextActions(contextMap, multiAction);
return multiAction;
}
private void getLocalContextActions(ActionContext localContext,
MultiExecutableAction multiAction) {
for (ActionData actionData : actions) {
if (actionData.isMyProvider(localContext)) {
hasLocalActionsForKeyBinding = true;
if (isValidAndEnabled(actionData, localContext)) {
list.add(new ExecutableAction(actionData.action, localContext));
if (!actionData.isMyProvider(localContext)) {
continue;
}
if (!isValid(actionData, localContext)) {
continue;
}
multiAction.setLocal(true);
multiAction.setContext(localContext);
multiAction.addValidAction(actionData.action);
if (isEnabled(actionData, localContext)) {
multiAction.addEnabledAction(actionData.action);
}
}
}
if (hasLocalActionsForKeyBinding) {
// At this point, we have local actions that may or may not be enabled. Return here
// so that any component specific actions found below will not interfere with the
// provider's local actions
return list;
}
private void getLocalComponentActions(ActionContext localContext,
MultiExecutableAction multiAction) {
//
// 2) Check for actions local to the source component
//
for (ActionData actionData : actions) {
if (!(actionData.action instanceof ComponentBasedDockingAction componentAction)) {
continue;
}
if (componentAction.isValidComponentContext(localContext)) {
hasLocalActionsForKeyBinding = true;
if (isValidAndEnabled(actionData, localContext)) {
list.add(new ExecutableAction(actionData.action, localContext));
if (!componentAction.isValidComponentContext(localContext)) {
continue;
}
multiAction.setContext(localContext);
multiAction.addValidAction(actionData.action);
if (isEnabled(actionData, localContext)) {
multiAction.addEnabledAction(actionData.action);
}
}
}
if (hasLocalActionsForKeyBinding) {
// We have locals, ignore the globals. This prevents global actions from processing
// the given keybinding when a local action exits, regardless of enablement.
return list;
}
private void getGlobalActions(ActionContext localContext,
MultiExecutableAction multiAction) {
//
// 3) Check for default context actions
//
for (ActionData actionData : actions) {
if (!actionData.isGlobalAction()) {
continue;
@ -223,13 +227,25 @@ public class MultipleKeyAction extends DockingKeyBindingAction {
// When looking for context matches, we prefer local context, even though this
// is a 'global' action. This allows more specific context to be used when available
if (isValidAndEnabled(actionData, localContext)) {
list.add(new ExecutableAction(actionData.action, localContext));
if (!isValid(actionData, localContext)) {
continue;
}
// this happens if we are in a dialog, default context is not used
if (contextMap == null) {
multiAction.setContext(localContext);
multiAction.addValidAction(actionData.action);
if (isEnabled(actionData, localContext)) {
multiAction.addEnabledAction(actionData.action);
}
}
}
private void getGlobalDefaultContextActions(
Map<Class<? extends ActionContext>, ActionContext> contextMap,
MultiExecutableAction multiAction) {
for (ActionData actionData : actions) {
if (!actionData.isGlobalAction()) {
continue;
}
@ -238,19 +254,33 @@ public class MultipleKeyAction extends DockingKeyBindingAction {
}
ActionContext defaultContext = contextMap.get(actionData.getContextType());
if (isValidAndEnabled(actionData, defaultContext)) {
list.add(new ExecutableAction(actionData.action, defaultContext));
}
}
return list;
if (!isValid(actionData, defaultContext)) {
continue;
}
private boolean isValidAndEnabled(ActionData actionData, ActionContext context) {
multiAction.setContext(defaultContext);
multiAction.addValidAction(actionData.action);
if (isEnabled(actionData, defaultContext)) {
multiAction.addEnabledAction(actionData.action);
}
}
}
private boolean isValid(ActionData actionData, ActionContext context) {
if (context == null) {
return false;
}
DockingActionIf a = actionData.action;
return a.isValidContext(context) && a.isEnabledForContext(context);
return a.isValidContext(context);
}
private boolean isEnabled(ActionData actionData, ActionContext context) {
if (context == null) {
return false;
}
DockingActionIf a = actionData.action;
return a.isEnabledForContext(context);
}
@Override
@ -259,65 +289,87 @@ public class MultipleKeyAction extends DockingKeyBindingAction {
}
@Override
public KeyBindingPrecedence getKeyBindingPrecedence() {
return geValidKeyBindingPrecedence(null);
public ExecutableAction getExecutableAction(Component source) {
ExecutableAction action = createExecutableAction(source);
// If menu active, disable all default key bindings
if (ignoreActionWhileMenuShowing(action)) {
return new MultiExecutableAction();
}
/**
* This is a special version of {@link #getKeyBindingPrecedence()} that allows the internal
* key event processing to specify the source component when determining how precedence should
* be established for the actions contained herein.
* @param source the component; may be null
* @return the precedence; may be null
*/
public KeyBindingPrecedence geValidKeyBindingPrecedence(Component source) {
List<ExecutableAction> validActions = getActionsForCurrentOrDefaultContext(source);
if (validActions.isEmpty()) {
return null; // a signal that no actions are valid for the current context
return action;
}
if (validActions.size() != 1) {
return KeyBindingPrecedence.DefaultLevel;
}
ExecutableAction actionProxy = validActions.get(0);
DockingActionIf action = actionProxy.getAction();
return action.getKeyBindingData().getKeyBindingPrecedence();
}
private List<ExecutableAction> getActionsForCurrentOrDefaultContext(Object eventSource) {
private ExecutableAction createExecutableAction(Object eventSource) {
DockingWindowManager dwm = tool.getWindowManager();
Window window = getWindow(dwm, eventSource);
if (window instanceof DockingDialog) {
return getDialogActions(window);
return createDialogActions(eventSource, window);
}
ComponentProvider localProvider = getProvider(dwm, eventSource);
ActionContext localContext = getLocalContext(localProvider);
localContext.setSourceObject(eventSource);
Map<Class<? extends ActionContext>, ActionContext> contextMap =
tool.getWindowManager().getDefaultActionContextMap();
List<ExecutableAction> validActions = getValidContextActions(localContext, contextMap);
return validActions;
dwm.getDefaultActionContextMap();
return createNonDialogExecutableAction(localContext, contextMap);
}
private List<ExecutableAction> getDialogActions(Window window) {
private ExecutableAction createDialogActions(Object eventSource, Window window) {
MultiExecutableAction multiAction = new MultiExecutableAction();
DockingDialog dockingDialog = (DockingDialog) window;
DialogComponentProvider provider = dockingDialog.getDialogComponent();
if (provider == null) {
// this can happen if the dialog is closed during key event processing
return Collections.emptyList();
return multiAction;
}
ActionContext context = provider.getActionContext(null);
if (context == null) {
return Collections.emptyList();
return multiAction;
}
List<ExecutableAction> validActions = getValidContextActions(context, null);
return validActions;
//
// 1) Check for local actions
//
// Note: dialog key binding actions are proxy actions that get added to the tool as global
// actions. Thus, there are no 'local' actions for the dialog.
//
// 2) Check for actions local to the source component (e.g., GTable and GTree)
//
getLocalComponentActions(context, multiAction);
if (multiAction.isValid()) {
// At this point, we have local component actions that may or may not be enabled. Exit
// so that any global actions found below will not interfere with these component
// actions.
return multiAction;
}
//
// 3) Check for global actions using the current context. As noted above, at the time of
// writing, dialog actions are all registered at the global level.
//
getGlobalActions(context, multiAction);
// The choice to ignore global actions for modal dialogs was made long ago. We cannot
// remember why the choice was made, but speculate that odd things can happen when
// keybindings are processed with modal dialogs open. For now, do not let non-dialog
// actions get processed for modal dialogs. This can be changed in the future if needed.
if (provider.isModal()) {
multiAction.filterAndKeepOnlyDialogActions(provider);
}
// Note: we currently do not use *default* global actions in dialogs. It is not clear if
// this decision was intentional.
// if (!provider.isModal()) {
// getGlobalDefaultContextActions(...);
// }
return multiAction;
}
private ComponentProvider getProvider(DockingWindowManager dwm, Object eventSource) {
@ -383,6 +435,9 @@ public class MultipleKeyAction extends DockingKeyBindingAction {
}
boolean isMyProvider(ActionContext localContext) {
if (provider == null) {
return false;
}
ComponentProvider otherProvider = localContext.getComponentProvider();
return provider == otherProvider;
}
@ -392,6 +447,142 @@ public class MultipleKeyAction extends DockingKeyBindingAction {
String providerString = provider == null ? "" : provider.toString() + " - ";
return providerString + action;
}
}
/**
* An extension of {@link ExecutableAction} that itself contains 0 or more
* {@link ExecutableAction}s. This class is used to create a snapshot of all actions valid and
* enabled for a given keystroke.
*/
private class MultiExecutableAction implements ExecutableAction {
private List<DockingActionIf> validActions = new ArrayList<>();
private List<DockingActionIf> enabledActions = new ArrayList<>();
private ActionContext context;
private boolean isLocalAction;
@Override
public void execute() {
if (enabledActions.size() == 1) {
DockingActionIf action = enabledActions.get(0);
tool.setStatusInfo("");
// Toggle actions do not toggle its state directly therefor we have to do it for
// them before we execute the action.
if (action instanceof ToggleDockingActionIf) {
ToggleDockingActionIf toggleAction = (ToggleDockingActionIf) action;
toggleAction.setSelected(!toggleAction.isSelected());
}
action.actionPerformed(context);
return;
}
// If more than one action, prompt user to choose from multiple actions
MultiActionDialog dialog =
new MultiActionDialog(KeyBindingUtils.parseKeyStroke(keyStroke), enabledActions,
context);
// doing the show in an invoke later seems to fix a strange swing bug that lock up
// the program if you tried to invoke a new action too quickly after invoking
// it the first time
Swing.runLater(() -> DockingWindowManager.showDialog(dialog));
}
@Override
public KeyBindingPrecedence getKeyBindingPrecedence() {
KeyBindingPrecedence precedence = KeyBindingPrecedence.DefaultLevel;
if (enabledActions.size() == 1) {
DockingActionIf action = enabledActions.get(0);
precedence = action.getKeyBindingData().getKeyBindingPrecedence();
}
return precedence;
}
@Override
public boolean isValid() {
return !validActions.isEmpty();
}
@Override
public boolean isEnabled() {
return !enabledActions.isEmpty();
}
void setLocal(boolean isLocal) {
this.isLocalAction = isLocal;
}
void setContext(ActionContext context) {
if (this.context != null && this.context != context) {
throw new IllegalArgumentException("Context cannot be changed once set");
}
this.context = context;
}
void addValidAction(DockingActionIf a) {
validActions.add(a);
}
void addEnabledAction(DockingActionIf a) {
enabledActions.add(a);
}
/**
* Keeps only those actions in the list that are owned by the given dialog provider
* @param provider the provider
*/
void filterAndKeepOnlyDialogActions(DialogComponentProvider provider) {
Iterator<DockingActionIf> it = validActions.iterator();
while (it.hasNext()) {
DockingActionIf action = it.next();
if (!provider.isDialogKeyBindingAction(action)) {
it.remove();
enabledActions.remove(action);
}
}
}
private String getContextText(Component focusOwner) {
DockingWindowManager dwm = tool.getWindowManager();
Window window = getWindow(dwm, focusOwner);
if (window instanceof DockingDialog) {
return "in this dialog";
}
if (!isLocalAction) {
// no need to warn about global/default actions, as that may be annoying when the
// keystrokes bubble up to the global level
return null;
}
ComponentProvider provider = context.getComponentProvider();
if (provider != null) {
return "in " + provider.getName();
}
return "for context";
}
@Override
public void reportNotEnabled(Component focusOwner) {
String contextText = getContextText(focusOwner);
if (contextText == null) {
return;
}
DockingActionIf action = validActions.get(0);
String actionName = action.getName();
String ksText = KeyBindingUtils.parseKeyStroke(keyStroke);
String message =
"'%s' (%s) not currently enabled %s".formatted(actionName, ksText, contextText);
tool.setStatusInfo(message, true);
Toolkit.getDefaultToolkit().beep();
}
}
}

View file

@ -15,6 +15,10 @@
*/
package docking.action;
import java.awt.Component;
import java.awt.event.ActionEvent;
import javax.help.UnsupportedOperationException;
import javax.swing.KeyStroke;
import docking.*;
@ -33,13 +37,29 @@ public class SystemKeyBindingAction extends DockingKeyBindingAction {
return dockingAction;
}
private ActionContext getContext(Component focusOwner) {
ComponentProvider provider = tool.getActiveComponentProvider();
ActionContext context = getLocalContext(provider);
context.setSourceObject(focusOwner);
return context;
}
@Override
public boolean isSystemKeybindingPrecedence() {
return true;
}
@Override
public KeyBindingPrecedence getKeyBindingPrecedence() {
return KeyBindingPrecedence.SystemActionsLevel;
public ExecutableAction getExecutableAction(Component focusOwner) {
ActionContext context = getContext(focusOwner);
return new SystemExecutableAction(dockingAction, context);
}
@Override
public void actionPerformed(ActionEvent e) {
// A vestige from when we used to send this class through the Swing API. Execution is now
// done on the ExecutableAction this class creates.
throw new UnsupportedOperationException();
}
}

View file

@ -258,6 +258,11 @@ public class SharedStubKeyBindingAction extends DockingAction implements Options
return false;
}
@Override
public boolean isValidContext(ActionContext context) {
return false;
}
@Override
public boolean isEnabledForContext(ActionContext context) {
return false;

View file

@ -1923,7 +1923,7 @@ public class GTree extends JPanel implements BusyListener {
@Override
public void actionPerformed(ActionContext context) {
GTree gTree = (GTree) context.getSourceComponent();
GTree gTree = getTree(context);
gTree.tree.isCopyFormatted = true;
try {
Action builtinCopyAction = TransferHandler.getCopyAction();
@ -1948,7 +1948,7 @@ public class GTree extends JPanel implements BusyListener {
GTreeAction activateFilterAction = new GTreeAction("Table/Tree Activate Filter", owner) {
@Override
public void actionPerformed(ActionContext context) {
GTree gTree = (GTree) context.getSourceComponent();
GTree gTree = getTree(context);
gTree.filterProvider.activate();
}
};
@ -1966,7 +1966,7 @@ public class GTree extends JPanel implements BusyListener {
GTreeAction toggleFilterAction = new GTreeAction("Table/Tree Toggle Filter", owner) {
@Override
public void actionPerformed(ActionContext context) {
GTree gTree = (GTree) context.getSourceComponent();
GTree gTree = getTree(context);
gTree.filterProvider.toggleVisibility();
}
};