From a3ca69bc7261a82391d5051ecea8f43e5369d9a9 Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Wed, 30 Aug 2023 08:38:46 +0300 Subject: [PATCH] Non-fullscreen view - ViewComponent can take view and drive it as non-fullscreen - ViewDoneEvent which InputView now uses - ComponentUiCommands is a sample where we add ideas for views in flow components - Allow View to set eventloop - Relates #850 --- .../shell/component/ViewComponent.java | 76 +++++++++++++++++++ .../component/view/control/InputView.java | 7 ++ .../shell/component/view/control/View.java | 8 ++ .../component/view/control/ViewDoneEvent.java | 38 ++++++++++ .../view/control/InputViewTests.java | 44 +++++++++-- .../samples/standard/ComponentUiCommands.java | 36 +++++++++ 6 files changed, 201 insertions(+), 8 deletions(-) create mode 100644 spring-shell-core/src/main/java/org/springframework/shell/component/ViewComponent.java create mode 100644 spring-shell-core/src/main/java/org/springframework/shell/component/view/control/ViewDoneEvent.java create mode 100644 spring-shell-samples/spring-shell-sample-commands/src/main/java/org/springframework/shell/samples/standard/ComponentUiCommands.java diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/ViewComponent.java b/spring-shell-core/src/main/java/org/springframework/shell/component/ViewComponent.java new file mode 100644 index 00000000..0d729705 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/ViewComponent.java @@ -0,0 +1,76 @@ +/* + * 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.component; + +import org.jline.terminal.Terminal; + +import org.springframework.messaging.Message; +import org.springframework.shell.component.view.TerminalUI; +import org.springframework.shell.component.view.control.View; +import org.springframework.shell.component.view.control.ViewDoneEvent; +import org.springframework.shell.component.view.event.EventLoop; +import org.springframework.shell.component.view.message.ShellMessageBuilder; +import org.springframework.util.Assert; + +/** + * Handles view execution in a non-fullscreen setup. + * + * @author Janne Valkealahti + */ +public class ViewComponent { + + private final Terminal terminal; + private final View view; + private EventLoop eventLoop; + + public ViewComponent(Terminal terminal, View view) { + Assert.notNull(terminal, "terminal must be set"); + Assert.notNull(view, "view must be set"); + this.terminal = terminal; + this.view = view; + } + + /** + * Run a view execution loop. + */ + public void run() { + TerminalUI ui = new TerminalUI(terminal); + eventLoop = ui.getEventLoop(); + eventLoop.onDestroy(eventLoop.viewEvents(ViewDoneEvent.class, view) + .subscribe(event -> { + exit(); + } + )); + view.setEventLoop(eventLoop); + ui.setRoot(view, false); + ui.run(); + } + + /** + * Request exit from an execution loop. + */ + public void exit() { + if (eventLoop == null) { + return; + } + Message msg = ShellMessageBuilder.withPayload("int") + .setEventType(EventLoop.Type.SYSTEM) + .setPriority(0) + .build(); + eventLoop.dispatch(msg); + } + +} diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/InputView.java b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/InputView.java index a15a7dbd..de2cd152 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/InputView.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/InputView.java @@ -23,6 +23,7 @@ import org.springframework.shell.component.view.event.KeyEvent.Key; import org.springframework.shell.component.view.event.KeyHandler; import org.springframework.shell.component.view.geom.Position; import org.springframework.shell.component.view.geom.Rectangle; +import org.springframework.shell.component.view.message.ShellMessageBuilder; import org.springframework.shell.component.view.screen.Screen; /** @@ -41,6 +42,7 @@ public class InputView extends BoxView { registerKeyBinding(Key.CursorRight, event -> right()); registerKeyBinding(Key.Delete, () -> delete()); registerKeyBinding(Key.Backspace, () -> backspace()); + registerKeyBinding(Key.Enter, () -> done()); } @Override @@ -117,4 +119,9 @@ public class InputView extends BoxView { private void right() { moveCursor(1); } + + private void done() { + dispatch(ShellMessageBuilder.ofView(this, ViewDoneEvent.of(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 1e329ff2..412b5f15 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 @@ -16,6 +16,7 @@ package org.springframework.shell.component.view.control; import org.springframework.lang.Nullable; +import org.springframework.shell.component.view.event.EventLoop; import org.springframework.shell.component.view.event.KeyHandler; import org.springframework.shell.component.view.event.MouseHandler; @@ -79,4 +80,11 @@ public interface View extends Control { @Nullable KeyHandler getHotKeyHandler(); + /** + * Sets an {@link EventLoop}. + * + * @param eventLoop the event loop + */ + void setEventLoop(@Nullable EventLoop eventLoop); + } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/ViewDoneEvent.java b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/ViewDoneEvent.java new file mode 100644 index 00000000..834339a7 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/ViewDoneEvent.java @@ -0,0 +1,38 @@ +/* + * 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.component.view.control; + +/** + * Spesialisation of a {@link ViewEvent} indicating that a {@link View} is done. + * + * @author Janne Valkealahti + */ +public interface ViewDoneEvent extends ViewEvent { + + /** + * Create a generic event with empty view args. + * + * @param view the view + * @return a generic view done event + */ + static ViewDoneEvent of(View view) { + return new GenericViewDoneEvent(view, ViewEventArgs.EMPTY); + } + + record GenericViewDoneEvent(View view, ViewEventArgs args) implements ViewDoneEvent { + } + +} diff --git a/spring-shell-core/src/test/java/org/springframework/shell/component/view/control/InputViewTests.java b/spring-shell-core/src/test/java/org/springframework/shell/component/view/control/InputViewTests.java index 97cdb99f..e30d8d3e 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/component/view/control/InputViewTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/component/view/control/InputViewTests.java @@ -15,12 +15,17 @@ */ package org.springframework.shell.component.view.control; +import java.time.Duration; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; import org.springframework.shell.component.view.event.KeyEvent; import org.springframework.shell.component.view.event.KeyEvent.Key; +import org.springframework.shell.component.view.event.KeyHandler.KeyHandlerResult; import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -30,11 +35,11 @@ class InputViewTests extends AbstractViewTests { private static final String CURSOR_INDEX_FIELD = "cursorIndex"; private static final String CURSOR_POSITION_METHOD = "cursorPosition"; + InputView view; + @Nested class Input { - InputView view; - @BeforeEach void setup() { view = new InputView(); @@ -82,8 +87,6 @@ class InputViewTests extends AbstractViewTests { @Nested class CursorPositions { - InputView view; - @BeforeEach void setup() { view = new InputView(); @@ -128,8 +131,6 @@ class InputViewTests extends AbstractViewTests { @Nested class MoveAndDeletions { - InputView view; - @BeforeEach void setup() { view = new InputView(); @@ -157,8 +158,6 @@ class InputViewTests extends AbstractViewTests { @Nested class MoveAndMods { - InputView view; - @BeforeEach void setup() { view = new InputView(); @@ -183,4 +182,33 @@ class InputViewTests extends AbstractViewTests { } + @Nested + class Events { + + @BeforeEach + void setup() { + view = new InputView(); + view.setRect(0, 0, 10, 1); + configure(view); + } + + @Test + void handlesKeyEnter() { + Flux actions = eventLoop + .viewEvents(ViewDoneEvent.class); + StepVerifier verifier = StepVerifier.create(actions) + .expectNextCount(1) + .thenCancel() + .verifyLater(); + + KeyHandlerResult result = handleKey(view, KeyEvent.Key.Enter); + + assertThat(result).isNotNull().satisfies(r -> { + assertThat(r.consumed()).isTrue(); + }); + verifier.verify(Duration.ofSeconds(1)); + } + + } + } diff --git a/spring-shell-samples/spring-shell-sample-commands/src/main/java/org/springframework/shell/samples/standard/ComponentUiCommands.java b/spring-shell-samples/spring-shell-sample-commands/src/main/java/org/springframework/shell/samples/standard/ComponentUiCommands.java new file mode 100644 index 00000000..f96007ea --- /dev/null +++ b/spring-shell-samples/spring-shell-sample-commands/src/main/java/org/springframework/shell/samples/standard/ComponentUiCommands.java @@ -0,0 +1,36 @@ +/* + * 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.samples.standard; + +import org.springframework.shell.command.annotation.Command; +import org.springframework.shell.component.ViewComponent; +import org.springframework.shell.component.view.control.InputView; +import org.springframework.shell.standard.AbstractShellComponent; + +@Command +public class ComponentUiCommands extends AbstractShellComponent { + + @Command(command = "componentui string") + public String stringInput() { + InputView view = new InputView(); + view.setRect(0, 0, 10, 1); + ViewComponent component = new ViewComponent(getTerminal(), view); + component.run(); + String input = view.getInputText(); + return String.format("Input was '%s'", input); + } + +}