diff --git a/demos/manual/build.gradle.kts b/demos/manual/build.gradle.kts index e73bfc2d60..ab7f884377 100644 --- a/demos/manual/build.gradle.kts +++ b/demos/manual/build.gradle.kts @@ -55,4 +55,10 @@ tasks.register("runApplicationPanel") { group = "application" classpath = sourceSets.main.get().runtimeClasspath mainClass = "is.codion.manual.common.demo.ApplicationPanel" +} + +tasks.register("runKeyBindingPanel") { + group = "application" + classpath = sourceSets.main.get().runtimeClasspath + mainClass = "is.codion.manual.keybinding.KeyBindingPanel" } \ No newline at end of file diff --git a/demos/manual/src/main/java/is/codion/manual/keybinding/KeyBindingModel.java b/demos/manual/src/main/java/is/codion/manual/keybinding/KeyBindingModel.java index 8a592e4ba8..383e098f1b 100644 --- a/demos/manual/src/main/java/is/codion/manual/keybinding/KeyBindingModel.java +++ b/demos/manual/src/main/java/is/codion/manual/keybinding/KeyBindingModel.java @@ -19,7 +19,7 @@ package is.codion.manual.keybinding; import is.codion.common.item.Item; -import is.codion.manual.keybinding.KeyBindingModel.KeyBindingColumns.Id; +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.Columns; @@ -29,115 +29,153 @@ 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.Arrays.asList; import static java.util.Comparator.comparing; import static java.util.stream.Collectors.joining; -import static java.util.stream.Collectors.toList; final class KeyBindingModel { - private static final Collection EXCLUDED_COMPONENTS = asList("PopupMenuSeparator", "ToolBarSeparator", "DesktopIcon"); + private static final Set 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 FilterTableModel tableModel; - private final FilterComboBoxModel componentComboBoxModel; + private final FilterComboBoxModel componentModel; + private final FilterTableModel tableModel; - KeyBindingModel(FilterComboBoxModel> lookAndFeelComboBoxModel) { - this.componentComboBoxModel = FilterComboBoxModel.builder(new ComponentItems(lookAndFeelComboBoxModel)).build(); - this.componentComboBoxModel.refresh(); + KeyBindingModel(FilterComboBoxModel> lookAndFeelModel) { + this.componentModel = FilterComboBoxModel.builder(new ComponentItems(lookAndFeelModel)).build(); + this.componentModel.refresh(); this.tableModel = FilterTableModel.builder(new KeyBindingColumns()) .supplier(new KeyBindingItems()) .build(); - bindEvents(lookAndFeelComboBoxModel); + bindEvents(lookAndFeelModel); } - FilterComboBoxModel componentComboBoxModel() { - return componentComboBoxModel; + FilterComboBoxModel componentModel() { + return componentModel; } - FilterTableModel tableModel() { + FilterTableModel tableModel() { return tableModel; } - private void bindEvents(FilterComboBoxModel> lookAndFeelComboBoxModel) { - componentComboBoxModel.refresher().success().addListener(tableModel::refresh); - componentComboBoxModel.selection().item().addListener(tableModel::refresh); - lookAndFeelComboBoxModel.selection().item().addListener(componentComboBoxModel::refresh); + private void bindEvents(FilterComboBoxModel lookAndFeelModel) { + // Refresh the component combo box when a look and feel is selected + lookAndFeelModel.selection().item().addListener(componentModel::refresh); + // Refresh the table model when the component combo box has been refreshed + componentModel.refresher().success().addListener(tableModel::refresh); + // And when a component is selected + componentModel.selection().item().addListener(tableModel::refresh); } - private static String className(String componentName) { - if (componentName.equals("JTableHeader")) { - return PACKAGE + "table." + componentName; - } + record KeyBindingRow(String action, String whenFocused, String whenInFocusedWindow, String whenAncestor) { - return PACKAGE + componentName; + 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 KeyBinding { + static final class KeyBindingColumns implements Columns { - private final String action; - private final String whenFocused; - private final String whenInFocusedWindow; - private final String whenAncestor; + enum ColumnId { + ACTION, + WHEN_FOCUSED, + WHEN_IN_FOCUSED_WINDOW, + WHEN_ANCESTOR + } - private KeyBinding(String action, String whenFocused, String whenInFocusedWindow, String whenAncestor) { - this.action = action; - this.whenFocused = whenFocused; - this.whenInFocusedWindow = whenInFocusedWindow; - this.whenAncestor = whenAncestor; + private static final List IDENTIFIERS = List.of(ColumnId.values()); + + @Override + public List identifiers() { + return IDENTIFIERS; } - private static KeyBinding create(Object actionKey, JComponent component) { - return new KeyBinding(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))); + @Override + public Class columnClass(ColumnId columnId) { + return String.class; } - private static String keyStrokes(Object actionKey, InputMap inputMap) { - KeyStroke[] allKeys = inputMap.allKeys(); - if (allKeys == null) { - return ""; - } + @Override + public Object value(KeyBindingRow row, ColumnId columnId) { + return row.value(columnId); + } + } - return Arrays.stream(allKeys) - .filter(keyStroke -> inputMap.get(keyStroke).equals(actionKey)) - .map(Objects::toString) - .map(KeyBinding::movePressedReleased) - .collect(joining(", ")); + // Provides the items when populating the component combo box model + private static final class ComponentItems implements Supplier> { + + private final FilterComboBoxModel> lookAndFeelModel; + + private ComponentItems(FilterComboBoxModel> lookAndFeelModel) { + this.lookAndFeelModel = lookAndFeelModel; } - private static String movePressedReleased(String keyStroke) { - if (keyStroke.contains(PRESSED)) { - return keyStroke.replace(PRESSED, "") + " pressed"; - } - if (keyStroke.contains(RELEASED)) { - return keyStroke.replace(RELEASED, "") + " released"; + @Override + public Collection get() { + return lookAndFeelModel.selection().item().optional() + .map(Item::value) + .map(LookAndFeelProvider::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 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 keyStroke; + return Optional.empty(); } } - private final class KeyBindingItems implements Supplier> { + // Provides the rows when populating the key binding table model + private final class KeyBindingItems implements Supplier> { @Override - public Collection get() { - String componentName = componentComboBoxModel.getSelectedItem(); - if (componentName == null) { - return List.of(); + public Collection 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; } - String componentClassName = className(componentName); + + return PACKAGE + componentName; + } + + private static List keyBindings(String componentClassName) { try { JComponent component = (JComponent) Class.forName(componentClassName).getDeclaredConstructor().newInstance(); ActionMap actionMap = component.getActionMap(); @@ -148,85 +186,43 @@ public Collection get() { return Arrays.stream(allKeys) .sorted(comparing(Objects::toString)) - .map(actionKey -> KeyBinding.create(actionKey, component)) - .collect(toList()); + .map(actionKey -> row(actionKey, component)) + .toList(); } catch (Exception e) { throw new RuntimeException(e); } } - } - - public static final class KeyBindingColumns implements Columns { - public enum Id { - ACTION_COLUMN, - WHEN_FOCUSED_COLUMN, - WHEN_IN_FOCUSED_WINDOW_COLUMN, - WHEN_ANCESTOR_COLUMN - } - - private static final List IDENTIFIERS = List.of(Id.values()); - - @Override - public List identifiers() { - return IDENTIFIERS; - } - - @Override - public Class columnClass(Id identifier) { - return String.class; - } - - @Override - public Object value(KeyBinding keyBinding, Id identifier) { - return switch (identifier) { - case ACTION_COLUMN -> keyBinding.action; - case WHEN_FOCUSED_COLUMN -> keyBinding.whenFocused; - case WHEN_IN_FOCUSED_WINDOW_COLUMN -> keyBinding.whenInFocusedWindow; - case WHEN_ANCESTOR_COLUMN -> keyBinding.whenAncestor; - }; - } - } - - private static final class ComponentItems implements Supplier> { - - private final FilterComboBoxModel> lookAndFeelComboBoxModel; - - private ComponentItems(FilterComboBoxModel> lookAndFeelComboBoxModel) { - this.lookAndFeelComboBoxModel = lookAndFeelComboBoxModel; + 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))); } - @Override - public Collection get() { - Item selectedItem = lookAndFeelComboBoxModel.getSelectedItem(); - if (selectedItem == null) { - return List.of(); + private static String keyStrokes(Object actionKey, InputMap inputMap) { + KeyStroke[] allKeys = inputMap.allKeys(); + if (allKeys == null) { + return ""; } - LookAndFeelProvider lookAndFeelProvider = selectedItem.value(); - try { - return lookAndFeelProvider.lookAndFeel().getDefaults().keySet().stream() - .map(Object::toString) - .map(ComponentItems::componentName) - .flatMap(Optional::stream) - .sorted() - .collect(toList()); - } - catch (Exception e) { - throw new RuntimeException(e); - } + return Arrays.stream(allKeys) + .filter(keyStroke -> inputMap.get(keyStroke).equals(actionKey)) + .map(Objects::toString) + .map(KeyBindingItems::movePressedReleased) + .collect(joining(", ")); } - private static Optional 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); - } + 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 Optional.empty(); + return keyStroke; } } } diff --git a/demos/manual/src/main/java/is/codion/manual/keybinding/KeyBindingPanel.java b/demos/manual/src/main/java/is/codion/manual/keybinding/KeyBindingPanel.java index 20a2c61030..d95d774b40 100644 --- a/demos/manual/src/main/java/is/codion/manual/keybinding/KeyBindingPanel.java +++ b/demos/manual/src/main/java/is/codion/manual/keybinding/KeyBindingPanel.java @@ -18,8 +18,8 @@ */ package is.codion.manual.keybinding; -import is.codion.manual.keybinding.KeyBindingModel.KeyBinding; -import is.codion.manual.keybinding.KeyBindingModel.KeyBindingColumns.Id; +import is.codion.manual.keybinding.KeyBindingModel.KeyBindingColumns.ColumnId; +import is.codion.manual.keybinding.KeyBindingModel.KeyBindingRow; 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; @@ -31,6 +31,7 @@ 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; @@ -38,13 +39,11 @@ import java.util.Arrays; import java.util.List; -import static is.codion.manual.keybinding.KeyBindingModel.KeyBindingColumns.Id.*; 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.component.text.TextComponents.preferredTextFieldHeight; import static is.codion.swing.common.ui.laf.LookAndFeelComboBox.lookAndFeelComboBox; +import static is.codion.swing.common.ui.laf.LookAndFeelProvider.findLookAndFeelProvider; import static is.codion.swing.common.ui.layout.Layouts.borderLayout; -import static java.util.Arrays.asList; /** * A utility for displaying component action/input maps for installed look and feels.
@@ -56,15 +55,16 @@ public final class KeyBindingPanel extends JPanel { private final LookAndFeelComboBox lookAndFeelComboBox = lookAndFeelComboBox(true); private final KeyBindingModel keyBindingModel; - private final FilterTable table; + private final FilterTable table; private final JComboBox componentComboBox; public KeyBindingPanel() { super(borderLayout()); this.keyBindingModel = new KeyBindingModel(lookAndFeelComboBox.getModel()); - this.table = FilterTable.builder(keyBindingModel.tableModel(), createColumns()).build(); - this.componentComboBox = comboBox(keyBindingModel.componentComboBoxModel()) - .preferredHeight(preferredTextFieldHeight()) + this.table = FilterTable.builder(keyBindingModel.tableModel(), createColumns()) + .autoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS) + .build(); + this.componentComboBox = comboBox(keyBindingModel.componentModel()) .preferredWidth(200) .build(); setBorder(emptyBorder()); @@ -84,27 +84,27 @@ public KeyBindingPanel() { add(table.searchField(), BorderLayout.SOUTH); } - private static List> createColumns() { - FilterTableColumn action = FilterTableColumn.builder(ACTION_COLUMN) - .headerValue("Action") - .build(); - FilterTableColumn whenFocused = FilterTableColumn.builder(WHEN_FOCUSED_COLUMN) - .headerValue("When Focused") - .build(); - FilterTableColumn whenInFocusedWindow = FilterTableColumn.builder(WHEN_IN_FOCUSED_WINDOW_COLUMN) - .headerValue("When in Focused Window") - .build(); - FilterTableColumn whenAncestor = FilterTableColumn.builder(WHEN_ANCESTOR_COLUMN) - .headerValue("When Ancestor") - .build(); - - return asList(action, whenFocused, whenInFocusedWindow, whenAncestor); + private static List> 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"); Arrays.stream(FlatAllIJThemes.INFOS) .forEach(LookAndFeelProvider::addLookAndFeel); + findLookAndFeelProvider("com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMonokaiProIJTheme") + .ifPresent(LookAndFeelProvider::enable); SwingUtilities.invokeLater(() -> Windows.frame(new KeyBindingPanel()) .title("Key Bindings") .defaultCloseOperation(WindowConstants.EXIT_ON_CLOSE) diff --git a/documentation/src/docs/asciidoc/tutorials/keybinding.adoc b/documentation/src/docs/asciidoc/tutorials/keybinding.adoc index 4fd5a9f1d7..99e36ef390 100644 --- a/documentation/src/docs/asciidoc/tutorials/keybinding.adoc +++ b/documentation/src/docs/asciidoc/tutorials/keybinding.adoc @@ -3,6 +3,13 @@ :docinfo: shared-head :dir-source: ../../../../../demos/manual/src/main/java +Demonstrates basic FilterTableModel, FilterComboBoxModel and FilterTable usage. + +[source,shell] +---- +gradlew codion-demos-manual:runKeyBindingPanel +---- + == KeyBindingPanel [source,java,linenums] diff --git a/swing/common-model/src/main/java/is/codion/swing/common/model/component/table/FilterTableModel.java b/swing/common-model/src/main/java/is/codion/swing/common/model/component/table/FilterTableModel.java index 6b22a914cb..433e246ecc 100644 --- a/swing/common-model/src/main/java/is/codion/swing/common/model/component/table/FilterTableModel.java +++ b/swing/common-model/src/main/java/is/codion/swing/common/model/component/table/FilterTableModel.java @@ -227,6 +227,8 @@ interface Builder { interface Columns { /** + * This method gets called quite often, so it is recommended to return + * a constant List instance, instead of creating one each time. * @return the column identifiers */ List identifiers();