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
This commit is contained in:
Janne Valkealahti
2023-08-30 08:38:46 +03:00
parent ba691de250
commit a3ca69bc72
6 changed files with 201 additions and 8 deletions

View File

@@ -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<String> msg = ShellMessageBuilder.withPayload("int")
.setEventType(EventLoop.Type.SYSTEM)
.setPriority(0)
.build();
eventLoop.dispatch(msg);
}
}

View File

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

View File

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

View File

@@ -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 {
}
}

View File

@@ -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<ViewDoneEvent> 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));
}
}
}

View File

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