GP-5967 - Improved Options Key Binding UI

This commit is contained in:
dragonmacher 2025-09-04 17:02:11 -04:00
parent eef9950870
commit d538513428
12 changed files with 281 additions and 260 deletions

View file

@ -70,7 +70,7 @@ public class KeyBindingsTest extends AbstractGhidraHeadedIntegrationTest {
setUpDialog(); setUpDialog();
grabActionsWithoutKeybinding(); grabActionsWithoutKeyBinding();
} }
@After @After
@ -545,7 +545,7 @@ public class KeyBindingsTest extends AbstractGhidraHeadedIntegrationTest {
} }
// find 2 actions that do not have key bindings so that we can add and change the values // find 2 actions that do not have key bindings so that we can add and change the values
private void grabActionsWithoutKeybinding() { private void grabActionsWithoutKeyBinding() {
Set<DockingActionIf> list = tool.getAllActions(); Set<DockingActionIf> list = tool.getAllActions();
for (DockingActionIf action : list) { for (DockingActionIf action : list) {
if (ignoreAction(action)) { if (ignoreAction(action)) {

View file

@ -123,6 +123,8 @@ icon.widget.imagepanel.zoom.out = icon.zoom.out
icon.widget.filterpanel.filter.off = filter_off.png icon.widget.filterpanel.filter.off = filter_off.png
icon.widget.filterpanel.filter.on = filter_on.png icon.widget.filterpanel.filter.on = filter_on.png
icon.text.field.clear = icon.delete[size(12,12)]
icon.widget.pathmanager.reset = trash-empty.png icon.widget.pathmanager.reset = trash-empty.png
icon.widget.table.header.help = info_small.png icon.widget.table.header.help = info_small.png

View file

@ -15,12 +15,13 @@
*/ */
package docking; package docking;
import java.awt.BorderLayout;
import java.util.Objects; import java.util.Objects;
import javax.swing.*; import javax.swing.*;
import docking.widgets.checkbox.GCheckBox; import docking.widgets.EmptyBorderButton;
import docking.widgets.label.GLabel;
import generic.theme.GIcon;
import gui.event.MouseBinding; import gui.event.MouseBinding;
/** /**
@ -31,9 +32,7 @@ public class ActionBindingPanel extends JPanel {
private static final String DISABLED_HINT = "Select an action"; private static final String DISABLED_HINT = "Select an action";
private KeyEntryPanel keyEntryPanel; private KeyEntryPanel keyEntryPanel;
private JCheckBox useMouseBindingCheckBox;
private MouseEntryTextField mouseEntryField; private MouseEntryTextField mouseEntryField;
private JPanel textFieldPanel;
private DockingActionInputBindingListener listener; private DockingActionInputBindingListener listener;
@ -47,42 +46,31 @@ public class ActionBindingPanel extends JPanel {
setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS)); setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
textFieldPanel = new JPanel(new BorderLayout());
keyEntryPanel = new KeyEntryPanel(20, ks -> listener.keyStrokeChanged(ks)); keyEntryPanel = new KeyEntryPanel(20, ks -> listener.keyStrokeChanged(ks));
keyEntryPanel.setDisabledHint(DISABLED_HINT); keyEntryPanel.setDisabledHint(DISABLED_HINT);
keyEntryPanel.setEnabled(false); // enabled on action selection keyEntryPanel.setEnabled(false); // enabled on action selection
mouseEntryField = new MouseEntryTextField(20, mb -> listener.mouseBindingChanged(mb)); mouseEntryField = new MouseEntryTextField(20, mb -> listener.mouseBindingChanged(mb));
mouseEntryField.setDisabledHint(DISABLED_HINT); mouseEntryField.setDisabledHint(DISABLED_HINT);
mouseEntryField.setEnabled(false); // enabled on action selection
textFieldPanel.add(keyEntryPanel, BorderLayout.NORTH); JButton clearMouseButton = new EmptyBorderButton(new GIcon("icon.text.field.clear"));
clearMouseButton.setName("Clear Mouse Binding");
clearMouseButton.addActionListener(e -> mouseEntryField.clearMouseBinding());
String checkBoxText = "Enter Mouse Binding"; GLabel keyBindingLabel = new GLabel("Key Binding: ");
useMouseBindingCheckBox = new GCheckBox(checkBoxText); JTextField tf = keyEntryPanel.getTextField();
useMouseBindingCheckBox keyBindingLabel.setLabelFor(tf);
.setToolTipText("When checked, the text field accepts mouse buttons");
useMouseBindingCheckBox.setName(checkBoxText);
useMouseBindingCheckBox.addItemListener(e -> updateTextField());
add(textFieldPanel); GLabel mouseBindingLabel = new GLabel("Mouse Binding: ");
add(Box.createHorizontalStrut(5)); mouseBindingLabel.setLabelFor(mouseBindingLabel);
add(useMouseBindingCheckBox);
}
private void updateTextField() { add(keyBindingLabel);
add(keyEntryPanel);
if (useMouseBindingCheckBox.isSelected()) { add(Box.createHorizontalStrut(30));
textFieldPanel.remove(keyEntryPanel); add(mouseBindingLabel);
textFieldPanel.add(mouseEntryField, BorderLayout.NORTH); add(mouseEntryField);
} add(Box.createHorizontalStrut(2));
else { add(clearMouseButton);
textFieldPanel.remove(mouseEntryField);
textFieldPanel.add(keyEntryPanel, BorderLayout.NORTH);
}
validate();
repaint();
} }
public void setKeyBindingData(KeyStroke ks, MouseBinding mb) { public void setKeyBindingData(KeyStroke ks, MouseBinding mb) {
@ -113,11 +101,6 @@ public class ActionBindingPanel extends JPanel {
} }
public void clearMouseBinding() { public void clearMouseBinding() {
mouseEntryField.clearField(); mouseEntryField.clearMouseBinding();
} }
public boolean isMouseBinding() {
return useMouseBindingCheckBox.isSelected();
}
} }

View file

@ -18,7 +18,7 @@ package docking;
import javax.swing.*; import javax.swing.*;
import docking.widgets.EmptyBorderButton; import docking.widgets.EmptyBorderButton;
import resources.Icons; import generic.theme.GIcon;
/** /**
* A panel that holds a {@link KeyEntryTextField} and a button for clearing the current key binding. * A panel that holds a {@link KeyEntryTextField} and a button for clearing the current key binding.
@ -41,7 +41,7 @@ public class KeyEntryPanel extends JPanel {
setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS)); setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
keyEntryField = new KeyEntryTextField(columns, listener); keyEntryField = new KeyEntryTextField(columns, listener);
clearButton = new EmptyBorderButton(Icons.DELETE_ICON); clearButton = new EmptyBorderButton(new GIcon("icon.text.field.clear"));
clearButton.setName("Clear Key Binding"); clearButton.setName("Clear Key Binding");
clearButton.addActionListener(e -> keyEntryField.clearKeyStroke()); clearButton.addActionListener(e -> keyEntryField.clearKeyStroke());

View file

@ -15,8 +15,8 @@
*/ */
package docking; package docking;
import java.awt.event.KeyEvent; import java.awt.event.*;
import java.awt.event.KeyListener; import java.awt.event.FocusEvent.Cause;
import java.util.Objects; import java.util.Objects;
import javax.swing.KeyStroke; import javax.swing.KeyStroke;
@ -47,7 +47,21 @@ public class KeyEntryTextField extends HintTextField {
getAccessibleContext().setAccessibleName(getName()); getAccessibleContext().setAccessibleName(getName());
setColumns(columns); setColumns(columns);
this.listener = listener; this.listener = listener;
// remove the default mouse listeners to prevent pasting
MouseListener[] oldListeners1 = getMouseListeners();
for (MouseListener l : oldListeners1) {
removeMouseListener(l);
}
addKeyListener(new MyKeyListener()); addKeyListener(new MyKeyListener());
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
requestFocusInWindow(Cause.MOUSE_EVENT);
}
});
} }
@Override @Override

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -16,6 +16,7 @@
package docking; package docking;
import java.awt.event.*; import java.awt.event.*;
import java.awt.event.FocusEvent.Cause;
import java.util.Objects; import java.util.Objects;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -37,6 +38,12 @@ public class MouseEntryTextField extends HintTextField {
getAccessibleContext().setAccessibleName(getName()); getAccessibleContext().setAccessibleName(getName());
this.listener = Objects.requireNonNull(listener); this.listener = Objects.requireNonNull(listener);
// remove the default mouse listeners to prevent pasting
MouseListener[] oldListeners1 = getMouseListeners();
for (MouseListener l : oldListeners1) {
removeMouseListener(l);
}
addMouseListener(new MyMouseListener()); addMouseListener(new MyMouseListener());
addKeyListener(new MyKeyListener()); addKeyListener(new MyKeyListener());
} }
@ -63,10 +70,23 @@ public class MouseEntryTextField extends HintTextField {
processMouseBinding(mb, false); processMouseBinding(mb, false);
} }
/**
* Clears the state of this class, but does not notify listeners. This allows clients to
* control the state of the field without having a callback change the client state.
*/
public void clearField() { public void clearField() {
processMouseBinding(null, false); processMouseBinding(null, false);
} }
/**
* Clears the state of this class and notifies this client. This effectively allows for the
* programmatic setting of the mouse binding in use to be null, or in the 'no mouse binding set'
* state.
*/
public void clearMouseBinding() {
processMouseBinding(null, true);
}
private void processMouseBinding(MouseBinding mb, boolean notify) { private void processMouseBinding(MouseBinding mb, boolean notify) {
this.mouseBinding = mb; this.mouseBinding = mb;
@ -90,6 +110,8 @@ public class MouseEntryTextField extends HintTextField {
return; return;
} }
requestFocusInWindow(Cause.MOUSE_EVENT);
int modifiersEx = e.getModifiersEx(); int modifiersEx = e.getModifiersEx();
int button = e.getButton(); int button = e.getButton();
@ -102,6 +124,7 @@ public class MouseEntryTextField extends HintTextField {
} }
processMouseBinding(new MouseBinding(button, modifiersEx), true); processMouseBinding(new MouseBinding(button, modifiersEx), true);
e.consume(); e.consume();
} }

View file

@ -94,7 +94,7 @@ public class KeyBindingUtils {
public static ToolOptions importKeyBindings() { public static ToolOptions importKeyBindings() {
// show a filechooser for the user to choose a location // show a filechooser for the user to choose a location
InputStream inputStream = getInputStreamForFile(getStartingDir()); InputStream inputStream = getInputStreamForFile(getStartingDir());
return createOptionsforKeybindings(inputStream); return createOptionsforKeyBindings(inputStream);
} }
/** /**
@ -107,7 +107,7 @@ public class KeyBindingUtils {
* @return An options object that is composed of key binding names and their * @return An options object that is composed of key binding names and their
* associated keystrokes. * associated keystrokes.
*/ */
public static ToolOptions createOptionsforKeybindings(InputStream inputStream) { public static ToolOptions createOptionsforKeyBindings(InputStream inputStream) {
if (inputStream == null) { if (inputStream == null) {
return null; return null;
} }

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -54,7 +54,7 @@ public class SetKeyBindingAction extends DockingAction {
if (!action.getKeyBindingType().supportsKeyBindings()) { if (!action.getKeyBindingType().supportsKeyBindings()) {
Component parent = windowManager.getActiveComponent(); Component parent = windowManager.getActiveComponent();
Msg.showInfo(getClass(), parent, "Unable to Set Keybinding", Msg.showInfo(getClass(), parent, "Unable to Set Key Binding",
"Action \"" + getActionName(action) + "\" does not support key bindings"); "Action \"" + getActionName(action) + "\" does not support key bindings");
return; return;
} }

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -241,7 +241,7 @@ public class ToolActions implements DockingToolActions, PropertyChangeListener {
private void loadKeyBindingFromOptions(DockingActionIf action, ActionTrigger actionTrigger) { private void loadKeyBindingFromOptions(DockingActionIf action, ActionTrigger actionTrigger) {
String fullName = action.getFullName(); String fullName = action.getFullName();
String description = "Keybinding for " + fullName; String description = "Key Binding for " + fullName;
options.registerOption(fullName, OptionType.ACTION_TRIGGER, actionTrigger, null, options.registerOption(fullName, OptionType.ACTION_TRIGGER, actionTrigger, null,
description); description);

View file

@ -26,7 +26,7 @@ import utilities.util.reflection.ReflectionUtilities;
/** /**
* *
* Class to render a String that has new line characters as a multiline * Class to render a String that has new line characters as a multi-line
* label. Calculates the resizing and centering characteristics. * label. Calculates the resizing and centering characteristics.
* <p> * <p>
* Not affected by HTML formatting. * Not affected by HTML formatting.
@ -49,12 +49,19 @@ public class MultiLineLabel extends JPanel {
protected String[] lines; // lines to text to display protected String[] lines; // lines to text to display
protected int num_lines; // number of lines protected int num_lines; // number of lines
protected int margin_width; // left and right margins protected int margin_width; // left and right margins
protected int margin_height; // top and botton margins protected int margin_height; // top and bottom margins
protected int line_height; // total height of font protected int line_height; // total height of font
protected int line_ascent; // font height above baseline protected int line_ascent; // font height above baseline
protected int[] line_widths; // how wide each line is protected int[] line_widths; // how wide each line is
protected int max_width; // width of widest line protected int max_width; // width of widest line
protected int alignment = CENTER; // default alignment of text protected int alignment = CENTER; // default alignment of text
private VerticalAlignment verticalAlignment = VerticalAlignment.MIDDLE;
/** Values for controlling vertical alignment of the text */
public enum VerticalAlignment {
TOP,
MIDDLE;
}
/** /**
* Default constructor. * Default constructor.
@ -91,9 +98,9 @@ public class MultiLineLabel extends JPanel {
} }
/** /**
* breaks specified label into array of lines. * Breaks specified label into array of lines.
* *
*@param label String to display in canvas. *@param label String to display.
*/ */
protected void newLabel(String label) { protected void newLabel(String label) {
if (label == null) { if (label == null) {
@ -119,9 +126,6 @@ public class MultiLineLabel extends JPanel {
protected void measure() { protected void measure() {
FontMetrics fm = this.getFontMetrics(this.getFont()); FontMetrics fm = this.getFontMetrics(this.getFont());
// if no font metrics yet, just return
if (fm == null) { if (fm == null) {
return; return;
} }
@ -139,9 +143,9 @@ public class MultiLineLabel extends JPanel {
} }
/** /**
* Set a new label for JPanel * Set a new label to display.
* *
* @param label String to display in canvas * @param label String to display
*/ */
public void setLabel(String label) { public void setLabel(String label) {
@ -163,7 +167,7 @@ public class MultiLineLabel extends JPanel {
} }
/** /**
* Get the label text. * {@return the label text.}
*/ */
public String getLabel() { public String getLabel() {
StringBuffer sb = new StringBuffer(); StringBuffer sb = new StringBuffer();
@ -189,11 +193,6 @@ public class MultiLineLabel extends JPanel {
repaint(); repaint();
} }
/**
* Sets a new color for Canvas
*
*@param c Color to display in canvas
*/
@Override @Override
public void setForeground(Color c) { public void setForeground(Color c) {
super.setForeground(c); super.setForeground(c);
@ -209,6 +208,15 @@ public class MultiLineLabel extends JPanel {
repaint(); repaint();
} }
/**
* Sets the vertical alignment of the text. The default is {@link VerticalAlignment#MIDDLE}.
* @param alignment the alignment
*/
public void setVerticalAlignment(VerticalAlignment alignment) {
this.verticalAlignment = alignment;
repaint();
}
/** /**
* Set margin width. * Set margin width.
* @param mw the new margin width. * @param mw the new margin width.
@ -227,29 +235,20 @@ public class MultiLineLabel extends JPanel {
repaint(); repaint();
} }
/**
* Get alignment for text, LEFT, CENTER, RIGHT.
*/
public final int getAlignment() { public final int getAlignment() {
return alignment; return alignment;
} }
/**
* Get margin width.
*/
public final int getMarginWidth() { public final int getMarginWidth() {
return margin_width; return margin_width;
} }
/**
*Get margin height.
*/
public final int getMarginHeight() { public final int getMarginHeight() {
return margin_height; return margin_height;
} }
/** /**
* This method is invoked after Canvas is first created * This method is invoked after this class is first created
* but before it can be actually displayed. After we have * but before it can be actually displayed. After we have
* invoked our superclass's addNotify() method, we have font * invoked our superclass's addNotify() method, we have font
* metrics and can successfully call measure() to figure out * metrics and can successfully call measure() to figure out
@ -257,48 +256,38 @@ public class MultiLineLabel extends JPanel {
*/ */
@Override @Override
public void addNotify() { public void addNotify() {
super.addNotify(); super.addNotify();
measure(); measure();
} }
/**
* This method is called by a layout manager when it wants
* to know how big we'd like to be
*/
@Override @Override
public java.awt.Dimension getPreferredSize() { public Dimension getPreferredSize() {
return new Dimension(max_width + 2 * margin_width, return new Dimension(max_width + 2 * margin_width,
num_lines * line_height + 2 * margin_height); num_lines * line_height + 2 * margin_height);
} }
/**
* This method is called when layout manager wants to
* know the bare minimum amount of space we need to get by.
*/
@Override @Override
public java.awt.Dimension getMinimumSize() { public Dimension getMinimumSize() {
return new Dimension(max_width, num_lines * line_height); return new Dimension(max_width, num_lines * line_height);
} }
/**
* This method draws label (applets use same method).
* Note that it handles the margins and the alignment, but
* that is does not have to worry about the color or font --
* the superclass takes care of setting those in the Graphics
* object we've passed.
* @param g the graphics context to paint with.
*/
@Override @Override
public void paint(Graphics g) { public void paint(Graphics g) {
int x, y; paintBorder(g);
Dimension d = this.getSize();
// g.clearRect(0, 0, d.width, d.height);
y = line_ascent + (d.height - num_lines * line_height) / 2; Dimension d = this.getSize();
int y;
if (verticalAlignment == VerticalAlignment.MIDDLE) {
y = line_ascent + (d.height - num_lines * line_height) / 2;
}
else {
y = margin_height + line_ascent;
}
int x;
for (int i = 0; i < num_lines; i++, y += line_height) { for (int i = 0; i < num_lines; i++, y += line_height) {
switch (alignment) { switch (alignment) {
case LEFT: case LEFT:
@ -313,14 +302,11 @@ public class MultiLineLabel extends JPanel {
break; break;
} }
GraphicsUtils.drawString(this, g, lines[i], x, y); GraphicsUtils.drawString(this, g, lines[i], x, y);
} }
} }
/**
* Simple test for the MultiLineLabel class.
* @param args not used
*/
public static void main(String[] args) { public static void main(String[] args) {
MultiLineLabel mlab = new MultiLineLabel( MultiLineLabel mlab = new MultiLineLabel(

View file

@ -29,9 +29,8 @@ import org.jdesktop.animation.timing.interpolation.PropertySetter;
import docking.util.AnimationUtils; import docking.util.AnimationUtils;
import docking.widgets.label.GIconLabel; import docking.widgets.label.GIconLabel;
import generic.theme.GIcon;
import ghidra.util.SystemUtilities; import ghidra.util.SystemUtilities;
import resources.Icons;
import resources.ResourceManager;
/** /**
* A label that displays an icon that, when clicked, will clear the contents of the * A label that displays an icon that, when clicked, will clear the contents of the
@ -39,8 +38,7 @@ import resources.ResourceManager;
*/ */
public class ClearFilterLabel extends GIconLabel { public class ClearFilterLabel extends GIconLabel {
private Icon RAW_ICON = Icons.DELETE_ICON; private Icon ICON = new GIcon("icon.text.field.clear");
private Icon ICON = ResourceManager.getScaledIcon(RAW_ICON, 10, 10);
private static final float FULLY_TRANSPARENT = 0F; private static final float FULLY_TRANSPARENT = 0F;
private static final float FULLY_OPAQUE = .6F; private static final float FULLY_OPAQUE = .6F;

View file

@ -33,14 +33,14 @@ import docking.action.DockingActionIf;
import docking.actions.*; import docking.actions.*;
import docking.tool.util.DockingToolConstants; import docking.tool.util.DockingToolConstants;
import docking.widgets.*; import docking.widgets.*;
import docking.widgets.label.GIconLabel; import docking.widgets.MultiLineLabel.VerticalAlignment;
import docking.widgets.table.*; import docking.widgets.table.*;
import generic.theme.Gui; import generic.theme.Gui;
import ghidra.framework.options.*; import ghidra.framework.options.*;
import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.PluginTool;
import ghidra.util.*; import ghidra.framework.plugintool.ServiceProviderStub;
import ghidra.util.layout.PairLayout; import ghidra.util.Msg;
import ghidra.util.layout.VerticalLayout; import ghidra.util.Swing;
import gui.event.MouseBinding; import gui.event.MouseBinding;
import help.Help; import help.Help;
import help.HelpService; import help.HelpService;
@ -51,7 +51,8 @@ import resources.Icons;
*/ */
public class KeyBindingsPanel extends JPanel { public class KeyBindingsPanel extends JPanel {
private static final int STATUS_LABEL_HEIGHT = 60; private static final String GETTING_STARTED_MESSAGE =
"<html><i>Select an action to change a keybinding";
private final static int ACTION_NAME = 0; private final static int ACTION_NAME = 0;
private final static int KEY_BINDING = 1; private final static int KEY_BINDING = 1;
@ -61,7 +62,6 @@ public class KeyBindingsPanel extends JPanel {
private JTextPane statusLabel; private JTextPane statusLabel;
private GTable actionTable; private GTable actionTable;
private JPanel infoPanel;
private MultiLineLabel collisionLabel; private MultiLineLabel collisionLabel;
private KeyBindingsTableModel tableModel; private KeyBindingsTableModel tableModel;
private ActionBindingListener actionBindingListener = new ActionBindingListener(); private ActionBindingListener actionBindingListener = new ActionBindingListener();
@ -76,6 +76,9 @@ public class KeyBindingsPanel extends JPanel {
private boolean firingTableDataChanged; private boolean firingTableDataChanged;
private PropertyChangeListener propertyChangeListener; private PropertyChangeListener propertyChangeListener;
private JPanel gettingStartedPanel;
private JPanel activeActionPanel;
public KeyBindingsPanel(PluginTool tool) { public KeyBindingsPanel(PluginTool tool) {
this.tool = tool; this.tool = tool;
@ -110,7 +113,7 @@ public class KeyBindingsPanel extends JPanel {
// clear the action to avoid the appearance of editing while restoring // clear the action to avoid the appearance of editing while restoring
actionTable.clearSelection(); actionTable.clearSelection();
restoreDefaultKeybindings(); restoreDefaultKeyBindings();
}); });
} }
@ -135,50 +138,69 @@ public class KeyBindingsPanel extends JPanel {
} }
private void createPanelComponents() { private void createPanelComponents() {
setLayout(new BorderLayout(10, 10)); setLayout(new BorderLayout(10, 10));
// A stub panel to take up about the same amount of space as the active panel. This stub
// panel will get swapped for the active panel when a selection is made in the table. Using
// the stub panel is easier than trying to visually disable the editing widgets.
gettingStartedPanel = new JPanel();
activeActionPanel = createActiveActionPanel();
tableModel = new KeyBindingsTableModel(new ArrayList<>(keyBindings.getUniqueActions())); tableModel = new KeyBindingsTableModel(new ArrayList<>(keyBindings.getUniqueActions()));
actionTable = new GTable(tableModel); actionTable = new GTable(tableModel);
JScrollPane sp = new JScrollPane(actionTable); JScrollPane actionsScroller = new JScrollPane(actionTable);
actionTable.setPreferredScrollableViewportSize(new Dimension(400, 100)); actionTable.setPreferredScrollableViewportSize(new Dimension(400, 100));
actionTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); actionTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
actionTable.setHTMLRenderingEnabled(true); actionTable.setHTMLRenderingEnabled(true);
actionTable.getSelectionModel().addListSelectionListener(new TableSelectionListener());
adjustTableColumns(); adjustTableColumns();
// middle panel - filter field and import/export buttons // middle panel - filter field and import/export buttons
JPanel importExportPanel = createImportExportPanel(); JPanel importExportPanel = createImportExportPanel();
tableFilterPanel = new GTableFilterPanel<>(actionTable, tableModel); tableFilterPanel = new GTableFilterPanel<>(actionTable, tableModel);
JPanel middlePanel = new JPanel(new BorderLayout()); JPanel filterAndExportsPanel = new JPanel(new BorderLayout());
middlePanel.add(tableFilterPanel, BorderLayout.NORTH); filterAndExportsPanel.add(tableFilterPanel, BorderLayout.NORTH);
middlePanel.add(importExportPanel, BorderLayout.SOUTH); filterAndExportsPanel.add(importExportPanel, BorderLayout.SOUTH);
// contains the upper panel (table) and the middle panel) // contains the upper panel (table and the middle panel)
JPanel centerPanel = new JPanel(new BorderLayout()); JPanel centerPanel = new JPanel(new BorderLayout());
centerPanel.add(sp, BorderLayout.CENTER); centerPanel.add(actionsScroller, BorderLayout.CENTER);
centerPanel.add(middlePanel, BorderLayout.SOUTH); centerPanel.add(filterAndExportsPanel, BorderLayout.SOUTH);
add(centerPanel, BorderLayout.CENTER);
add(gettingStartedPanel, BorderLayout.SOUTH);
// make both panels the same size so that as we swap them, the UI doesn't jump
Dimension preferredSize = activeActionPanel.getPreferredSize();
gettingStartedPanel.setPreferredSize(preferredSize);
}
private JPanel createActiveActionPanel() {
// lower panel - key entry panel and status panel // lower panel - key entry panel and status panel
JPanel keyPanel = createKeyEntryPanel(); JPanel keyPanel = createKeyEntryPanel();
JComponent statusPanel = createStatusPanel(keyPanel); JPanel collisionAreaPanel = createCollisionArea();
add(centerPanel, BorderLayout.CENTER); JPanel parentPanel = new JPanel(new BorderLayout());
add(statusPanel, BorderLayout.SOUTH); parentPanel.add(keyPanel, BorderLayout.NORTH);
parentPanel.add(collisionAreaPanel, BorderLayout.SOUTH);
actionTable.getSelectionModel().addListSelectionListener(new TableSelectionListener()); return parentPanel;
} }
private JPanel createStatusPanel(JPanel keyPanel) { private JPanel createStatusPanel() {
statusLabel = new JTextPane(); statusLabel = new JTextPane();
statusLabel.setEnabled(false); statusLabel.setEnabled(false);
DockingUtils.setTransparent(statusLabel); DockingUtils.setTransparent(statusLabel);
statusLabel.setBorder(BorderFactory.createEmptyBorder(5, 10, 0, 5)); statusLabel.setBorder(BorderFactory.createEmptyBorder(5, 10, 0, 5));
statusLabel.setContentType("text/html"); // render any HTML we find in descriptions statusLabel.setContentType("text/html"); // render any HTML we find in descriptions
statusLabel.setText(GETTING_STARTED_MESSAGE);
// make sure the label gets enough space // make the label wide enough to show a line of text, but set a limit to force wrapping
statusLabel.setPreferredSize(new Dimension(0, STATUS_LABEL_HEIGHT)); statusLabel.setPreferredSize(new Dimension(300, 30));
statusLabel.setFont(Gui.getFont(FONT_ID)); statusLabel.setFont(Gui.getFont(FONT_ID));
helpButton = new EmptyBorderButton(Icons.HELP_ICON); helpButton = new EmptyBorderButton(Icons.HELP_ICON);
@ -189,70 +211,47 @@ public class KeyBindingsPanel extends JPanel {
hs.showHelp(action, false, KeyBindingsPanel.this); hs.showHelp(action, false, KeyBindingsPanel.this);
}); });
JPanel helpButtonPanel = new JPanel(); JPanel statusPanel = new JPanel();
helpButtonPanel.setLayout(new BoxLayout(helpButtonPanel, BoxLayout.PAGE_AXIS)); statusPanel.setLayout(new BoxLayout(statusPanel, BoxLayout.LINE_AXIS));
helpButtonPanel.add(helpButton); statusPanel.add(helpButton);
helpButtonPanel.add(Box.createVerticalGlue()); statusPanel.add(statusLabel);
JPanel lowerStatusPanel = new JPanel(); statusPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 0, 0));
lowerStatusPanel.setLayout(new BoxLayout(lowerStatusPanel, BoxLayout.X_AXIS));
lowerStatusPanel.add(helpButtonPanel);
lowerStatusPanel.add(statusLabel);
JPanel panel = new JPanel(new VerticalLayout(5)); return statusPanel;
panel.add(keyPanel);
panel.add(lowerStatusPanel);
return panel;
} }
private JPanel createKeyEntryPanel() { private JPanel createKeyEntryPanel() {
actionBindingPanel = new ActionBindingPanel(actionBindingListener); actionBindingPanel = new ActionBindingPanel(actionBindingListener);
// this is the lower panel that holds the key entry text field // add some space at the bottom of the input area to separate it from the info area
JPanel p = new JPanel(new FlowLayout(FlowLayout.LEFT)); actionBindingPanel.setBorder(BorderFactory.createEmptyBorder(10, 0, 20, 0));
p.add(actionBindingPanel);
JPanel keyPanel = new JPanel(new BorderLayout()); JPanel keyPanel = new JPanel(new BorderLayout());
keyPanel.add(actionBindingPanel, BorderLayout.NORTH);
JPanel defaultPanel = new JPanel(new BorderLayout());
// the content of the left-hand side label
MultiLineLabel mlabel =
new MultiLineLabel("To add or change a key binding, select an action\n" +
"and type any key combination.");
JPanel labelPanel = new JPanel();
labelPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 0, 0));
BoxLayout bl = new BoxLayout(labelPanel, BoxLayout.X_AXIS);
labelPanel.setLayout(bl);
labelPanel.add(Box.createHorizontalStrut(5));
labelPanel.add(new GIconLabel(Icons.INFO_ICON));
labelPanel.add(Box.createHorizontalStrut(5));
labelPanel.add(mlabel);
// the default panel is the panel that holds left-hand side label
defaultPanel.add(labelPanel, BorderLayout.NORTH);
defaultPanel.setBorder(BorderFactory.createLoweredBevelBorder());
// the info panel is the holds the right-hand label and is inside of
// a scroll pane
infoPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
collisionLabel = new MultiLineLabel(" ");
collisionLabel.setName("CollisionLabel");
infoPanel.add(collisionLabel);
JScrollPane sp = new JScrollPane(infoPanel);
sp.setPreferredSize(defaultPanel.getPreferredSize());
// inner panel holds the two label panels
JPanel innerPanel = new JPanel(new PairLayout(2, 6));
innerPanel.add(defaultPanel);
innerPanel.add(sp);
keyPanel.add(innerPanel, BorderLayout.CENTER);
keyPanel.add(p, BorderLayout.SOUTH);
return keyPanel; return keyPanel;
} }
private JPanel createCollisionArea() {
collisionLabel = new MultiLineLabel(" ");
collisionLabel.setVerticalAlignment(VerticalAlignment.TOP);
collisionLabel.setName("CollisionLabel");
JScrollPane collisionScroller = new JScrollPane(collisionLabel);
int height = 100; // enough to show the typical number of collisions without scrolling
collisionScroller.setPreferredSize(new Dimension(400, height));
// note: we add a strut so that when the scroll pane is hidden, the size does not change
JPanel parentPanel = new JPanel(new BorderLayout());
parentPanel.add(collisionScroller, BorderLayout.CENTER);
parentPanel.add(Box.createVerticalStrut(height), BorderLayout.WEST);
JPanel alignmentPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
alignmentPanel.add(parentPanel);
return alignmentPanel;
}
private JPanel createImportExportPanel() { private JPanel createImportExportPanel() {
JButton importButton = new JButton("Import..."); JButton importButton = new JButton("Import...");
importButton.setToolTipText("Load key binding settings from a file"); importButton.setToolTipText("Load key binding settings from a file");
@ -290,11 +289,16 @@ public class KeyBindingsPanel extends JPanel {
}); });
}); });
JPanel containerPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); JPanel statusPanel = createStatusPanel();
containerPanel.add(importButton);
containerPanel.add(exportButton);
return containerPanel; JPanel buttonPanel = new JPanel();
buttonPanel.add(importButton);
buttonPanel.add(exportButton);
JPanel parentPanel = new JPanel(new BorderLayout());
parentPanel.add(statusPanel, BorderLayout.WEST);
parentPanel.add(buttonPanel, BorderLayout.EAST);
return parentPanel;
} }
private boolean showApplyPrompt() { private boolean showApplyPrompt() {
@ -354,7 +358,7 @@ public class KeyBindingsPanel extends JPanel {
column.setPreferredWidth(150); column.setPreferredWidth(150);
} }
private void restoreDefaultKeybindings() { private void restoreDefaultKeyBindings() {
keyBindings.restoreOptions(); keyBindings.restoreOptions();
// let the table know that changes may have been made // let the table know that changes may have been made
@ -398,13 +402,20 @@ public class KeyBindingsPanel extends JPanel {
} }
private void updateCollisionPanel(String text) { private void updateCollisionPanel(String text) {
infoPanel.removeAll();
infoPanel.repaint(); // Hide the scroll pane when there is nothing to show
collisionLabel = new MultiLineLabel(text); Container parent = collisionLabel.getParent().getParent();
collisionLabel.setName("CollisionLabel"); if (text.isBlank()) {
infoPanel.add(collisionLabel); parent.setVisible(false);
infoPanel.invalidate(); }
else {
parent.setVisible(true);
}
collisionLabel.setLabel(text);
collisionLabel.invalidate();
validate(); validate();
repaint();
} }
private void loadKeyBindingsFromImportedOptions(Options keyBindingOptions) { private void loadKeyBindingsFromImportedOptions(Options keyBindingOptions) {
@ -448,7 +459,7 @@ public class KeyBindingsPanel extends JPanel {
DockingActionIf action = getSelectedAction(); DockingActionIf action = getSelectedAction();
if (action == null) { if (action == null) {
statusLabel.setText("No action is selected."); statusLabel.setText(GETTING_STARTED_MESSAGE);
return; return;
} }
@ -463,7 +474,7 @@ public class KeyBindingsPanel extends JPanel {
String selectedActionName = action.getFullName(); String selectedActionName = action.getFullName();
if (setActionKeyStroke(selectedActionName, ks)) { if (setActionKeyStroke(selectedActionName, ks)) {
showActionsMappedToKeyStroke(ks); showActionsMappedToKeyStroke(ks);
tableModel.fireTableDataChanged(); fireRowChanged();
changesMade(true); changesMade(true);
} }
} }
@ -474,17 +485,23 @@ public class KeyBindingsPanel extends JPanel {
DockingActionIf action = getSelectedAction(); DockingActionIf action = getSelectedAction();
if (action == null) { if (action == null) {
statusLabel.setText("No action is selected."); statusLabel.setText(GETTING_STARTED_MESSAGE);
return; return;
} }
String selectedActionName = action.getFullName(); String selectedActionName = action.getFullName();
if (setMouseBinding(selectedActionName, mb)) { if (setMouseBinding(selectedActionName, mb)) {
tableModel.fireTableDataChanged(); fireRowChanged();
changesMade(true); changesMade(true);
} }
} }
private void fireRowChanged() {
int viewRow = actionTable.getSelectedRow();
int modelRow = tableFilterPanel.getModelRow(viewRow);
tableModel.fireTableRowsUpdated(modelRow, modelRow);
}
private boolean setMouseBinding(String actionName, MouseBinding mouseBinding) { private boolean setMouseBinding(String actionName, MouseBinding mouseBinding) {
if (keyBindings.isMouseBindingInUse(actionName, mouseBinding)) { if (keyBindings.isMouseBindingInUse(actionName, mouseBinding)) {
@ -525,9 +542,25 @@ public class KeyBindingsPanel extends JPanel {
return keyBindings.getKeyStrokesByFullActionName(); return keyBindings.getKeyStrokesByFullActionName();
} }
private void swapView(JComponent newView) {
// the lower panel we want to swap is at index 1 (index 0 is the table area)
Component component = getComponent(1);
if (component == newView) {
return; // nothing to do
}
remove(component);
add(newView, BorderLayout.SOUTH);
Container parent = getParent();
parent.validate();
parent.repaint();
}
//================================================================================================== //==================================================================================================
// Inner Classes // Inner Classes
//================================================================================================== //==================================================================================================
/** /**
* Selection listener class for the table model. * Selection listener class for the table model.
*/ */
@ -539,15 +572,22 @@ public class KeyBindingsPanel extends JPanel {
} }
helpButton.setEnabled(false); helpButton.setEnabled(false);
String fullActionName = getSelectedActionName();
if (fullActionName == null) { DockingActionIf action = getSelectedAction();
statusLabel.setText(""); if (action == null) {
swapView(gettingStartedPanel);
statusLabel.setText(GETTING_STARTED_MESSAGE);
actionBindingPanel.setEnabled(false); actionBindingPanel.setEnabled(false);
helpButton.setToolTipText("Select action in table for help");
return; return;
} }
actionBindingPanel.setEnabled(true); String fullActionName = getSelectedActionName();
swapView(activeActionPanel);
actionBindingPanel.setEnabled(true);
helpButton.setEnabled(true); helpButton.setEnabled(true);
clearInfoPanel(); clearInfoPanel();
@ -559,59 +599,54 @@ public class KeyBindingsPanel extends JPanel {
MouseBinding mb = keyBindings.getMouseBinding(fullActionName); MouseBinding mb = keyBindings.getMouseBinding(fullActionName);
actionBindingPanel.setKeyBindingData(ks, mb); actionBindingPanel.setKeyBindingData(ks, mb);
// make sure the label gets enough space
statusLabel.setPreferredSize(
new Dimension(statusLabel.getPreferredSize().width, STATUS_LABEL_HEIGHT));
DockingActionIf action = getSelectedAction();
String description = action.getDescription(); String description = action.getDescription();
if (description == null || description.trim().isEmpty()) { if (StringUtils.isBlank(description)) {
description = action.getName(); description = action.getName();
} }
statusLabel.setText("<html>" + HTMLUtilities.escapeHTML(description)); // Not sure why we escape the html here. Probably just to be safe.
statusLabel.setText("<html>" + description);
helpButton.setToolTipText("Help for " + action.getName());
} }
} }
private class KeyBindingsTableModel extends AbstractSortedTableModel<DockingActionIf> { private class KeyBindingsTableModel
private final String[] columnNames = { "Action Name", "KeyBinding", "Plugin Name" }; extends GDynamicColumnTableModel<DockingActionIf, Object> {
private List<DockingActionIf> actions; private List<DockingActionIf> actions;
KeyBindingsTableModel(List<DockingActionIf> actions) { public KeyBindingsTableModel(List<DockingActionIf> actions) {
super(0); super(new ServiceProviderStub());
this.actions = actions; this.actions = actions;
} }
@Override @Override
public String getName() { protected TableColumnDescriptor<DockingActionIf> createTableColumnDescriptor() {
return "Keybindings"; TableColumnDescriptor<DockingActionIf> 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("Owner", String.class, a -> a.getOwnerDescription());
descriptor.addHiddenColumn("Description", String.class, a -> a.getDescription());
return descriptor;
} }
@Override @Override
public Object getColumnValueForRow(DockingActionIf action, int columnIndex) { public String getName() {
return "Key Bindings";
String fullName = action.getFullName();
switch (columnIndex) {
case ACTION_NAME:
return action.getName();
case KEY_BINDING:
String text = "";
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();
case PLUGIN_NAME:
return action.getOwnerDescription();
}
return "Unknown Column!";
} }
@Override @Override
@ -620,28 +655,8 @@ public class KeyBindingsPanel extends JPanel {
} }
@Override @Override
public boolean isSortable(int columnIndex) { public Object getDataSource() {
return true; return null;
}
@Override
public String getColumnName(int column) {
return columnNames[column];
}
@Override
public int getColumnCount() {
return columnNames.length;
}
@Override
public int getRowCount() {
return actions.size();
}
@Override
public Class<?> getColumnClass(int columnIndex) {
return String.class;
} }
} }