Merge remote-tracking branch 'origin/GP-6628-dragonmacher-dialog-keybindings-visibility--SQUASHED'

This commit is contained in:
Ryan Kurtz
2026-03-31 05:27:11 -04:00
8 changed files with 409 additions and 135 deletions
@@ -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|
@@ -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>
@@ -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)) {
@@ -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();
}
@@ -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();
@@ -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
@@ -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;
}