diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/view/TerminalUI.java b/spring-shell-core/src/main/java/org/springframework/shell/component/view/TerminalUI.java index 77e03d7b..1251d425 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/view/TerminalUI.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/view/TerminalUI.java @@ -229,12 +229,24 @@ public class TerminalUI { private void handleKeyEvent(KeyEvent event) { log.trace("handleKeyEvent {}", event); - if (rootView != null && rootView.hasFocus()) { - KeyHandler handler = rootView.getKeyHandler(); + + if (rootView != null) { + // if hotkeys consume, we're done + KeyHandler handler = rootView.getHotKeyHandler(); if (handler != null) { KeyHandlerResult result = handler.handle(KeyHandler.argsOf(event)); - if (result.focus() != null) { - setFocus(result.focus()); + if (result.consumed()) { + return; + } + } + // continue with one having focus + if (rootView.hasFocus()) { + handler = rootView.getKeyHandler(); + if (handler != null) { + KeyHandlerResult result = handler.handle(KeyHandler.argsOf(event)); + if (result.focus() != null) { + setFocus(result.focus()); + } } } } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/AbstractView.java b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/AbstractView.java index 2b95faf1..262f7bd4 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/AbstractView.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/AbstractView.java @@ -55,6 +55,7 @@ public abstract class AbstractView extends AbstractControl implements View { private int layer; private EventLoop eventLoop; private Map keyBindings = new HashMap<>(); + private Map hotKeyBindings = new HashMap<>(); private Map mouseBindings = new HashMap<>(); public AbstractView() { @@ -180,6 +181,25 @@ public abstract class AbstractView extends AbstractControl implements View { return handler; } + @Override + public KeyHandler getHotKeyHandler() { + log.trace("getHotKeyHandler() {}", this); + KeyHandler handler = args -> { + KeyEvent event = args.event(); + boolean consumed = false; + Integer key = event.key(); + if (key != null) { + KeyBindingValue keyBindingValue = getHotKeyBindings().get(key); + if (keyBindingValue != null) { + consumed = dispatchRunCommand(event, keyBindingValue); + } + + } + return KeyHandler.resultOf(event, consumed, null); + }; + return handler; + } + /** * Sets a callback function which is invoked after a {@link View} has been * drawn. @@ -236,6 +256,24 @@ public abstract class AbstractView extends AbstractControl implements View { }); } + protected void registerHotKeyBinding(Integer keyType, String keyCommand) { + registerHotKeyBinding(keyType, keyCommand, null, null); + } + + protected void registerHotKeyBinding(Integer keyType, KeyBindingConsumer keyConsumer) { + registerHotKeyBinding(keyType, null, keyConsumer, null); + } + + protected void registerHotKeyBinding(Integer keyType, Runnable keyRunnable) { + registerHotKeyBinding(keyType, null, null, keyRunnable); + } + + private void registerHotKeyBinding(Integer keyType, String keyCommand, KeyBindingConsumer keyConsumer, Runnable keyRunnable) { + hotKeyBindings.compute(keyType, (key, old) -> { + return KeyBindingValue.of(old, keyCommand, keyConsumer, keyRunnable); + }); + } + record KeyBindingValue(String keyCommand, KeyBindingConsumer keyConsumer, Runnable keyRunnable) { static KeyBindingValue of(KeyBindingValue old, String keyCommand, KeyBindingConsumer keyConsumer, Runnable keyRunnable) { @@ -257,6 +295,15 @@ public abstract class AbstractView extends AbstractControl implements View { return keyBindings; } + /** + * Get hotkey bindings. + * + * @return hotkey bindings + */ + protected Map getHotKeyBindings() { + return hotKeyBindings; + } + record MouseBindingValue(String mouseCommand, MouseBindingConsumer mouseConsumer, Runnable mouseRunnable, Predicate mousePredicate) { static MouseBindingValue of(MouseBindingValue old, String mouseCommand, MouseBindingConsumer mouseConsumer, diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/AppView.java b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/AppView.java index e80d8dee..d9e42491 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/AppView.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/AppView.java @@ -133,6 +133,11 @@ public class AppView extends BoxView { return otherHandler.thenIfNotConsumed(handler); } + @Override + public KeyHandler getHotKeyHandler() { + return menu != null ? menu.getHotKeyHandler() : super.getHotKeyHandler(); + } + @Override public boolean hasFocus() { if (grid != null) { diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/MenuBarView.java b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/MenuBarView.java index e50b28b8..2082dae3 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/MenuBarView.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/MenuBarView.java @@ -35,8 +35,8 @@ import org.springframework.shell.component.view.geom.Dimension; import org.springframework.shell.component.view.geom.Rectangle; import org.springframework.shell.component.view.screen.Screen; import org.springframework.shell.component.view.screen.Screen.Writer; -import org.springframework.shell.style.ThemeResolver; import org.springframework.shell.component.view.screen.ScreenItem; +import org.springframework.shell.style.ThemeResolver; /** * {@link MenuBarView} shows {@link MenuBarItem items} horizontally and is @@ -187,6 +187,17 @@ public class MenuBarView extends BoxView { return x; } + private int itemIndex(MenuBarItem item) { + int index = 0; + for (MenuBarItem i : items) { + if (i == item) { + return index; + } + index++; + } + return -1; + } + private void select(MouseEvent event) { int x = event.x(); int y = event.y(); @@ -256,6 +267,23 @@ public class MenuBarView extends BoxView { public void setItems(List items) { this.items.clear(); this.items.addAll(items); + registerHotKeys(); + } + + private void selectItem(MenuBarItem item) { + int index = itemIndex(item); + if (index > -1) { + setSelected(index); + checkMenuView(); + } + } + + private void registerHotKeys() { + getItems().stream() + .filter(item -> item.getHotKey() != null) + .forEach(item -> { + registerHotKeyBinding(item.getHotKey(), () -> selectItem(item)); + }); } /** @@ -265,6 +293,7 @@ public class MenuBarView extends BoxView { private String title; private List items; + private Integer hotKey; public MenuBarItem(String title) { this(title, null); @@ -286,6 +315,15 @@ public class MenuBarView extends BoxView { public List getItems() { return items; } + + public Integer getHotKey() { + return hotKey; + } + + public MenuBarItem setHotKey(Integer hotKey) { + this.hotKey = hotKey; + return this; + } } } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/View.java b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/View.java index a4fb14fc..1e329ff2 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/View.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/View.java @@ -60,13 +60,23 @@ public interface View extends Control { MouseHandler getMouseHandler(); /** - * Gets a {@link View} mouse {@link KeyHandler}. Can be {@code null} which + * Gets a {@link View} key {@link KeyHandler}. Can be {@code null} which * indicates view will not handle any key events. * - * @return a view mouse handler + * @return a view key handler * @see KeyHandler */ @Nullable KeyHandler getKeyHandler(); + /** + * Gets a {@link View} hotkey {@link KeyHandler}. Can be {@code null} which + * indicates view will not handle any key events. + * + * @return a view hotkey handler + * @see KeyHandler + */ + @Nullable + KeyHandler getHotKeyHandler(); + } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/view/event/KeyBinder.java b/spring-shell-core/src/main/java/org/springframework/shell/component/view/event/KeyBinder.java index 6b32fc2f..019aa996 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/view/event/KeyBinder.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/view/event/KeyBinder.java @@ -45,6 +45,10 @@ public class KeyBinder { keyMap.bind(KeyEvent.Key.Char, Character.toString(i)); } + for (char i = KeyEvent.Key.a; i <= KeyEvent.Key.z; i++) { + keyMap.bind(i | KeyEvent.KeyMask.AltMask, alt(i)); + } + keyMap.bind(KeyEvent.Key.q | KeyEvent.KeyMask.CtrlMask, ctrl('q')); keyMap.bind(KeyEvent.Key.Mouse, key(terminal, Capability.key_mouse)); diff --git a/spring-shell-core/src/test/java/org/springframework/shell/component/view/control/MenuBarViewTests.java b/spring-shell-core/src/test/java/org/springframework/shell/component/view/control/MenuBarViewTests.java index d51c490b..4b78a98f 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/component/view/control/MenuBarViewTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/component/view/control/MenuBarViewTests.java @@ -44,6 +44,18 @@ class MenuBarViewTests extends AbstractViewTests { assertThat(view.getItems()).hasSize(1); } + @Test + void hotkeys() { + MenuBarItem item; + + item = MenuBarItem.of("title"); + assertThat(item.getHotKey()).isNull(); + item.setHotKey(Key.f); + assertThat(item.getHotKey()).isEqualTo(Key.f); + + item = MenuBarItem.of("title").setHotKey(Key.f); + assertThat(item.getHotKey()).isEqualTo(Key.f); + } } @Nested diff --git a/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/Catalog.java b/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/Catalog.java index 28fca330..57f9173f 100644 --- a/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/Catalog.java +++ b/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/Catalog.java @@ -46,6 +46,7 @@ import org.springframework.shell.component.view.control.cell.ListCell; import org.springframework.shell.component.view.event.EventLoop; import org.springframework.shell.component.view.event.KeyEvent; import org.springframework.shell.component.view.event.KeyEvent.Key; +import org.springframework.shell.component.view.event.KeyEvent.KeyMask; import org.springframework.shell.component.view.geom.Rectangle; import org.springframework.shell.component.view.message.ShellMessageBuilder; import org.springframework.shell.component.view.screen.Screen; @@ -281,11 +282,13 @@ public class Catalog { .toArray(MenuItem[]::new); MenuBarView menuBar = MenuBarView.of( MenuBarItem.of("File", - MenuItem.of("Quit", MenuItemCheckStyle.NOCHECK, quitAction)), + MenuItem.of("Quit", MenuItemCheckStyle.NOCHECK, quitAction)) + .setHotKey(Key.f | KeyMask.AltMask), MenuBarItem.of("Theme", - themeItems), + themeItems) + .setHotKey(Key.t | KeyMask.AltMask), MenuBarItem.of("Help", - MenuItem.of("About")) + MenuItem.of("About")) ); menuBar.setThemeResolver(themeResolver);