From 03bd71d0122ccd4d0867e24acac226e6a2823042 Mon Sep 17 00:00:00 2001 From: dragonmacher <48328597+dragonmacher@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:02:55 -0400 Subject: [PATCH] GP-6628 - Key Bindings - Updated the options UI to show unregistered key bindings --- Ghidra/Features/Base/certification.manifest | 1 - .../help/topics/Tool/ToolOptions_Dialog.htm | 29 +- .../bean/opteditor/OptionsDialogTest.java | 21 +- .../actions/ActionBindingsDescriptor.java | 67 ++++ ...KeyBindings.java => KeyBindingsModel.java} | 317 ++++++++++++++---- .../java/docking/actions/KeyEntryDialog.java | 4 +- .../Project/data/project.theme.properties | 3 + .../plugintool/dialog/KeyBindingsPanel.java | 102 +++--- 8 files changed, 409 insertions(+), 135 deletions(-) create mode 100644 Ghidra/Framework/Docking/src/main/java/docking/actions/ActionBindingsDescriptor.java rename Ghidra/Framework/Docking/src/main/java/docking/actions/{KeyBindings.java => KeyBindingsModel.java} (59%) diff --git a/Ghidra/Features/Base/certification.manifest b/Ghidra/Features/Base/certification.manifest index 534d1571f1..86eea1fbd4 100644 --- a/Ghidra/Features/Base/certification.manifest +++ b/Ghidra/Features/Base/certification.manifest @@ -979,7 +979,6 @@ src/main/resources/images/hoverOff.gif||GHIDRA||||END| src/main/resources/images/hoverOn.gif||GHIDRA||||END| src/main/resources/images/icon_link.gif||FAMFAMFAM Mini Icons - Public Domain|||famfamfam mini icon set|END| src/main/resources/images/imported_bookmark.gif||GHIDRA||||END| -src/main/resources/images/katomic.png||Nuvola Icons - LGPL 2.1|||Nuvola icon set|END| src/main/resources/images/key.png||FAMFAMFAM Icons - CC 2.5|||silk|END| src/main/resources/images/kmessedwords.png||Nuvola Icons - LGPL 2.1||||END| src/main/resources/images/label.png||GHIDRA||||END| diff --git a/Ghidra/Features/Base/src/main/help/help/topics/Tool/ToolOptions_Dialog.htm b/Ghidra/Features/Base/src/main/help/help/topics/Tool/ToolOptions_Dialog.htm index d7517cbdd8..03177e4b34 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/Tool/ToolOptions_Dialog.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/Tool/ToolOptions_Dialog.htm @@ -126,13 +126,28 @@ -

When a key is mapped to multiple - actions, and more than one of these actions is valid in the current context (i.e., the action - is enabled), then a dialog is displayed for you to choose what action you want to - perform.

- -

To avoid the extra step of choosing the action from the dialog, do not map the same key to - actions that are applicable in the same context.

+
+

When a key is mapped to multiple + actions, and more than one of these actions is valid in the current context (i.e., the action + is enabled), then a dialog is displayed for you to choose what action you want to + perform.

+ +

To avoid the extra step of choosing the action from the dialog, do not map the same key to + actions that are applicable in the same context.

+
+ +
+

Unregistered action key + bindings will be displayed in a lighter color in the table. These values can still + be changed.

+ +

Unregistered actions may appear if plugins have been removed. They may also appear + for dialogs or components that have not yet been created in the system. Because + some actions are registred on-demand, not all actions in the system will appear in + this table. Once an action UI has been shown in a given tool session, then its + actions appear in this table for the remainder of the tool session. +

+

Remove a Key Binding

diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/util/bean/opteditor/OptionsDialogTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/util/bean/opteditor/OptionsDialogTest.java index b68b9fe570..08453269fd 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/util/bean/opteditor/OptionsDialogTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/util/bean/opteditor/OptionsDialogTest.java @@ -37,6 +37,7 @@ import org.junit.*; import docking.*; import docking.action.DockingActionIf; +import docking.actions.ActionBindingsDescriptor; import docking.actions.KeyBindingUtils; import docking.options.editor.*; import docking.tool.ToolConstants; @@ -887,10 +888,10 @@ public class OptionsDialogTest extends AbstractGhidraHeadedIntegrationTest { JTable table = (JTable) getInstanceField("actionTable", panel); @SuppressWarnings("unchecked") - RowObjectFilterModel model = - (RowObjectFilterModel) table.getModel(); + RowObjectFilterModel model = + (RowObjectFilterModel) table.getModel(); - DockingActionIf rowValue = model.getModelData().get(row); + ActionBindingsDescriptor rowValue = model.getModelData().get(row); String keyBindingColumnValue = (String) model.getColumnValueForRow(rowValue, 1 /* key binding column */); @@ -917,10 +918,10 @@ public class OptionsDialogTest extends AbstractGhidraHeadedIntegrationTest { JTable table = (JTable) getInstanceField("actionTable", panel); @SuppressWarnings("unchecked") - RowObjectFilterModel model = - (RowObjectFilterModel) table.getModel(); + RowObjectFilterModel model = + (RowObjectFilterModel) table.getModel(); - DockingActionIf rowValue = model.getModelData().get(row); + ActionBindingsDescriptor rowValue = model.getModelData().get(row); String keyBindingColumnValue = (String) model.getColumnValueForRow(rowValue, 1 /* key binding column */); @@ -1036,14 +1037,14 @@ public class OptionsDialogTest extends AbstractGhidraHeadedIntegrationTest { private int selectRowForAction(KeyBindingsPanel panel, String actionName, String actionOwner) { final JTable table = (JTable) getInstanceField("actionTable", panel); @SuppressWarnings("unchecked") - final RowObjectFilterModel model = - (RowObjectFilterModel) table.getModel(); + final RowObjectFilterModel model = + (RowObjectFilterModel) table.getModel(); int actionRow = -1; - List modelData = model.getModelData(); + List modelData = model.getModelData(); int rowCount = modelData.size(); for (int i = 0; i < rowCount; i++) { - DockingActionIf rowData = modelData.get(i); + ActionBindingsDescriptor rowData = modelData.get(i); String rowActionName = (String) model.getColumnValueForRow(rowData, 0 /* action name column */); if (rowActionName.equals(actionName)) { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/actions/ActionBindingsDescriptor.java b/Ghidra/Framework/Docking/src/main/java/docking/actions/ActionBindingsDescriptor.java new file mode 100644 index 0000000000..f763b21248 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/actions/ActionBindingsDescriptor.java @@ -0,0 +1,67 @@ +/* ### + * 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.actions; + +import docking.action.DockingActionIf; + +/** + * An interface that allows the {@link KeyBindingsModel} API to provide key and mouse binding information + * to clients, without having to have a registered action to provide the information. Action + * descriptions will be loaded from the saved tool options. If no plugin has registered an action + * for the current tool session, then the an unregistered action descriptor will get created when + * editing key and mouse bindings via the options UI. + */ +public interface ActionBindingsDescriptor { + + /** + * {@return the action name without the owner} + */ + public String getName(); + + /** + * {@return the full action name in the format: 'Name (Owner)'} + */ + public String getFullName(); + + /** + * {@return the owner name(s) of the action} + */ + public String getOwnerDescription(); + + /** + * {@return the action description or a blank string} + */ + public String getDescription(); + + /** + * {@return a string that shows all key and mouse binding info for the action} + */ + public String getBindingText(); + + /** + * The action for the binding. This will be an arbitrary action for shared bindings. + * This will be null if this class represents an unregistered action. + * + * @return an action or null + */ + public DockingActionIf getRepresentativeAction(); + + /** + * {@return true if a plugin has registered an action for this descriptor; false is no action + * has been registered} + */ + public boolean isRegistered(); +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/actions/KeyBindings.java b/Ghidra/Framework/Docking/src/main/java/docking/actions/KeyBindingsModel.java similarity index 59% rename from Ghidra/Framework/Docking/src/main/java/docking/actions/KeyBindings.java rename to Ghidra/Framework/Docking/src/main/java/docking/actions/KeyBindingsModel.java index 0376fd8cd1..7137f6db12 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/actions/KeyBindings.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/actions/KeyBindingsModel.java @@ -17,6 +17,7 @@ package docking.actions; import java.util.*; import java.util.Map.Entry; +import java.util.stream.Collectors; import javax.swing.KeyStroke; @@ -30,31 +31,31 @@ import gui.event.MouseBinding; import util.CollectionUtils; /** - * An object that maps actions to key strokes and mouse bindings. + * An object that maps actions to key strokes and mouse bindings. This class loads key bindings + * from tool options. Clients can use this model to manage the values of each binding until the + * user is done editing the bindings. When the client is finished, the bindings changes can be: + * applied, ignored or restored to default settings. *

* This class knows how to load all system actions and how to load any key and mouse bindings for * those actions from the tool's options. Clients can make changes to the state of this class that * can then be applied to the system by calling {@link #applyChanges()}. */ -public class KeyBindings { +public class KeyBindingsModel { private Tool tool; private ToolOptions keyBindingOptions; - // allows clients to populate a table of all actions - private List uniqueActions = new ArrayList<>(); - // allows clients to know if a given key stroke or mouse binding is in use private Map> actionNamesByKeyStroke = new HashMap<>(); private Map actionNameByMouseBinding = new HashMap<>(); // tracks all changes to an action's key stroke and mouse bindings, which allows us to apply // and restore options values - private Map actionInfoByFullName = new HashMap<>(); + private Map actionInfoByFullName = new HashMap<>(); private String longestActionName = ""; - public KeyBindings(Tool tool) { + public KeyBindingsModel(Tool tool) { this.tool = tool; keyBindingOptions = tool.getOptions(DockingToolConstants.KEY_BINDINGS); @@ -62,15 +63,19 @@ public class KeyBindings { init(); } - public List getUniqueActions() { - return Collections.unmodifiableList(uniqueActions); + public List getActionBindings() { + + return actionInfoByFullName.values() + .stream() + .map(state -> (ActionBindingsDescriptor) state) + .collect(Collectors.toList()); } /* used for testing */ public Map getKeyStrokesByFullActionName() { Map result = new HashMap<>(); - Set> entries = actionInfoByFullName.entrySet(); - for (Entry entry : entries) { + Set> entries = actionInfoByFullName.entrySet(); + for (Entry entry : entries) { String key = entry.getKey(); KeyStroke value = entry.getValue().getCurrentKeyStroke(); result.put(key, value); @@ -83,12 +88,12 @@ public class KeyBindings { } public KeyStroke getKeyStroke(String fullName) { - ActionKeyBindingState info = actionInfoByFullName.get(fullName); + KeyBindingState info = actionInfoByFullName.get(fullName); return info.getCurrentKeyStroke(); } public MouseBinding getMouseBinding(String fullName) { - ActionKeyBindingState info = actionInfoByFullName.get(fullName); + KeyBindingState info = actionInfoByFullName.get(fullName); return info.getCurrentMouseBinding(); } @@ -114,11 +119,9 @@ public class KeyBindings { sb.append(" "); String name = names.get(i); - ActionKeyBindingState info = actionInfoByFullName.get(name); - DockingActionIf action = info.getRepresentativeAction(); - String shortName = action.getName(); - sb.append(shortName); - sb.append(" (").append(action.getOwnerDescription()).append(')'); + KeyBindingState state = actionInfoByFullName.get(name); + String fullName = state.getFullName(); + sb.append(fullName); if (i < names.size() - 1) { sb.append("\n"); } @@ -155,7 +158,7 @@ public class KeyBindings { actionNameByMouseBinding.put(newBinding, fullName); } - ActionKeyBindingState info = actionInfoByFullName.get(fullName); + KeyBindingState info = actionInfoByFullName.get(fullName); info.setCurrentMouseBinding(newBinding); return true; } @@ -174,14 +177,14 @@ public class KeyBindings { } addActionKeyStroke(fullName, newKs); - ActionKeyBindingState info = actionInfoByFullName.get(fullName); + KeyBindingState info = actionInfoByFullName.get(fullName); info.setCurrentKeyStroke(newKs); return true; } public boolean removeKeyStroke(String fullName) { - ActionKeyBindingState info = actionInfoByFullName.get(fullName); + KeyBindingState info = actionInfoByFullName.get(fullName); if (info == null) { return false; // not sure if this can happen } @@ -201,7 +204,7 @@ public class KeyBindings { * system started. */ public void restoreOptions() { - for (ActionKeyBindingState info : actionInfoByFullName.values()) { + for (KeyBindingState info : actionInfoByFullName.values()) { info.restore(keyBindingOptions); } } @@ -210,8 +213,8 @@ public class KeyBindings { * Cancels any pending changes that have not yet been applied. */ public void cancelChanges() { - for (ActionKeyBindingState info : actionInfoByFullName.values()) { - info.cancelChanges(); + for (KeyBindingState info : actionInfoByFullName.values()) { + info.clearChanges(); } } @@ -219,7 +222,7 @@ public class KeyBindings { * Applies any pending changes. */ public void applyChanges() { - for (ActionKeyBindingState info : actionInfoByFullName.values()) { + for (KeyBindingState info : actionInfoByFullName.values()) { info.apply(keyBindingOptions); } } @@ -242,6 +245,7 @@ public class KeyBindings { actionInfoByFullName = new HashMap<>(); + Set registeredNames = new HashSet<>(); Map> actionsByFullName = KeyBindingUtils.getAllActionsByFullName(tool); Set>> entries = actionsByFullName.entrySet(); @@ -252,6 +256,8 @@ public class KeyBindings { String fullName = entry.getKey(); ActionTrigger trigger = keyBindingOptions.getActionTrigger(fullName, null); + registeredNames.add(fullName); + KeyStroke ks = null; MouseBinding mb = null; @@ -260,18 +266,45 @@ public class KeyBindings { mb = trigger.getMouseBinding(); } - ActionKeyBindingState info = new ActionKeyBindingState(actions, ks, mb); - actionInfoByFullName.put(fullName, info); - - uniqueActions.add(info.getRepresentativeAction()); + ActionKeyBindingState state = new ActionKeyBindingState(actions, ks, mb); + actionInfoByFullName.put(fullName, state); addActionKeyStroke(fullName, ks); - String shortName = info.getShortName(); + String shortName = state.getName(); if (shortName.length() > longestActionName.length()) { longestActionName = shortName; } } + + // ask options for unregistered key binding options + List allNamesList = keyBindingOptions.getOptionNames(); + Set unregisteredNames = new HashSet<>(allNamesList); + unregisteredNames.removeAll(registeredNames); + + for (String fullName : unregisteredNames) { + KeyStroke ks = null; + MouseBinding mb = null; + + ActionTrigger trigger = keyBindingOptions.getActionTrigger(fullName, null); + if (trigger != null) { + ks = trigger.getKeyStroke(); + mb = trigger.getMouseBinding(); + } + + UnregisteredActionKeyBindingState state = + new UnregisteredActionKeyBindingState(fullName, ks, mb); + actionInfoByFullName.put(fullName, state); + + addActionKeyStroke(fullName, ks); + + int description = fullName.indexOf("("); + String shortName = fullName.substring(0, description).trim(); + if (shortName.length() > longestActionName.length()) { + longestActionName = shortName; + } + + } } private void addActionKeyStroke(String actionName, KeyStroke ks) { @@ -289,62 +322,74 @@ public class KeyBindings { } } - /** - * A class to store current and original values for key strokes and mouse bindings. This is - * used to apply changes and restore default values. - */ - private class ActionKeyBindingState { + private abstract class KeyBindingState implements ActionBindingsDescriptor { - private List actions = new ArrayList<>(); - private KeyStroke originalKeyStroke; - private KeyStroke currentKeyStroke; - private MouseBinding originalMouseBinding; - private MouseBinding currentMouseBinding; + KeyStroke originalKeyStroke; + KeyStroke currentKeyStroke; + MouseBinding originalMouseBinding; + MouseBinding currentMouseBinding; - ActionKeyBindingState(List actions, KeyStroke ks, MouseBinding mb) { - this.actions.addAll(actions); + KeyBindingState(KeyStroke ks, MouseBinding mb) { this.originalKeyStroke = ks; this.currentKeyStroke = ks; this.originalMouseBinding = mb; this.currentMouseBinding = mb; } - public DockingActionIf getRepresentativeAction() { - // pick one action, they are all conceptually the same - return actions.get(0); + abstract KeyBindingData getCurrentKeyBindingData(); + + // restores the key/mouse binding options to their default values + abstract void restore(ToolOptions options); + + KeyBindingData getOriginalKeyBindingData() { + + if (originalKeyStroke == null && originalMouseBinding == null) { + return null; // the key binding data does not exist or has been cleared + } + + ActionTrigger trigger = new ActionTrigger(originalKeyStroke, originalMouseBinding); + return new KeyBindingData(trigger); } - String getShortName() { - // pick one action, they are all conceptually the same - return actions.get(0).getName(); + @Override + public String getBindingText() { + String text = ""; + String fullName = getFullName(); + KeyStroke ks = getKeyStroke(fullName); + if (ks != null) { + text += KeyBindingUtils.parseKeyStroke(ks); + } + + MouseBinding mb = getMouseBinding(fullName); + if (mb != null) { + text += " (" + mb.getDisplayText() + ")"; + } + + return text.trim(); } - String getFullName() { - return getRepresentativeAction().getFullName(); - } - - public MouseBinding getCurrentMouseBinding() { + MouseBinding getCurrentMouseBinding() { return currentMouseBinding; } - public void setCurrentMouseBinding(MouseBinding newMouseBinding) { + void setCurrentMouseBinding(MouseBinding newMouseBinding) { this.currentMouseBinding = newMouseBinding; } - public KeyStroke getCurrentKeyStroke() { + KeyStroke getCurrentKeyStroke() { return currentKeyStroke; } - public void setCurrentKeyStroke(KeyStroke newKeyStroke) { + void setCurrentKeyStroke(KeyStroke newKeyStroke) { this.currentKeyStroke = newKeyStroke; } - public void cancelChanges() { + void clearChanges() { currentKeyStroke = originalKeyStroke; currentMouseBinding = originalMouseBinding; } - public void apply(ToolOptions keyStrokeOptions) { + void apply(ToolOptions keyStrokeOptions) { if (!hasChanged()) { return; } @@ -353,19 +398,17 @@ public class KeyBindings { apply(keyStrokeOptions, kbd); } - private void apply(ToolOptions keyStrokeOptions, KeyBindingData keyBinding) { - + void apply(ToolOptions keyStrokeOptions, KeyBindingData keyBinding) { + String fullName = getFullName(); if (keyBinding == null) { - // no bindings; bindings have been cleared - for (DockingActionIf action : actions) { - action.setUnvalidatedKeyBindingData(null); - } + keyStrokeOptions.setActionTrigger(fullName, null); + currentKeyStroke = null; + currentMouseBinding = null; return; } // 1) update the options with the new value ActionTrigger newTrigger = keyBinding.getActionTrigger(); - String fullName = getFullName(); keyStrokeOptions.setActionTrigger(fullName, newTrigger); // 2) update our state so the UI shows the new value @@ -373,12 +416,7 @@ public class KeyBindings { currentMouseBinding = newTrigger.getMouseBinding(); } - private boolean hasChanged() { - return !Objects.equals(originalKeyStroke, currentKeyStroke) || - !Objects.equals(originalMouseBinding, currentMouseBinding); - } - - private boolean matches(KeyBindingData kbData) { + boolean matches(KeyBindingData kbData) { if (CollectionUtils.isAllNull(kbData, currentKeyStroke, currentMouseBinding)) { return true; @@ -397,7 +435,137 @@ public class KeyBindings { return Objects.equals(otherMb, currentMouseBinding); } - private KeyBindingData getCurrentKeyBindingData() { + boolean hasChanged() { + return !Objects.equals(originalKeyStroke, currentKeyStroke) || + !Objects.equals(originalMouseBinding, currentMouseBinding); + } + } + + /** + * Represents info for an action that is known by the options, but has not been registered. + */ + private class UnregisteredActionKeyBindingState extends KeyBindingState { + + private String fullName; + private String actionName; + private String ownerName; + + UnregisteredActionKeyBindingState(String fullName, KeyStroke ks, MouseBinding mb) { + super(ks, mb); + this.fullName = fullName; + + int descriptionIndex = fullName.indexOf("("); + this.actionName = fullName.substring(0, descriptionIndex).trim(); + + int lastParen = fullName.lastIndexOf(")"); + this.ownerName = fullName.substring(descriptionIndex + 1, lastParen).trim(); + } + + @Override + public String getName() { + return actionName; + } + + @Override + public String getFullName() { + return fullName; + } + + @Override + public String getOwnerDescription() { + return ownerName; + } + + @Override + public String getDescription() { + return ""; // no action; no description + } + + @Override + public DockingActionIf getRepresentativeAction() { + return null; // no action + } + + @Override + public boolean isRegistered() { + return false; + } + + @Override + KeyBindingData getCurrentKeyBindingData() { + if (currentKeyStroke == null && currentMouseBinding == null) { + return null; // no bindings or the values have been cleared + } + + ActionTrigger trigger = new ActionTrigger(currentKeyStroke, currentMouseBinding); + return new KeyBindingData(trigger); + } + + @Override + void restore(ToolOptions options) { + + if (!hasChanged()) { + return; + } + + KeyBindingData defaultBinding = getOriginalKeyBindingData(); + + clearChanges(); + + if (!matches(defaultBinding)) { + apply(options, defaultBinding); + } + } + + } + + /** + * A class to store current and original values for key strokes and mouse bindings. This is + * used to apply changes and restore default values. + */ + private class ActionKeyBindingState extends KeyBindingState { + + private List actions = new ArrayList<>(); + + ActionKeyBindingState(List actions, KeyStroke ks, MouseBinding mb) { + super(ks, mb); + this.actions.addAll(actions); + } + + @Override + public String getName() { + // pick one action, they are all conceptually the same + return getRepresentativeAction().getName(); + } + + @Override + public String getFullName() { + return getRepresentativeAction().getFullName(); + } + + @Override + public String getOwnerDescription() { + return getRepresentativeAction().getOwnerDescription(); + } + + @Override + public String getDescription() { + return getRepresentativeAction().getDescription(); + } + + @Override + public DockingActionIf getRepresentativeAction() { + // pick one action, they are all conceptually the same + return actions.get(0); + } + + @Override + public boolean isRegistered() { + return true; + } + + @Override + KeyBindingData getCurrentKeyBindingData() { if (currentKeyStroke == null && currentMouseBinding == null) { return null; // the key binding data does not exist or has been cleared @@ -410,11 +578,12 @@ public class KeyBindings { } // restores the options to their default values - public void restore(ToolOptions options) { + @Override + void restore(ToolOptions options) { DockingActionIf action = getRepresentativeAction(); KeyBindingData defaultBinding = action.getDefaultKeyBindingData(); - cancelChanges(); + clearChanges(); if (!matches(defaultBinding)) { apply(options, defaultBinding); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/actions/KeyEntryDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/actions/KeyEntryDialog.java index a349f29d89..f3d1082331 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/actions/KeyEntryDialog.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/actions/KeyEntryDialog.java @@ -37,7 +37,7 @@ import resources.Icons; */ public class KeyEntryDialog extends DialogComponentProvider { - private KeyBindings keyBindings; + private KeyBindingsModel keyBindings; private ToolActions toolActions; private DockingActionIf action; @@ -54,7 +54,7 @@ public class KeyEntryDialog extends DialogComponentProvider { this.action = action; this.toolActions = (ToolActions) tool.getToolActions(); - this.keyBindings = new KeyBindings(tool); + this.keyBindings = new KeyBindingsModel(tool); setUpAttributes(); createPanel(); diff --git a/Ghidra/Framework/Project/data/project.theme.properties b/Ghidra/Framework/Project/data/project.theme.properties index 1fc29cfa51..e2085fae9b 100644 --- a/Ghidra/Framework/Project/data/project.theme.properties +++ b/Ghidra/Framework/Project/data/project.theme.properties @@ -8,6 +8,9 @@ color.fg.extensionpanel.path = color.palette.blue color.fg.extensionpanel.details.title = color.palette.maroon color.fg.extensionpanel.details.version = color.palette.blue +color.fg.options.keybindings.table.unregistered = color.palette.lightgray +color.fg.options.keybindings.table.unregistered.selected.unfocused = color.palette.gray + color.fg.pluginpanel.name = color.fg color.fg.pluginpanel.description = color.palette.gray diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/KeyBindingsPanel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/KeyBindingsPanel.java index 0a78fe40c6..1ade1749a0 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/KeyBindingsPanel.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/KeyBindingsPanel.java @@ -35,6 +35,7 @@ import docking.tool.util.DockingToolConstants; import docking.widgets.*; import docking.widgets.MultiLineLabel.VerticalAlignment; import docking.widgets.table.*; +import generic.theme.GColor; import generic.theme.Gui; import ghidra.framework.options.*; import ghidra.framework.plugintool.PluginTool; @@ -66,10 +67,10 @@ public class KeyBindingsPanel extends JPanel { private KeyBindingsTableModel tableModel; private ActionBindingListener actionBindingListener = new ActionBindingListener(); private ActionBindingPanel actionBindingPanel; - private GTableFilterPanel tableFilterPanel; + private GTableFilterPanel tableFilterPanel; private EmptyBorderButton helpButton; - private KeyBindings keyBindings; + private KeyBindingsModel keyBindings; private boolean unappliedChanges; private PluginTool tool; @@ -82,7 +83,7 @@ public class KeyBindingsPanel extends JPanel { public KeyBindingsPanel(PluginTool tool) { this.tool = tool; - this.keyBindings = new KeyBindings(tool); + this.keyBindings = new KeyBindingsModel(tool); createPanelComponents(); @@ -147,7 +148,7 @@ public class KeyBindingsPanel extends JPanel { gettingStartedPanel = new JPanel(); activeActionPanel = createActiveActionPanel(); - tableModel = new KeyBindingsTableModel(new ArrayList<>(keyBindings.getUniqueActions())); + tableModel = new KeyBindingsTableModel(new ArrayList<>(keyBindings.getActionBindings())); actionTable = new GTable(tableModel); JScrollPane actionsScroller = new JScrollPane(actionTable); @@ -156,6 +157,8 @@ public class KeyBindingsPanel extends JPanel { actionTable.setHTMLRenderingEnabled(true); actionTable.getSelectionModel().addListSelectionListener(new TableSelectionListener()); + actionTable.setDefaultRenderer(String.class, new KeyBindingsRenderer()); + adjustTableColumns(); // middle panel - filter field and import/export buttons @@ -206,9 +209,9 @@ public class KeyBindingsPanel extends JPanel { helpButton = new EmptyBorderButton(Icons.HELP_ICON); helpButton.setEnabled(false); helpButton.addActionListener(e -> { - DockingActionIf action = getSelectedAction(); + ActionBindingsDescriptor binding = getSelectedBinding(); HelpService hs = Help.getHelpService(); - hs.showHelp(action, false, KeyBindingsPanel.this); + hs.showHelp(binding, false, KeyBindingsPanel.this); }); JPanel statusPanel = new JPanel(); @@ -372,7 +375,7 @@ public class KeyBindingsPanel extends JPanel { unappliedChanges = changes; } - private DockingActionIf getSelectedAction() { + private ActionBindingsDescriptor getSelectedBinding() { if (actionTable.getSelectedRowCount() == 0) { return null; } @@ -381,7 +384,7 @@ public class KeyBindingsPanel extends JPanel { } private String getSelectedActionName() { - DockingActionIf action = getSelectedAction(); + ActionBindingsDescriptor action = getSelectedBinding(); if (action == null) { return null; } @@ -457,21 +460,22 @@ public class KeyBindingsPanel extends JPanel { private void updateKeyStroke(KeyStroke ks) { clearInfoPanel(); - DockingActionIf action = getSelectedAction(); - if (action == null) { + ActionBindingsDescriptor binding = getSelectedBinding(); + if (binding == null) { statusLabel.setText(GETTING_STARTED_MESSAGE); return; } + DockingActionIf dockingAction = binding.getRepresentativeAction(); ToolActions toolActions = (ToolActions) tool.getToolActions(); - String errorMessage = toolActions.validateActionKeyBinding(action, ks); + String errorMessage = toolActions.validateActionKeyBinding(dockingAction, ks); if (errorMessage != null) { actionBindingPanel.clearKeyStroke(); statusLabel.setText(errorMessage); return; } - String selectedActionName = action.getFullName(); + String selectedActionName = binding.getFullName(); if (setActionKeyStroke(selectedActionName, ks)) { showActionsMappedToKeyStroke(ks); fireRowChanged(); @@ -483,13 +487,13 @@ public class KeyBindingsPanel extends JPanel { clearInfoPanel(); - DockingActionIf action = getSelectedAction(); - if (action == null) { + ActionBindingsDescriptor binding = getSelectedBinding(); + if (binding == null) { statusLabel.setText(GETTING_STARTED_MESSAGE); return; } - String selectedActionName = action.getFullName(); + String selectedActionName = binding.getFullName(); if (setMouseBinding(selectedActionName, mb)) { fireRowChanged(); changesMade(true); @@ -573,8 +577,8 @@ public class KeyBindingsPanel extends JPanel { helpButton.setEnabled(false); - DockingActionIf action = getSelectedAction(); - if (action == null) { + ActionBindingsDescriptor binding = getSelectedBinding(); + if (binding == null) { swapView(gettingStartedPanel); statusLabel.setText(GETTING_STARTED_MESSAGE); @@ -599,48 +603,64 @@ public class KeyBindingsPanel extends JPanel { MouseBinding mb = keyBindings.getMouseBinding(fullActionName); actionBindingPanel.setKeyBindingData(ks, mb); - String description = action.getDescription(); + String description = binding.getDescription(); if (StringUtils.isBlank(description)) { - description = action.getName(); + description = binding.getName(); } // Not sure why we escape the html here. Probably just to be safe. statusLabel.setText("" + description); - helpButton.setToolTipText("Help for " + action.getName()); + helpButton.setToolTipText("Help for " + binding.getName()); + } + } + + private static GColor COLOR_FG_UNREGISTERED = + new GColor("color.fg.options.keybindings.table.unregistered"); + private static GColor COLOR_FG_UNREGISTERED_SELECTED_UNFOCUSED = + new GColor("color.fg.options.keybindings.table.unregistered.selected.unfocused"); + + private class KeyBindingsRenderer extends GTableCellRenderer { + + @Override + public Component getTableCellRendererComponent(GTableCellRenderingData data) { + Component renderer = super.getTableCellRendererComponent(data); + ActionBindingsDescriptor action = (ActionBindingsDescriptor) data.getRowObject(); + if (!action.isRegistered()) { + + boolean selected = data.isSelected(); + boolean focused = actionTable.isFocusOwner(); + setForeground(COLOR_FG_UNREGISTERED); + + if (!focused && selected) { + // Selected and not focused; light gray background on some LaFs. Update the + // foreground to stand out against that color. + setForeground(COLOR_FG_UNREGISTERED_SELECTED_UNFOCUSED); + } + } + + return renderer; } } private class KeyBindingsTableModel - extends GDynamicColumnTableModel { + extends GDynamicColumnTableModel { - private List actions; + private List actions; - public KeyBindingsTableModel(List actions) { + public KeyBindingsTableModel(List actions) { super(new ServiceProviderStub()); this.actions = actions; } @Override - protected TableColumnDescriptor createTableColumnDescriptor() { - TableColumnDescriptor descriptor = new TableColumnDescriptor<>(); + protected TableColumnDescriptor createTableColumnDescriptor() { + TableColumnDescriptor descriptor = + new TableColumnDescriptor<>(); descriptor.addVisibleColumn("Action Name", String.class, a -> a.getName(), 1, true); - descriptor.addVisibleColumn("Key Binding", String.class, a -> { - String text = ""; - String fullName = a.getFullName(); - KeyStroke ks = keyBindings.getKeyStroke(fullName); - if (ks != null) { - text += KeyBindingUtils.parseKeyStroke(ks); - } - - MouseBinding mb = keyBindings.getMouseBinding(fullName); - if (mb != null) { - text += " (" + mb.getDisplayText() + ")"; - } - - return text.trim(); - }); + descriptor.addVisibleColumn("Key Binding", String.class, a -> a.getBindingText()); descriptor.addVisibleColumn("Owner", String.class, a -> a.getOwnerDescription()); descriptor.addHiddenColumn("Description", String.class, a -> a.getDescription()); + descriptor.addHiddenColumn("Registered?", Boolean.class, a -> a.isRegistered()); return descriptor; } @@ -650,7 +670,7 @@ public class KeyBindingsPanel extends JPanel { } @Override - public List getModelData() { + public List getModelData() { return actions; }