From bba7a82ab440a68813894aad1bf4737528919b77 Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Mon, 30 Oct 2023 09:31:03 +0000 Subject: [PATCH] Make StatusBarView work with hotkeys - StatusItem can now define a hotkey which is then bound to its action. - In catalog app replace use of raw key event to item's hotkey for status bar visibility. - Various doc updates. - Relates #826 --- .../shell/component/view/control/AppView.java | 5 +- .../component/view/control/StatusBarView.java | 42 ++++++++++++- .../view/control/StatusBarViewTests.java | 16 +++++ spring-shell-docs/modules/ROOT/nav.adoc | 2 + .../modules/ROOT/pages/tui/views/app.adoc | 41 ++++++++++++ .../modules/ROOT/pages/tui/views/menubar.adoc | 7 +++ .../ROOT/pages/tui/views/statusbar.adoc | 48 ++++++++++++++ .../shell/docs/StatusBarViewSnippets.java | 63 +++++++++++++++++++ .../shell/samples/catalog/Catalog.java | 25 ++------ 9 files changed, 227 insertions(+), 22 deletions(-) create mode 100644 spring-shell-docs/modules/ROOT/pages/tui/views/app.adoc create mode 100644 spring-shell-docs/modules/ROOT/pages/tui/views/statusbar.adoc create mode 100644 spring-shell-docs/src/test/java/org/springframework/shell/docs/StatusBarViewSnippets.java 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 611aff0d..26ee8533 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 @@ -135,7 +135,10 @@ public class AppView extends BoxView { @Override public KeyHandler getHotKeyHandler() { - return menu != null ? menu.getHotKeyHandler() : super.getHotKeyHandler(); + KeyHandler mainHandler = main != null ? main.getHotKeyHandler() : super.getHotKeyHandler(); + KeyHandler menuHandler = menu != null ? menu.getHotKeyHandler() : super.getHotKeyHandler(); + KeyHandler statusHandler = status != null ? status.getHotKeyHandler() : super.getHotKeyHandler(); + return mainHandler.thenIfNotConsumed(menuHandler).thenIfNotConsumed(statusHandler); } @Override diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/StatusBarView.java b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/StatusBarView.java index 4e426141..85c73dce 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/StatusBarView.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/StatusBarView.java @@ -121,6 +121,7 @@ public class StatusBarView extends BoxView { public void setItems(List items) { this.items.clear(); this.items.addAll(items); + registerHotKeys(); } /** @@ -132,6 +133,17 @@ public class StatusBarView extends BoxView { return items; } + private void registerHotKeys() { + getItems().stream() + .filter(item -> item.getHotKey() != null) + .forEach(item -> { + Runnable action = item.getAction(); + if (action != null) { + registerHotKeyBinding(item.getHotKey(), action); + } + }); + } + /** * {@link StatusItem} represents an item in a {@link StatusBarView}. */ @@ -139,14 +151,32 @@ public class StatusBarView extends BoxView { private String title; private Runnable action; + private Integer hotKey; public StatusItem(String title) { this(title, null); } public StatusItem(String title, Runnable action) { + this(title, action, null); + } + + public StatusItem(String title, Runnable action, Integer hotKey) { this.title = title; this.action = action; + this.hotKey = hotKey; + } + + public static StatusItem of(String title) { + return new StatusItem(title); + } + + public static StatusItem of(String title, Runnable action) { + return new StatusItem(title, action); + } + + public static StatusItem of(String title, Runnable action, Integer hotKey) { + return new StatusItem(title, action, hotKey); } public String getTitle() { @@ -157,8 +187,18 @@ public class StatusBarView extends BoxView { return action; } - public void setAction(Runnable action) { + public StatusItem setAction(Runnable action) { this.action = action; + return this; + } + + public Integer getHotKey() { + return hotKey; + } + + public StatusItem setHotKey(Integer hotKey) { + this.hotKey = hotKey; + return this; } } diff --git a/spring-shell-core/src/test/java/org/springframework/shell/component/view/control/StatusBarViewTests.java b/spring-shell-core/src/test/java/org/springframework/shell/component/view/control/StatusBarViewTests.java index 8dd74588..1f7f4e5b 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/component/view/control/StatusBarViewTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/component/view/control/StatusBarViewTests.java @@ -26,6 +26,7 @@ import reactor.test.StepVerifier; import org.springframework.shell.component.view.control.StatusBarView.StatusBarViewOpenSelectedItemEvent; import org.springframework.shell.component.view.control.StatusBarView.StatusItem; +import org.springframework.shell.component.view.event.KeyEvent.Key; import org.springframework.shell.component.view.event.MouseEvent; import org.springframework.shell.component.view.event.MouseHandler.MouseHandlerResult; import org.springframework.test.util.ReflectionTestUtils; @@ -51,8 +52,23 @@ class StatusBarViewTests extends AbstractViewTests { view = new StatusBarView(Arrays.asList(new StatusItem("item1"))); assertThat(view.getItems()).hasSize(1); + + view = new StatusBarView(Arrays.asList(StatusItem.of("item1"))); + assertThat(view.getItems()).hasSize(1); } + @Test + void hotkeys() { + StatusItem item; + + item = StatusItem.of("title"); + assertThat(item.getHotKey()).isNull(); + item.setHotKey(Key.f); + assertThat(item.getHotKey()).isEqualTo(Key.f); + + item = StatusItem.of("title").setHotKey(Key.f); + assertThat(item.getHotKey()).isEqualTo(Key.f); + } } @Nested diff --git a/spring-shell-docs/modules/ROOT/nav.adoc b/spring-shell-docs/modules/ROOT/nav.adoc index 6aa88cd5..cf4088aa 100644 --- a/spring-shell-docs/modules/ROOT/nav.adoc +++ b/spring-shell-docs/modules/ROOT/nav.adoc @@ -56,6 +56,7 @@ ** xref:tui/intro/index.adoc[] *** xref:tui/intro/terminalui.adoc[] ** xref:tui/views/index.adoc[] +*** xref:tui/views/app.adoc[] *** xref:tui/views/box.adoc[] *** xref:tui/views/button.adoc[] *** xref:tui/views/dialog.adoc[] @@ -63,6 +64,7 @@ *** xref:tui/views/list.adoc[] *** xref:tui/views/menu.adoc[] *** xref:tui/views/menubar.adoc[] +*** xref:tui/views/statusbar.adoc[] ** xref:tui/events/index.adoc[] *** xref:tui/events/eventloop.adoc[] *** xref:tui/events/key.adoc[] diff --git a/spring-shell-docs/modules/ROOT/pages/tui/views/app.adoc b/spring-shell-docs/modules/ROOT/pages/tui/views/app.adoc new file mode 100644 index 00000000..91e6811c --- /dev/null +++ b/spring-shell-docs/modules/ROOT/pages/tui/views/app.adoc @@ -0,0 +1,41 @@ += AppView +:page-section-summary-toc: 1 + +ifndef::snippets[:snippets: ../../../../../src/test/java/org/springframework/shell/docs] + +_AppView_ is a base implementation providing functionality to draw opinionated _application view_. +Inherits xref:tui/views/box.adoc[]. + +Generic idea is to have menu and status views which typically are xref:tui/views/menubar.adoc[] and +xref:tui/views/statusbar.adoc[] respectively. Main content view is then whatever user want to show +in it. + +[source, text] +---- +┌──────────────────────────┐ +│ Menu │ +├──────────────────────────┤ +│ │ +│ Main │ +│ │ +├──────────────────────────┤ +│ Status │ +└──────────────────────────┘ +---- + +== Key Handling +If menu has a focus key handling is processed there, then main is consulted for handling. +Lastly cursor left/right are processed to dispatch _AppViewEvent_. + +== HotKey Handling +Hotkeys are processed in order of _main_, _menu_ and _status_. + +== Events +.AppView Events +|=== +|Event |Description + +|AppViewEvent +|Direction for a next selection. + +|=== diff --git a/spring-shell-docs/modules/ROOT/pages/tui/views/menubar.adoc b/spring-shell-docs/modules/ROOT/pages/tui/views/menubar.adoc index 1bc6be88..e9403146 100644 --- a/spring-shell-docs/modules/ROOT/pages/tui/views/menubar.adoc +++ b/spring-shell-docs/modules/ROOT/pages/tui/views/menubar.adoc @@ -7,6 +7,13 @@ ifndef::snippets[:snippets: ../../../../../src/test/java/org/springframework/she _MenuBarView_ is a base implementation providing functionality to draw a menu bar. Inherits xref:tui/views/box.adoc[]. +[source, text] +---- +┌─────────────────────────────┐ +│ File Help │ +└─────────────────────────────┘ +---- + == Default Bindings Default _key bindigs_ are: diff --git a/spring-shell-docs/modules/ROOT/pages/tui/views/statusbar.adoc b/spring-shell-docs/modules/ROOT/pages/tui/views/statusbar.adoc new file mode 100644 index 00000000..bee090bb --- /dev/null +++ b/spring-shell-docs/modules/ROOT/pages/tui/views/statusbar.adoc @@ -0,0 +1,48 @@ += StatusBarView +:page-section-summary-toc: 1 + +ifndef::snippets[:snippets: ../../../../../src/test/java/org/springframework/shell/docs] + +_StatusBarView_ is a base implementation providing functionality to draw a status bar. +Inherits xref:tui/views/box.adoc[]. + +[source, text] +---- +┌─────────────────────────────┐ +│ Item1 | Item2 | Item3 │ +└─────────────────────────────┘ +---- + +You can create a simple status bar with an item: + +[source, java, indent=0] +---- +include::{snippets}/StatusBarViewSnippets.java[tag=simple] +---- + +Constructor can take array form which allows to lay out simple +item definitions in a _dsl_ style. + +[source, java, indent=0] +---- +include::{snippets}/StatusBarViewSnippets.java[tag=viaarray] +---- + +Items support runnable actions which generally as executed when +item is selected. It can also get attached to a hot key. + +[source, java, indent=0] +---- +include::{snippets}/StatusBarViewSnippets.java[tag=items] +---- + + +== Events +.StatusBarView Events +|=== +|Event |Description + +|StatusBarViewOpenSelectedItemEvent +|StatusItem is selected. + +|=== diff --git a/spring-shell-docs/src/test/java/org/springframework/shell/docs/StatusBarViewSnippets.java b/spring-shell-docs/src/test/java/org/springframework/shell/docs/StatusBarViewSnippets.java new file mode 100644 index 00000000..f64ec235 --- /dev/null +++ b/spring-shell-docs/src/test/java/org/springframework/shell/docs/StatusBarViewSnippets.java @@ -0,0 +1,63 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.shell.docs; + +import java.util.List; + +import org.springframework.shell.component.view.control.StatusBarView; +import org.springframework.shell.component.view.control.StatusBarView.StatusItem; +import org.springframework.shell.component.view.event.KeyEvent; +import org.springframework.shell.component.view.event.KeyEvent.Key; + +class StatusBarViewSnippets { + + @SuppressWarnings("unused") + void simple() { + // tag::simple[] + StatusItem item1 = new StatusBarView.StatusItem("Item1"); + StatusBarView statusBar = new StatusBarView(List.of(item1)); + // end::simple[] + } + + void items() { + // tag::items[] + StatusItem item1 = StatusBarView.StatusItem.of("Item1"); + + Runnable action1 = () -> {}; + StatusItem item2 = StatusBarView.StatusItem.of("Item2", action1); + + Runnable action2 = () -> {}; + StatusItem item3 = StatusBarView.StatusItem.of("Item3", action2, KeyEvent.Key.f10); + + StatusBarView statusBar = new StatusBarView(); + statusBar.setItems(List.of(item1, item2, item3)); + // end::items[] + } + + void viaArray() { + // tag::viaarray[] + new StatusBarView(new StatusItem[] { + StatusItem.of("Item1"), + StatusItem.of("Item2") + .setAction(() -> {}), + StatusItem.of("Item3") + .setAction(() -> {}) + .setHotKey(Key.f10) + }); + // end::viaarray[] + } + +} 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 1c0dd026..f68d1746 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 @@ -16,7 +16,6 @@ package org.springframework.shell.samples.catalog; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -203,21 +202,6 @@ public class Catalog { } )); - // we could potentially do keybinding somewhere else - // but at least this shows how to do it in low level. - // essentially we now just handle F10 to toggle - // menubar visibility - // TODO: when we get support for hotkeys we should do - // binding there - eventLoop.onDestroy(eventLoop.keyEvents() - .subscribe(event -> { - log.debug("Raw keyevent {}", event); - if (event.isKey(KeyEvent.Key.f10)) { - app.toggleStatusBarVisibility(); - } - } - )); - return app; } @@ -322,11 +306,12 @@ public class Catalog { private StatusBarView buildStatusBar(EventLoop eventLoop) { Runnable quitAction = () -> requestQuit(); - StatusBarView statusBar = new StatusBarView(); + Runnable visibilyAction = () -> app.toggleStatusBarVisibility(); + StatusBarView statusBar = new StatusBarView(new StatusItem[] { + StatusItem.of("CTRL-Q Quit", quitAction), + StatusItem.of("F10 Status Bar", visibilyAction, KeyEvent.Key.f10) + }); ui.configure(statusBar); - StatusItem item1 = new StatusBarView.StatusItem("CTRL-Q Quit", quitAction); - StatusItem item2 = new StatusBarView.StatusItem("F10 Status Bar"); - statusBar.setItems(Arrays.asList(item1, item2)); return statusBar; }