GP-6628 - Key Bindings - Updated the options UI to show unregistered key bindings

This commit is contained in:
dragonmacher
2026-03-27 19:02:55 -04:00
parent 36ad0e3efd
commit 03bd71d012
8 changed files with 409 additions and 135 deletions

View File

@@ -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|

View File

@@ -126,13 +126,28 @@
</OL>
</BLOCKQUOTE>
<P><IMG alt="" border="0" src="help/shared/note.yellow.png"> 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.</P>
<P>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.</P>
<BLOCKQUOTE>
<P><IMG alt="" border="0" src="help/shared/note.yellow.png"> 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.</P>
<P>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.</P>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P><IMG alt="" border="0" src="help/shared/note.yellow.png">Unregistered action key
bindings will be displayed in a lighter color in the table. These values can still
be changed.</P>
<P>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.
</P>
</BLOCKQUOTE>
<H3>Remove a Key Binding</H3>

View File

@@ -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<DockingActionIf> model =
(RowObjectFilterModel<DockingActionIf>) table.getModel();
RowObjectFilterModel<ActionBindingsDescriptor> model =
(RowObjectFilterModel<ActionBindingsDescriptor>) 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<DockingActionIf> model =
(RowObjectFilterModel<DockingActionIf>) table.getModel();
RowObjectFilterModel<ActionBindingsDescriptor> model =
(RowObjectFilterModel<ActionBindingsDescriptor>) 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<DockingActionIf> model =
(RowObjectFilterModel<DockingActionIf>) table.getModel();
final RowObjectFilterModel<ActionBindingsDescriptor> model =
(RowObjectFilterModel<ActionBindingsDescriptor>) table.getModel();
int actionRow = -1;
List<DockingActionIf> modelData = model.getModelData();
List<ActionBindingsDescriptor> 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)) {

View File

@@ -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();
}

View File

@@ -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.
* <p>
* 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<DockingActionIf> uniqueActions = new ArrayList<>();
// allows clients to know if a given key stroke or mouse binding is in use
private Map<KeyStroke, List<String>> actionNamesByKeyStroke = new HashMap<>();
private Map<MouseBinding, String> 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<String, ActionKeyBindingState> actionInfoByFullName = new HashMap<>();
private Map<String, KeyBindingState> 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<DockingActionIf> getUniqueActions() {
return Collections.unmodifiableList(uniqueActions);
public List<ActionBindingsDescriptor> getActionBindings() {
return actionInfoByFullName.values()
.stream()
.map(state -> (ActionBindingsDescriptor) state)
.collect(Collectors.toList());
}
/* used for testing */
public Map<String, KeyStroke> getKeyStrokesByFullActionName() {
Map<String, KeyStroke> result = new HashMap<>();
Set<Entry<String, ActionKeyBindingState>> entries = actionInfoByFullName.entrySet();
for (Entry<String, ActionKeyBindingState> entry : entries) {
Set<Entry<String, KeyBindingState>> entries = actionInfoByFullName.entrySet();
for (Entry<String, KeyBindingState> 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<String> registeredNames = new HashSet<>();
Map<String, List<DockingActionIf>> actionsByFullName =
KeyBindingUtils.getAllActionsByFullName(tool);
Set<Entry<String, List<DockingActionIf>>> 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<String> allNamesList = keyBindingOptions.getOptionNames();
Set<String> 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<DockingActionIf> 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<DockingActionIf> 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<DockingActionIf> actions = new ArrayList<>();
ActionKeyBindingState(List<DockingActionIf> 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);

View File

@@ -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();

View File

@@ -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

View File

@@ -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<DockingActionIf> tableFilterPanel;
private GTableFilterPanel<ActionBindingsDescriptor> 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("<html>" + 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<DockingActionIf, Object> {
extends GDynamicColumnTableModel<ActionBindingsDescriptor, Object> {
private List<DockingActionIf> actions;
private List<ActionBindingsDescriptor> actions;
public KeyBindingsTableModel(List<DockingActionIf> actions) {
public KeyBindingsTableModel(List<ActionBindingsDescriptor> actions) {
super(new ServiceProviderStub());
this.actions = actions;
}
@Override
protected TableColumnDescriptor<DockingActionIf> createTableColumnDescriptor() {
TableColumnDescriptor<DockingActionIf> descriptor = new TableColumnDescriptor<>();
protected TableColumnDescriptor<ActionBindingsDescriptor> createTableColumnDescriptor() {
TableColumnDescriptor<ActionBindingsDescriptor> 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<DockingActionIf> getModelData() {
public List<ActionBindingsDescriptor> getModelData() {
return actions;
}