Demonstrates basic FilterTableModel, FilterComboBoxModel and FilterTable usage.
gradlew demo-manual:runKeyBindingPanel
1. KeyBindingPanel
package is.codion.manual.keybinding;
import is.codion.manual.keybinding.KeyBindingModel.KeyBindingColumns.ColumnId;
import is.codion.manual.keybinding.KeyBindingModel.KeyBindingRow;
import is.codion.plugin.flatlaf.intellij.themes.monokaipro.MonokaiPro;
import is.codion.swing.common.ui.Windows;
import is.codion.swing.common.ui.component.table.FilterTable;
import is.codion.swing.common.ui.component.table.FilterTableColumn;
import is.codion.swing.common.ui.laf.LookAndFeelComboBox;
import is.codion.swing.common.ui.laf.LookAndFeelEnabler;
import javax.swing.JComboBox;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import java.awt.BorderLayout;
import java.util.List;
import static is.codion.swing.common.ui.border.Borders.emptyBorder;
import static is.codion.swing.common.ui.component.Components.*;
import static is.codion.swing.common.ui.laf.LookAndFeelComboBox.lookAndFeelComboBox;
import static is.codion.swing.common.ui.laf.LookAndFeelProvider.findLookAndFeel;
import static is.codion.swing.common.ui.layout.Layouts.borderLayout;
/**
* A utility for displaying component action/input maps for installed look and feels.<br>
* Based on <a href="https://tips4java.wordpress.com/2008/10/10/key-bindings/">KeyBindings.java by Rob Comick</a>
* @author Rob Camick
* @author bjorndarri
*/
public final class KeyBindingPanel extends JPanel {
private final LookAndFeelComboBox lookAndFeelComboBox = lookAndFeelComboBox(true);
private final KeyBindingModel keyBindingModel;
private final FilterTable<KeyBindingRow, ColumnId> table;
private final JComboBox<String> componentComboBox;
public KeyBindingPanel() {
super(borderLayout());
this.keyBindingModel = new KeyBindingModel(lookAndFeelComboBox.getModel());
this.table = FilterTable.builder(keyBindingModel.tableModel(), createColumns())
.autoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS)
.build();
this.componentComboBox = comboBox(keyBindingModel.componentModel())
.preferredWidth(200)
.build();
setBorder(emptyBorder());
add(flexibleGridLayoutPanel(1, 4)
.add(label("Look & Feel")
.horizontalAlignment(SwingConstants.RIGHT)
.preferredWidth(100)
.build())
.add(lookAndFeelComboBox)
.add(label("Component")
.horizontalAlignment(SwingConstants.RIGHT)
.preferredWidth(100)
.build())
.add(componentComboBox)
.build(), BorderLayout.NORTH);
add(new JScrollPane(table), BorderLayout.CENTER);
add(table.searchField(), BorderLayout.SOUTH);
}
private static List<FilterTableColumn<ColumnId>> createColumns() {
return List.of(FilterTableColumn.builder(ColumnId.ACTION)
.headerValue("Action")
.build(),
FilterTableColumn.builder(ColumnId.WHEN_FOCUSED)
.headerValue("When Focused")
.build(),
FilterTableColumn.builder(ColumnId.WHEN_IN_FOCUSED_WINDOW)
.headerValue("When in Focused Window")
.build(),
FilterTableColumn.builder(ColumnId.WHEN_ANCESTOR)
.headerValue("When Ancestor")
.build());
}
public static void main(String[] args) {
System.setProperty("sun.awt.disablegrab", "true");
findLookAndFeel(MonokaiPro.class)
.ifPresent(LookAndFeelEnabler::enable);
SwingUtilities.invokeLater(() -> Windows.frame(new KeyBindingPanel())
.title("Key Bindings")
.defaultCloseOperation(WindowConstants.EXIT_ON_CLOSE)
.centerFrame(true)
.show());
}
}
2. KeyBindingTableModel
package is.codion.manual.keybinding;
import is.codion.common.item.Item;
import is.codion.manual.keybinding.KeyBindingModel.KeyBindingColumns.ColumnId;
import is.codion.swing.common.model.component.combobox.FilterComboBoxModel;
import is.codion.swing.common.model.component.table.FilterTableModel;
import is.codion.swing.common.model.component.table.FilterTableModel.TableColumns;
import is.codion.swing.common.ui.laf.LookAndFeelEnabler;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.KeyStroke;
import javax.swing.LookAndFeel;
import java.util.Arrays;
import java.util.Collection;
import java.util.Hashtable;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.joining;
final class KeyBindingModel {
private static final Set<String> EXCLUDED_COMPONENTS = Set.of("PopupMenuSeparator", "ToolBarSeparator", "DesktopIcon");
private static final String PACKAGE = "javax.swing.";
private static final String PRESSED = "pressed ";
private static final String RELEASED = "released ";
private final FilterComboBoxModel<String> componentModel;
private final FilterTableModel<KeyBindingRow, ColumnId> tableModel;
KeyBindingModel(FilterComboBoxModel<Item<LookAndFeelEnabler>> lookAndFeelModel) {
this.componentModel = FilterComboBoxModel.builder(new ComponentItems(lookAndFeelModel)).build();
this.componentModel.items().refresh();
this.tableModel = FilterTableModel.builder(new KeyBindingColumns())
.supplier(new KeyBindingItems())
.build();
bindEvents(lookAndFeelModel);
}
FilterComboBoxModel<String> componentModel() {
return componentModel;
}
FilterTableModel<KeyBindingRow, ColumnId> tableModel() {
return tableModel;
}
private void bindEvents(FilterComboBoxModel<?> lookAndFeelModel) {
// Refresh the component combo box when a look and feel is selected
lookAndFeelModel.selection().item().addListener(componentModel.items()::refresh);
// Refresh the table model when the component combo box has been refreshed
componentModel.items().refresher().result().addListener(tableModel.items()::refresh);
// And when a component is selected
componentModel.selection().item().addListener(tableModel.items()::refresh);
}
record KeyBindingRow(String action, String whenFocused, String whenInFocusedWindow, String whenAncestor) {
Object value(ColumnId columnId) {
return switch (columnId) {
case ACTION -> action;
case WHEN_FOCUSED -> whenFocused;
case WHEN_IN_FOCUSED_WINDOW -> whenInFocusedWindow;
case WHEN_ANCESTOR -> whenAncestor;
};
}
}
static final class KeyBindingColumns implements TableColumns<KeyBindingRow, ColumnId> {
enum ColumnId {
ACTION,
WHEN_FOCUSED,
WHEN_IN_FOCUSED_WINDOW,
WHEN_ANCESTOR
}
private static final List<ColumnId> IDENTIFIERS = List.of(ColumnId.values());
@Override
public List<ColumnId> identifiers() {
return IDENTIFIERS;
}
@Override
public Class<?> columnClass(ColumnId columnId) {
return String.class;
}
@Override
public Object value(KeyBindingRow row, ColumnId columnId) {
return row.value(columnId);
}
}
// Provides the items when populating the component combo box model
private static final class ComponentItems implements Supplier<Collection<String>> {
private final FilterComboBoxModel<Item<LookAndFeelEnabler>> lookAndFeelModel;
private ComponentItems(FilterComboBoxModel<Item<LookAndFeelEnabler>> lookAndFeelModel) {
this.lookAndFeelModel = lookAndFeelModel;
}
@Override
public Collection<String> get() {
return lookAndFeelModel.selection().item().optional()
.map(Item::value)
.map(LookAndFeelEnabler::lookAndFeel)
.map(LookAndFeel::getDefaults)
.map(Hashtable::keySet)
.map(Collection::stream)
.map(keys -> keys
.map(Object::toString)
.map(ComponentItems::componentName)
.flatMap(Optional::stream)
.sorted()
.toList())
.orElse(List.of());
}
private static Optional<String> componentName(String key) {
if (key.endsWith("UI") && key.indexOf(".") == -1) {
String componentName = key.substring(0, key.length() - 2);
if (!EXCLUDED_COMPONENTS.contains(componentName)) {
return Optional.of("J" + componentName);
}
}
return Optional.empty();
}
}
// Provides the rows when populating the key binding table model
private final class KeyBindingItems implements Supplier<Collection<KeyBindingRow>> {
@Override
public Collection<KeyBindingRow> get() {
return componentModel.selection().item().optional()
.map(KeyBindingItems::componentClassName)
.map(KeyBindingItems::keyBindings)
.orElse(List.of());
}
private static String componentClassName(String componentName) {
if (componentName.equals("JTableHeader")) {
return PACKAGE + "table." + componentName;
}
return PACKAGE + componentName;
}
private static List<KeyBindingRow> keyBindings(String componentClassName) {
try {
JComponent component = (JComponent) Class.forName(componentClassName).getDeclaredConstructor().newInstance();
ActionMap actionMap = component.getActionMap();
Object[] allKeys = actionMap.allKeys();
if (allKeys == null) {
return List.of();
}
return Arrays.stream(allKeys)
.sorted(comparing(Objects::toString))
.map(actionKey -> row(actionKey, component))
.toList();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
private static KeyBindingRow row(Object actionKey, JComponent component) {
return new KeyBindingRow(actionKey.toString(),
keyStrokes(actionKey, component.getInputMap(JComponent.WHEN_FOCUSED)),
keyStrokes(actionKey, component.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)),
keyStrokes(actionKey, component.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)));
}
private static String keyStrokes(Object actionKey, InputMap inputMap) {
KeyStroke[] allKeys = inputMap.allKeys();
if (allKeys == null) {
return "";
}
return Arrays.stream(allKeys)
.filter(keyStroke -> inputMap.get(keyStroke).equals(actionKey))
.map(Objects::toString)
.map(KeyBindingItems::movePressedReleased)
.collect(joining(", "));
}
private static String movePressedReleased(String keyStroke) {
if (keyStroke.contains(PRESSED)) {
return keyStroke.replace(PRESSED, "") + " pressed";
}
if (keyStroke.contains(RELEASED)) {
return keyStroke.replace(RELEASED, "") + " released";
}
return keyStroke;
}
}
}