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;
    }
  }
}