Skip to content

Commit ccf6d77

Browse files
committed
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
1 parent 21d1ce8 commit ccf6d77

File tree

13 files changed

+384
-12
lines changed

13 files changed

+384
-12
lines changed

spring-shell-core/src/main/java/org/springframework/shell/component/view/TerminalUI.java

+1
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ public void configure(View view) {
196196
view.setViewService(getViewService());
197197
}
198198

199+
@Override
199200
public void setFocus(@Nullable View view) {
200201
if (focus != null) {
201202
focus.focus(focus, false);

spring-shell-core/src/main/java/org/springframework/shell/component/view/control/AbstractView.java

+5
Original file line numberDiff line numberDiff line change
@@ -164,13 +164,18 @@ public MouseHandler getMouseHandler() {
164164
int mouse = event.mouse();
165165
View view = null;
166166
boolean consumed = false;
167+
// mouse binding may consume and focus
167168
MouseBindingValue mouseBindingValue = getMouseBindings().get(mouse);
168169
if (mouseBindingValue != null) {
169170
if (mouseBindingValue.mousePredicate().test(event)) {
170171
view = this;
171172
consumed = dispatchMouseRunCommand(event, mouseBindingValue);
172173
}
173174
}
175+
// click in bounds focuses
176+
if (view == null && getRect().contains(event.x(), event.y())) {
177+
view = this;
178+
}
174179
return MouseHandler.resultOf(args.event(), consumed, view, this);
175180
};
176181
return handler;

spring-shell-core/src/main/java/org/springframework/shell/component/view/control/GridView.java

+38-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.slf4j.Logger;
2525
import org.slf4j.LoggerFactory;
2626

27+
import org.springframework.shell.component.view.event.KeyEvent.Key;
2728
import org.springframework.shell.component.view.event.KeyHandler;
2829
import org.springframework.shell.component.view.event.MouseHandler;
2930
import org.springframework.shell.component.view.event.MouseHandler.MouseHandlerResult;
@@ -214,17 +215,53 @@ public MouseHandler getMouseHandler() {
214215
};
215216
}
216217

218+
private void nextView() {
219+
View toFocus = null;
220+
boolean found = false;
221+
for (GridItem i : gridItems) {
222+
if (!i.visible) {
223+
continue;
224+
}
225+
if (toFocus == null) {
226+
toFocus = i.view;
227+
}
228+
if (found) {
229+
toFocus = i.view;
230+
break;
231+
}
232+
if (i.view.hasFocus()) {
233+
found = true;
234+
}
235+
}
236+
if (toFocus != null) {
237+
getViewService().setFocus(toFocus);
238+
}
239+
}
240+
241+
@Override
242+
protected void initInternal() {
243+
registerViewCommand(ViewCommand.NEXT_VIEW, () -> nextView());
244+
245+
registerKeyBinding(Key.Tab, ViewCommand.NEXT_VIEW);
246+
}
247+
217248
@Override
218249
public KeyHandler getKeyHandler() {
219250
log.trace("getKeyHandler()");
251+
KeyHandler handler = null;
220252
for (GridItem i : gridItems) {
221253
if (i.view.hasFocus()) {
222-
return i.view.getKeyHandler();
254+
handler = i.view.getKeyHandler();
255+
break;
223256
}
224257
}
258+
if (handler != null) {
259+
return handler.thenIfNotConsumed(super.getKeyHandler());
260+
}
225261
return super.getKeyHandler();
226262
}
227263

264+
228265
@Override
229266
public boolean hasFocus() {
230267
for (GridItem i : gridItems) {

spring-shell-core/src/main/java/org/springframework/shell/component/view/control/InputView.java

+50-10
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
import java.util.ArrayList;
1919
import java.util.stream.Collectors;
2020

21+
import org.slf4j.Logger;
22+
import org.slf4j.LoggerFactory;
23+
2124
import org.springframework.shell.component.message.ShellMessageBuilder;
2225
import org.springframework.shell.component.view.event.KeyEvent;
2326
import org.springframework.shell.component.view.event.KeyEvent.Key;
@@ -33,20 +36,28 @@
3336
*/
3437
public class InputView extends BoxView {
3538

39+
private final Logger log = LoggerFactory.getLogger(InputView.class);
3640
private final ArrayList<String> text = new ArrayList<>();
3741
private int cursorIndex = 0;
3842

3943
@Override
4044
protected void initInternal() {
41-
registerKeyBinding(Key.CursorLeft, event -> left());
42-
registerKeyBinding(Key.CursorRight, event -> right());
43-
registerKeyBinding(Key.Delete, () -> delete());
44-
registerKeyBinding(Key.Backspace, () -> backspace());
45-
registerKeyBinding(Key.Enter, () -> done());
45+
registerViewCommand(ViewCommand.ACCEPT, () -> done());
46+
registerViewCommand(ViewCommand.LEFT, () -> left());
47+
registerViewCommand(ViewCommand.RIGHT, () -> right());
48+
registerViewCommand(ViewCommand.DELETE_CHAR_LEFT, () -> deleteCharLeft());
49+
registerViewCommand(ViewCommand.DELETE_CHAR_RIGHT, () -> deleteCharRight());
50+
51+
registerKeyBinding(Key.Enter, ViewCommand.ACCEPT);
52+
registerKeyBinding(Key.CursorLeft, ViewCommand.LEFT);
53+
registerKeyBinding(Key.CursorRight, ViewCommand.RIGHT);
54+
registerKeyBinding(Key.Backspace, ViewCommand.DELETE_CHAR_LEFT);
55+
registerKeyBinding(Key.Delete, ViewCommand.DELETE_CHAR_RIGHT);
4656
}
4757

4858
@Override
4959
public KeyHandler getKeyHandler() {
60+
log.trace("getKeyHandler()");
5061
KeyHandler handler = args -> {
5162
KeyEvent event = args.event();
5263
boolean consumed = false;
@@ -68,9 +79,11 @@ protected void drawInternal(Screen screen) {
6879
Rectangle rect = getInnerRect();
6980
String s = getInputText();
7081
screen.writerBuilder().build().text(s, rect.x(), rect.y());
71-
screen.setShowCursor(hasFocus());
72-
int cPos = cursorPosition();
73-
screen.setCursorPosition(new Position(rect.x() + cPos, rect.y()));
82+
if (hasFocus()) {
83+
screen.setShowCursor(true);
84+
int cPos = cursorPosition();
85+
screen.setCursorPosition(new Position(rect.x() + cPos, rect.y()));
86+
}
7487
super.drawInternal(screen);
7588
}
7689

@@ -87,21 +100,34 @@ private int cursorPosition() {
87100
return text.stream().limit(cursorIndex).mapToInt(text -> text.length()).sum();
88101
}
89102

103+
private void dispatchTextChange(String oldText, String newText) {
104+
dispatch(ShellMessageBuilder.ofView(this, InputViewTextChangeEvent.of(this, oldText, newText)));
105+
}
106+
90107
private void add(String data) {
108+
String oldText = text.stream().collect(Collectors.joining());
91109
text.add(cursorIndex, data);
92110
moveCursor(1);
111+
String newText = text.stream().collect(Collectors.joining());
112+
dispatchTextChange(oldText, newText);
93113
}
94114

95-
private void backspace() {
115+
private void deleteCharLeft() {
96116
if (cursorIndex > 0) {
117+
String oldText = text.stream().collect(Collectors.joining());
97118
text.remove(cursorIndex - 1);
119+
String newText = text.stream().collect(Collectors.joining());
120+
dispatchTextChange(oldText, newText);
98121
}
99122
left();
100123
}
101124

102-
private void delete() {
125+
private void deleteCharRight() {
103126
if (cursorIndex < text.size()) {
127+
String oldText = text.stream().collect(Collectors.joining());
104128
text.remove(cursorIndex);
129+
String newText = text.stream().collect(Collectors.joining());
130+
dispatchTextChange(oldText, newText);
105131
}
106132
}
107133

@@ -124,4 +150,18 @@ private void done() {
124150
dispatch(ShellMessageBuilder.ofView(this, ViewDoneEvent.of(this)));
125151
}
126152

153+
public record InputViewTextChangeEventArgs(String oldText, String newText) implements ViewEventArgs {
154+
155+
public static InputViewTextChangeEventArgs of(String oldText, String newText) {
156+
return new InputViewTextChangeEventArgs(oldText, newText);
157+
}
158+
}
159+
160+
public record InputViewTextChangeEvent(View view, InputViewTextChangeEventArgs args) implements ViewEvent {
161+
162+
public static InputViewTextChangeEvent of(View view, String oldText, String newText) {
163+
return new InputViewTextChangeEvent(view, InputViewTextChangeEventArgs.of(oldText, newText));
164+
}
165+
}
166+
127167
}

spring-shell-core/src/main/java/org/springframework/shell/component/view/control/ViewCommand.java

+32
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,36 @@ public final class ViewCommand {
3939
*/
4040
public static String LINE_DOWN = "LineDown";
4141

42+
/**
43+
* Move focus to the next view.
44+
*
45+
* For example using tab to navigate into next input field.
46+
*/
47+
public static String NEXT_VIEW = "NextView";
48+
49+
/**
50+
* Accepts a current state.
51+
*/
52+
public static String ACCEPT = "Accept";
53+
54+
/**
55+
* Deletes the character on the left.
56+
*/
57+
public static String DELETE_CHAR_LEFT = "DeleteCharLeft";
58+
59+
/**
60+
* Deletes the character on the right.
61+
*/
62+
public static String DELETE_CHAR_RIGHT = "DeleteCharRight";
63+
64+
/**
65+
* Moves the selection left by one.
66+
*/
67+
public static String LEFT = "Left";
68+
69+
/**
70+
* Moves the selection righ by one.
71+
*/
72+
public static String RIGHT = "Right";
73+
4274
}

spring-shell-core/src/main/java/org/springframework/shell/component/view/control/ViewService.java

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package org.springframework.shell.component.view.control;
1717

18+
import org.springframework.lang.Nullable;
19+
1820
/**
1921
* Provides services for a {@link View} like handling modals.
2022
*
@@ -26,4 +28,5 @@ public interface ViewService {
2628

2729
void setModal(View view);
2830

31+
void setFocus(@Nullable View view);
2932
}

spring-shell-core/src/test/java/org/springframework/shell/component/view/control/AbstractViewTests.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ protected MouseEvent mouseClick(int x, int y) {
119119
}
120120

121121
protected MouseHandlerResult handleMouseClick(View view, int x, int y) {
122-
MouseEvent click = mouseClick(0, 2);
122+
MouseEvent click = mouseClick(x, y);
123123
return handleMouseClick(view, click);
124124
}
125125

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.shell.component.view.control;
17+
18+
import org.junit.jupiter.api.BeforeEach;
19+
import org.junit.jupiter.api.Nested;
20+
import org.junit.jupiter.api.Test;
21+
22+
import org.springframework.shell.component.view.event.MouseHandler.MouseHandlerResult;
23+
import org.springframework.shell.component.view.screen.Screen;
24+
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
27+
class BaseViewTests extends AbstractViewTests {
28+
29+
TestView view;
30+
31+
@Nested
32+
class Mouse {
33+
34+
@BeforeEach
35+
void setup() {
36+
view = new TestView();
37+
configure(view);
38+
}
39+
40+
@Test
41+
void clickInBounds() {
42+
view.setRect(0, 0, 80, 24);
43+
MouseHandlerResult result = handleMouseClick(view, 0, 0);
44+
assertThat(result).isNotNull().satisfies(r -> {
45+
assertThat(r.consumed()).isFalse();
46+
assertThat(r.focus()).isEqualTo(view);
47+
assertThat(r.capture()).isEqualTo(view);
48+
});
49+
}
50+
51+
@Test
52+
void clickOutOfBounds() {
53+
view.setRect(0, 0, 80, 24);
54+
MouseHandlerResult result = handleMouseClick(view, 100, 100);
55+
assertThat(result).isNotNull().satisfies(r -> {
56+
assertThat(r.consumed()).isFalse();
57+
assertThat(r.focus()).isNull();
58+
assertThat(r.capture()).isEqualTo(view);
59+
});
60+
}
61+
62+
}
63+
64+
private static class TestView extends AbstractView {
65+
66+
@Override
67+
protected void drawInternal(Screen screen) {
68+
}
69+
70+
}
71+
72+
}

spring-shell-core/src/test/java/org/springframework/shell/component/view/control/InputViewTests.java

+27
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,30 @@ class InputViewTests extends AbstractViewTests {
3737

3838
InputView view;
3939

40+
@Nested
41+
class Visual {
42+
43+
@BeforeEach
44+
void setup() {
45+
view = new InputView();
46+
configure(view);
47+
}
48+
49+
@Test
50+
void haveOneRow() {
51+
view.setShowBorder(false);
52+
view.setRect(0, 0, 80, 1);
53+
view.focus(view, true);
54+
55+
dispatchEvent(view, KeyEvent.of('1'));
56+
view.draw(screen24x80);
57+
58+
assertThat(forScreen(screen24x80)).hasHorizontalText("1", 0, 0, 1);
59+
assertThat(forScreen(screen24x80)).hasCursorInPosition(1, 0);
60+
}
61+
62+
}
63+
4064
@Nested
4165
class Input {
4266

@@ -50,6 +74,7 @@ void setup() {
5074
void shouldShowPlainText() {
5175
view.setShowBorder(true);
5276
view.setRect(0, 0, 80, 24);
77+
view.focus(view, true);
5378

5479
dispatchEvent(view, KeyEvent.of('1'));
5580
view.draw(screen24x80);
@@ -62,6 +87,7 @@ void shouldShowPlainText() {
6287
void shouldShowUnicode() {
6388
view.setShowBorder(true);
6489
view.setRect(0, 0, 80, 24);
90+
view.focus(view, true);
6591

6692
dispatchEvent(view, KeyEvent.of('★'));
6793
view.draw(screen24x80);
@@ -74,6 +100,7 @@ void shouldShowUnicode() {
74100
void shouldShowUnicodeEmoji() {
75101
view.setShowBorder(true);
76102
view.setRect(0, 0, 80, 24);
103+
view.focus(view, true);
77104

78105
dispatchEvent(view, KeyEvent.of("😂"));
79106
view.draw(screen24x80);

0 commit comments

Comments
 (0)