Add multiinput scenario
- InputView using viewcommands and has event for text change. - Experimental way to handle tab navigation in a layout views. - New viewcommands for tab navigation and moving cursor. - Mouse click takes focus in AbstractView if no view command binding. - MultiInputViewScenario now shows tab navigation. - Fixes #917
This commit is contained in:
@@ -196,6 +196,7 @@ public class TerminalUI implements ViewService {
|
||||
view.setViewService(getViewService());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFocus(@Nullable View view) {
|
||||
if (focus != null) {
|
||||
focus.focus(focus, false);
|
||||
|
||||
@@ -164,6 +164,7 @@ public abstract class AbstractView extends AbstractControl implements View {
|
||||
int mouse = event.mouse();
|
||||
View view = null;
|
||||
boolean consumed = false;
|
||||
// mouse binding may consume and focus
|
||||
MouseBindingValue mouseBindingValue = getMouseBindings().get(mouse);
|
||||
if (mouseBindingValue != null) {
|
||||
if (mouseBindingValue.mousePredicate().test(event)) {
|
||||
@@ -171,6 +172,10 @@ public abstract class AbstractView extends AbstractControl implements View {
|
||||
consumed = dispatchMouseRunCommand(event, mouseBindingValue);
|
||||
}
|
||||
}
|
||||
// click in bounds focuses
|
||||
if (view == null && getRect().contains(event.x(), event.y())) {
|
||||
view = this;
|
||||
}
|
||||
return MouseHandler.resultOf(args.event(), consumed, view, this);
|
||||
};
|
||||
return handler;
|
||||
|
||||
@@ -24,6 +24,7 @@ import java.util.Map.Entry;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.shell.component.view.event.KeyEvent.Key;
|
||||
import org.springframework.shell.component.view.event.KeyHandler;
|
||||
import org.springframework.shell.component.view.event.MouseHandler;
|
||||
import org.springframework.shell.component.view.event.MouseHandler.MouseHandlerResult;
|
||||
@@ -214,17 +215,53 @@ public class GridView extends BoxView {
|
||||
};
|
||||
}
|
||||
|
||||
private void nextView() {
|
||||
View toFocus = null;
|
||||
boolean found = false;
|
||||
for (GridItem i : gridItems) {
|
||||
if (!i.visible) {
|
||||
continue;
|
||||
}
|
||||
if (toFocus == null) {
|
||||
toFocus = i.view;
|
||||
}
|
||||
if (found) {
|
||||
toFocus = i.view;
|
||||
break;
|
||||
}
|
||||
if (i.view.hasFocus()) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (toFocus != null) {
|
||||
getViewService().setFocus(toFocus);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initInternal() {
|
||||
registerViewCommand(ViewCommand.NEXT_VIEW, () -> nextView());
|
||||
|
||||
registerKeyBinding(Key.Tab, ViewCommand.NEXT_VIEW);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyHandler getKeyHandler() {
|
||||
log.trace("getKeyHandler()");
|
||||
KeyHandler handler = null;
|
||||
for (GridItem i : gridItems) {
|
||||
if (i.view.hasFocus()) {
|
||||
return i.view.getKeyHandler();
|
||||
handler = i.view.getKeyHandler();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (handler != null) {
|
||||
return handler.thenIfNotConsumed(super.getKeyHandler());
|
||||
}
|
||||
return super.getKeyHandler();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean hasFocus() {
|
||||
for (GridItem i : gridItems) {
|
||||
|
||||
@@ -18,6 +18,9 @@ package org.springframework.shell.component.view.control;
|
||||
import java.util.ArrayList;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.shell.component.message.ShellMessageBuilder;
|
||||
import org.springframework.shell.component.view.event.KeyEvent;
|
||||
import org.springframework.shell.component.view.event.KeyEvent.Key;
|
||||
@@ -33,20 +36,28 @@ import org.springframework.shell.geom.Rectangle;
|
||||
*/
|
||||
public class InputView extends BoxView {
|
||||
|
||||
private final Logger log = LoggerFactory.getLogger(InputView.class);
|
||||
private final ArrayList<String> text = new ArrayList<>();
|
||||
private int cursorIndex = 0;
|
||||
|
||||
@Override
|
||||
protected void initInternal() {
|
||||
registerKeyBinding(Key.CursorLeft, event -> left());
|
||||
registerKeyBinding(Key.CursorRight, event -> right());
|
||||
registerKeyBinding(Key.Delete, () -> delete());
|
||||
registerKeyBinding(Key.Backspace, () -> backspace());
|
||||
registerKeyBinding(Key.Enter, () -> done());
|
||||
registerViewCommand(ViewCommand.ACCEPT, () -> done());
|
||||
registerViewCommand(ViewCommand.LEFT, () -> left());
|
||||
registerViewCommand(ViewCommand.RIGHT, () -> right());
|
||||
registerViewCommand(ViewCommand.DELETE_CHAR_LEFT, () -> deleteCharLeft());
|
||||
registerViewCommand(ViewCommand.DELETE_CHAR_RIGHT, () -> deleteCharRight());
|
||||
|
||||
registerKeyBinding(Key.Enter, ViewCommand.ACCEPT);
|
||||
registerKeyBinding(Key.CursorLeft, ViewCommand.LEFT);
|
||||
registerKeyBinding(Key.CursorRight, ViewCommand.RIGHT);
|
||||
registerKeyBinding(Key.Backspace, ViewCommand.DELETE_CHAR_LEFT);
|
||||
registerKeyBinding(Key.Delete, ViewCommand.DELETE_CHAR_RIGHT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyHandler getKeyHandler() {
|
||||
log.trace("getKeyHandler()");
|
||||
KeyHandler handler = args -> {
|
||||
KeyEvent event = args.event();
|
||||
boolean consumed = false;
|
||||
@@ -68,9 +79,11 @@ public class InputView extends BoxView {
|
||||
Rectangle rect = getInnerRect();
|
||||
String s = getInputText();
|
||||
screen.writerBuilder().build().text(s, rect.x(), rect.y());
|
||||
screen.setShowCursor(hasFocus());
|
||||
int cPos = cursorPosition();
|
||||
screen.setCursorPosition(new Position(rect.x() + cPos, rect.y()));
|
||||
if (hasFocus()) {
|
||||
screen.setShowCursor(true);
|
||||
int cPos = cursorPosition();
|
||||
screen.setCursorPosition(new Position(rect.x() + cPos, rect.y()));
|
||||
}
|
||||
super.drawInternal(screen);
|
||||
}
|
||||
|
||||
@@ -87,21 +100,34 @@ public class InputView extends BoxView {
|
||||
return text.stream().limit(cursorIndex).mapToInt(text -> text.length()).sum();
|
||||
}
|
||||
|
||||
private void add(String data) {
|
||||
text.add(cursorIndex, data);
|
||||
moveCursor(1);
|
||||
private void dispatchTextChange(String oldText, String newText) {
|
||||
dispatch(ShellMessageBuilder.ofView(this, InputViewTextChangeEvent.of(this, oldText, newText)));
|
||||
}
|
||||
|
||||
private void backspace() {
|
||||
private void add(String data) {
|
||||
String oldText = text.stream().collect(Collectors.joining());
|
||||
text.add(cursorIndex, data);
|
||||
moveCursor(1);
|
||||
String newText = text.stream().collect(Collectors.joining());
|
||||
dispatchTextChange(oldText, newText);
|
||||
}
|
||||
|
||||
private void deleteCharLeft() {
|
||||
if (cursorIndex > 0) {
|
||||
String oldText = text.stream().collect(Collectors.joining());
|
||||
text.remove(cursorIndex - 1);
|
||||
String newText = text.stream().collect(Collectors.joining());
|
||||
dispatchTextChange(oldText, newText);
|
||||
}
|
||||
left();
|
||||
}
|
||||
|
||||
private void delete() {
|
||||
private void deleteCharRight() {
|
||||
if (cursorIndex < text.size()) {
|
||||
String oldText = text.stream().collect(Collectors.joining());
|
||||
text.remove(cursorIndex);
|
||||
String newText = text.stream().collect(Collectors.joining());
|
||||
dispatchTextChange(oldText, newText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,4 +150,18 @@ public class InputView extends BoxView {
|
||||
dispatch(ShellMessageBuilder.ofView(this, ViewDoneEvent.of(this)));
|
||||
}
|
||||
|
||||
public record InputViewTextChangeEventArgs(String oldText, String newText) implements ViewEventArgs {
|
||||
|
||||
public static InputViewTextChangeEventArgs of(String oldText, String newText) {
|
||||
return new InputViewTextChangeEventArgs(oldText, newText);
|
||||
}
|
||||
}
|
||||
|
||||
public record InputViewTextChangeEvent(View view, InputViewTextChangeEventArgs args) implements ViewEvent {
|
||||
|
||||
public static InputViewTextChangeEvent of(View view, String oldText, String newText) {
|
||||
return new InputViewTextChangeEvent(view, InputViewTextChangeEventArgs.of(oldText, newText));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -39,4 +39,36 @@ public final class ViewCommand {
|
||||
*/
|
||||
public static String LINE_DOWN = "LineDown";
|
||||
|
||||
/**
|
||||
* Move focus to the next view.
|
||||
*
|
||||
* For example using tab to navigate into next input field.
|
||||
*/
|
||||
public static String NEXT_VIEW = "NextView";
|
||||
|
||||
/**
|
||||
* Accepts a current state.
|
||||
*/
|
||||
public static String ACCEPT = "Accept";
|
||||
|
||||
/**
|
||||
* Deletes the character on the left.
|
||||
*/
|
||||
public static String DELETE_CHAR_LEFT = "DeleteCharLeft";
|
||||
|
||||
/**
|
||||
* Deletes the character on the right.
|
||||
*/
|
||||
public static String DELETE_CHAR_RIGHT = "DeleteCharRight";
|
||||
|
||||
/**
|
||||
* Moves the selection left by one.
|
||||
*/
|
||||
public static String LEFT = "Left";
|
||||
|
||||
/**
|
||||
* Moves the selection righ by one.
|
||||
*/
|
||||
public static String RIGHT = "Right";
|
||||
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package org.springframework.shell.component.view.control;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Provides services for a {@link View} like handling modals.
|
||||
*
|
||||
@@ -26,4 +28,5 @@ public interface ViewService {
|
||||
|
||||
void setModal(View view);
|
||||
|
||||
void setFocus(@Nullable View view);
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ public class AbstractViewTests {
|
||||
}
|
||||
|
||||
protected MouseHandlerResult handleMouseClick(View view, int x, int y) {
|
||||
MouseEvent click = mouseClick(0, 2);
|
||||
MouseEvent click = mouseClick(x, y);
|
||||
return handleMouseClick(view, click);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.shell.component.view.event.MouseHandler.MouseHandlerResult;
|
||||
import org.springframework.shell.component.view.screen.Screen;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class BaseViewTests extends AbstractViewTests {
|
||||
|
||||
TestView view;
|
||||
|
||||
@Nested
|
||||
class Mouse {
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
view = new TestView();
|
||||
configure(view);
|
||||
}
|
||||
|
||||
@Test
|
||||
void clickInBounds() {
|
||||
view.setRect(0, 0, 80, 24);
|
||||
MouseHandlerResult result = handleMouseClick(view, 0, 0);
|
||||
assertThat(result).isNotNull().satisfies(r -> {
|
||||
assertThat(r.consumed()).isFalse();
|
||||
assertThat(r.focus()).isEqualTo(view);
|
||||
assertThat(r.capture()).isEqualTo(view);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void clickOutOfBounds() {
|
||||
view.setRect(0, 0, 80, 24);
|
||||
MouseHandlerResult result = handleMouseClick(view, 100, 100);
|
||||
assertThat(result).isNotNull().satisfies(r -> {
|
||||
assertThat(r.consumed()).isFalse();
|
||||
assertThat(r.focus()).isNull();
|
||||
assertThat(r.capture()).isEqualTo(view);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class TestView extends AbstractView {
|
||||
|
||||
@Override
|
||||
protected void drawInternal(Screen screen) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -37,6 +37,30 @@ class InputViewTests extends AbstractViewTests {
|
||||
|
||||
InputView view;
|
||||
|
||||
@Nested
|
||||
class Visual {
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
view = new InputView();
|
||||
configure(view);
|
||||
}
|
||||
|
||||
@Test
|
||||
void haveOneRow() {
|
||||
view.setShowBorder(false);
|
||||
view.setRect(0, 0, 80, 1);
|
||||
view.focus(view, true);
|
||||
|
||||
dispatchEvent(view, KeyEvent.of('1'));
|
||||
view.draw(screen24x80);
|
||||
|
||||
assertThat(forScreen(screen24x80)).hasHorizontalText("1", 0, 0, 1);
|
||||
assertThat(forScreen(screen24x80)).hasCursorInPosition(1, 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Input {
|
||||
|
||||
@@ -50,6 +74,7 @@ class InputViewTests extends AbstractViewTests {
|
||||
void shouldShowPlainText() {
|
||||
view.setShowBorder(true);
|
||||
view.setRect(0, 0, 80, 24);
|
||||
view.focus(view, true);
|
||||
|
||||
dispatchEvent(view, KeyEvent.of('1'));
|
||||
view.draw(screen24x80);
|
||||
@@ -62,6 +87,7 @@ class InputViewTests extends AbstractViewTests {
|
||||
void shouldShowUnicode() {
|
||||
view.setShowBorder(true);
|
||||
view.setRect(0, 0, 80, 24);
|
||||
view.focus(view, true);
|
||||
|
||||
dispatchEvent(view, KeyEvent.of('★'));
|
||||
view.draw(screen24x80);
|
||||
@@ -74,6 +100,7 @@ class InputViewTests extends AbstractViewTests {
|
||||
void shouldShowUnicodeEmoji() {
|
||||
view.setShowBorder(true);
|
||||
view.setRect(0, 0, 80, 24);
|
||||
view.focus(view, true);
|
||||
|
||||
dispatchEvent(view, KeyEvent.of("😂"));
|
||||
view.draw(screen24x80);
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
*** xref:tui/views/button.adoc[]
|
||||
*** xref:tui/views/dialog.adoc[]
|
||||
*** xref:tui/views/grid.adoc[]
|
||||
*** xref:tui/views/input.adoc[]
|
||||
*** xref:tui/views/list.adoc[]
|
||||
*** xref:tui/views/menu.adoc[]
|
||||
*** xref:tui/views/menubar.adoc[]
|
||||
|
||||
66
spring-shell-docs/modules/ROOT/pages/tui/views/input.adoc
Normal file
66
spring-shell-docs/modules/ROOT/pages/tui/views/input.adoc
Normal file
@@ -0,0 +1,66 @@
|
||||
= InputView
|
||||
:page-section-summary-toc: 1
|
||||
|
||||
ifndef::snippets[:snippets: ../../../../../src/test/java/org/springframework/shell/docs]
|
||||
|
||||
_InputView_ is a base implementation providing functionality to draw and modify
|
||||
text in a bounded _Rectancle_.
|
||||
|
||||
[source, java, indent=0]
|
||||
----
|
||||
include::{snippets}/InputViewSnippets.java[tag=sample]
|
||||
----
|
||||
|
||||
== Default Bindings
|
||||
Default _view commands_ are:
|
||||
|
||||
.ViewCommands
|
||||
|===
|
||||
|Command |Description
|
||||
|
||||
|LEFT
|
||||
|Cursor moves left
|
||||
|
||||
|RIGHT
|
||||
|Cursor moves right
|
||||
|
||||
|DELETE_CHAR_LEFT
|
||||
|Delete character left
|
||||
|
||||
|DELETE_CHAR_RIGHT
|
||||
|Delete character right
|
||||
|
||||
|===
|
||||
|
||||
Default _key bindigs_ are:
|
||||
|
||||
.Key
|
||||
|===
|
||||
|Command |Description
|
||||
|
||||
|CursorLeft
|
||||
|Bound ViewCommand LEFT
|
||||
|
||||
|CursorRight
|
||||
|Bound ViewCommand RIGHT
|
||||
|
||||
|Backspace
|
||||
|Bound ViewCommand DELETE_CHAR_LEFT
|
||||
|
||||
|Delete
|
||||
|Bound ViewCommand DELETE_CHAR_RIGHT
|
||||
|
||||
|===
|
||||
|
||||
== Events
|
||||
|
||||
Events are sent depending on a used list type.
|
||||
|
||||
.InputView Events
|
||||
|===
|
||||
|Event |Description
|
||||
|
||||
|InputViewTextChangeEvent
|
||||
|Input text has changed
|
||||
|
||||
|===
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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 org.springframework.shell.component.view.control.InputView;
|
||||
|
||||
class InputViewSnippets {
|
||||
|
||||
class Dump1 {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
void dump1() {
|
||||
// tag::sample[]
|
||||
InputView input = new InputView();
|
||||
String text = input.getInputText();
|
||||
// end::sample[]
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.catalog.scenario.other;
|
||||
|
||||
import org.springframework.shell.component.view.control.GridView;
|
||||
import org.springframework.shell.component.view.control.InputView;
|
||||
import org.springframework.shell.component.view.control.View;
|
||||
import org.springframework.shell.component.view.screen.Color;
|
||||
import org.springframework.shell.samples.catalog.scenario.AbstractScenario;
|
||||
import org.springframework.shell.samples.catalog.scenario.Scenario;
|
||||
import org.springframework.shell.samples.catalog.scenario.ScenarioComponent;;
|
||||
|
||||
@ScenarioComponent(name = "Multi inputview", description = "Multi InputView sample", category = { Scenario.CATEGORY_OTHER })
|
||||
public class MultiInputViewScenario extends AbstractScenario {
|
||||
|
||||
@Override
|
||||
public View build() {
|
||||
GridView grid = new GridView();
|
||||
configure(grid);
|
||||
grid.setRowSize(1, 1, 1);
|
||||
grid.setColumnSize(0);
|
||||
|
||||
InputView input1 = new InputView();
|
||||
input1.setBackgroundColor(Color.GREEN);
|
||||
configure(input1);
|
||||
|
||||
InputView input2 = new InputView();
|
||||
input2.setBackgroundColor(Color.BLUE);
|
||||
configure(input2);
|
||||
|
||||
InputView input3 = new InputView();
|
||||
input3.setBackgroundColor(Color.RED);
|
||||
configure(input3);
|
||||
|
||||
grid.addItem(input1, 0, 0, 1, 1, 0, 0);
|
||||
grid.addItem(input2, 1, 0, 1, 1, 0, 0);
|
||||
grid.addItem(input3, 2, 0, 1, 1, 0, 0);
|
||||
|
||||
return grid;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user