Initial terminal ui implementation
- This commit adds proof of concept work for terminal ui as is. - Some things work, some don't but we need to start from somewhere. Further development continues in a main. - Essentially we are starting to have enough so that it merits to move all this work into a main repo. - Everything new is kept under org.springframework.shell.component.view and will get revisiter later to find correct locations for some classes. - Catalog sample has been modified to provide "showcase" app for terminal ui features. This is a start while it already contains some usefull scenarios. - Relates #800 - Relates #801 - Relates #802 - Relates #803 - Relates #804 - Relates #805 - Relates #806 - Relates #807 - Relates #808 - Relates #809 - Relates #810 - Relates #811
This commit is contained in:
7
.vscode/launch.json
vendored
7
.vscode/launch.json
vendored
@@ -94,6 +94,13 @@
|
||||
"mainClass": "org.springframework.shell.samples.SpringShellSample",
|
||||
"projectName": "spring-shell-sample-e2e",
|
||||
"args": "e2e reg exit-code --arg1 fun"
|
||||
},
|
||||
{
|
||||
"type": "java",
|
||||
"name": "catalog",
|
||||
"request": "launch",
|
||||
"mainClass": "org.springframework.shell.samples.catalog.SpringShellApplication",
|
||||
"projectName": "spring-shell-sample-catalog"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -13,5 +13,6 @@
|
||||
"checkstyle.header.file": "https://raw.githubusercontent.com/spring-cloud/spring-cloud-dataflow-build/main/spring-cloud-dataflow-build-tools/src/main/resources/checkstyle-header.txt"
|
||||
},
|
||||
"java.checkstyle.version": "8.29",
|
||||
"java.configuration.maven.notCoveredPluginExecutionSeverity": "ignore"
|
||||
"java.configuration.maven.notCoveredPluginExecutionSeverity": "ignore",
|
||||
"java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx2G -Xms100m -Xlog:disable"
|
||||
}
|
||||
@@ -10,6 +10,7 @@ dependencies {
|
||||
api('org.springframework:spring-core')
|
||||
api('org.springframework.boot:spring-boot-starter-validation')
|
||||
api('org.springframework:spring-messaging')
|
||||
api('io.projectreactor:reactor-core')
|
||||
api('org.jline:jline')
|
||||
api('org.antlr:ST4')
|
||||
api('commons-io:commons-io')
|
||||
@@ -17,4 +18,5 @@ dependencies {
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
testImplementation 'org.awaitility:awaitility'
|
||||
testImplementation 'com.google.jimfs:jimfs'
|
||||
testImplementation 'io.projectreactor:reactor-test'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.io.IOError;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.jline.keymap.BindingReader;
|
||||
import org.jline.keymap.KeyMap;
|
||||
import org.jline.terminal.Attributes;
|
||||
import org.jline.terminal.Size;
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.terminal.Terminal.Signal;
|
||||
import org.jline.utils.AttributedString;
|
||||
import org.jline.utils.Display;
|
||||
import org.jline.utils.InfoCmp.Capability;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.shell.component.view.control.View;
|
||||
import org.springframework.shell.component.view.event.DefaultEventLoop;
|
||||
import org.springframework.shell.component.view.event.EventLoop;
|
||||
import org.springframework.shell.component.view.event.KeyBinder;
|
||||
import org.springframework.shell.component.view.event.KeyEvent;
|
||||
import org.springframework.shell.component.view.event.KeyHandler;
|
||||
import org.springframework.shell.component.view.event.MouseEvent;
|
||||
import org.springframework.shell.component.view.event.KeyHandler.KeyHandlerResult;
|
||||
import org.springframework.shell.component.view.event.MouseHandler;
|
||||
import org.springframework.shell.component.view.event.MouseHandler.MouseHandlerResult;
|
||||
import org.springframework.shell.component.view.geom.Rectangle;
|
||||
import org.springframework.shell.component.view.message.ShellMessageBuilder;
|
||||
import org.springframework.shell.component.view.message.ShellMessageHeaderAccessor;
|
||||
import org.springframework.shell.component.view.screen.DefaultScreen;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@link TerminalUI} is a main component orchestrating terminal, eventloop,
|
||||
* key/mouse events and view structure to work together. In many ways it can
|
||||
* be think of being a "main application" when terminal ui is shown in
|
||||
* a screen.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class TerminalUI {
|
||||
|
||||
private final static Logger log = LoggerFactory.getLogger(TerminalUI.class);
|
||||
private final Terminal terminal;
|
||||
private final BindingReader bindingReader;
|
||||
private final KeyMap<Integer> keyMap = new KeyMap<>();
|
||||
private final DefaultScreen virtualDisplay = new DefaultScreen();
|
||||
private Display display;
|
||||
private Size size;
|
||||
private View rootView;
|
||||
private boolean fullScreen;
|
||||
private final KeyBinder keyBinder;
|
||||
private DefaultEventLoop eventLoop = new DefaultEventLoop();
|
||||
private View focus = null;
|
||||
|
||||
/**
|
||||
* Constructs a handler with a given terminal.
|
||||
*
|
||||
* @param terminal the terminal
|
||||
*/
|
||||
public TerminalUI(Terminal terminal) {
|
||||
Assert.notNull(terminal, "terminal must be set");
|
||||
this.terminal = terminal;
|
||||
this.bindingReader = new BindingReader(terminal.reader());
|
||||
this.keyBinder = new KeyBinder(terminal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a root view.
|
||||
*
|
||||
* @param root the root view
|
||||
* @param fullScreen if root view should request full screen
|
||||
*/
|
||||
public void setRoot(View root, boolean fullScreen) {
|
||||
setFocus(root);
|
||||
this.rootView = root;
|
||||
this.fullScreen = fullScreen;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run and start execution loop. This method blocks until run loop exits.
|
||||
*/
|
||||
public void run() {
|
||||
bindKeyMap(keyMap);
|
||||
display = new Display(terminal, fullScreen);
|
||||
size = new Size();
|
||||
loop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an {@link EventLoop}.
|
||||
*
|
||||
* @return an event loop
|
||||
*/
|
||||
public EventLoop getEventLoop() {
|
||||
return eventLoop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redraw a whole screen. Essentially a message is dispatched to an event loop
|
||||
* which is handled as soon as possible.
|
||||
*/
|
||||
public void redraw() {
|
||||
getEventLoop().dispatch(ShellMessageBuilder.ofRedraw());
|
||||
}
|
||||
|
||||
public void setFocus(@Nullable View view) {
|
||||
if (focus != null) {
|
||||
focus.focus(focus, false);
|
||||
}
|
||||
focus = view;
|
||||
if (focus != null) {
|
||||
focus.focus(focus, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void render(int rows, int columns) {
|
||||
if (rootView == null) {
|
||||
return;
|
||||
}
|
||||
rootView.setRect(0, 0, columns, rows);
|
||||
rootView.draw(virtualDisplay);
|
||||
}
|
||||
|
||||
private void display() {
|
||||
log.trace("display()");
|
||||
size.copy(terminal.getSize());
|
||||
if (fullScreen) {
|
||||
display.clear();
|
||||
display.resize(size.getRows(), size.getColumns());
|
||||
display.reset();
|
||||
rootView.setRect(0, 0, size.getColumns(), size.getRows());
|
||||
virtualDisplay.resize(size.getRows(), size.getColumns());
|
||||
virtualDisplay.setShowCursor(false);
|
||||
render(size.getRows(), size.getColumns());
|
||||
}
|
||||
else {
|
||||
display.resize(size.getRows(), size.getColumns());
|
||||
Rectangle rect = rootView.getRect();
|
||||
virtualDisplay.resize(rect.height(), rect.width());
|
||||
virtualDisplay.setShowCursor(false);
|
||||
render(rect.height(), rect.width());
|
||||
}
|
||||
|
||||
List<AttributedString> newLines = virtualDisplay.getScreenLines();
|
||||
|
||||
int targetCursorPos = 0;
|
||||
if (virtualDisplay.isShowCursor()) {
|
||||
terminal.puts(Capability.cursor_visible);
|
||||
targetCursorPos = size.cursorPos(virtualDisplay.getCursorPosition().y(), virtualDisplay.getCursorPosition().x());
|
||||
log.debug("Display targetCursorPos {}", targetCursorPos);
|
||||
}
|
||||
else {
|
||||
terminal.puts(Capability.cursor_invisible);
|
||||
}
|
||||
display.update(newLines, targetCursorPos);
|
||||
}
|
||||
|
||||
private void dispatchWinch() {
|
||||
Message<String> message = MessageBuilder.withPayload("WINCH")
|
||||
.setHeader(ShellMessageHeaderAccessor.EVENT_TYPE, EventLoop.Type.SIGNAL)
|
||||
.build();
|
||||
eventLoop.dispatch(message);
|
||||
}
|
||||
|
||||
private void registerEventHandling() {
|
||||
// XXX: think this again
|
||||
eventLoop.onDestroy(eventLoop.events()
|
||||
.filter(m -> {
|
||||
return ObjectUtils.nullSafeEquals(m.getHeaders().get(ShellMessageHeaderAccessor.EVENT_TYPE), EventLoop.Type.SIGNAL);
|
||||
})
|
||||
.doOnNext(m -> {
|
||||
display();
|
||||
})
|
||||
.subscribe());
|
||||
|
||||
// XXX: think this again
|
||||
eventLoop.onDestroy(eventLoop.events()
|
||||
.filter(m -> {
|
||||
return ObjectUtils.nullSafeEquals(m.getHeaders().get(ShellMessageHeaderAccessor.EVENT_TYPE), EventLoop.Type.SYSTEM);
|
||||
})
|
||||
.doOnNext(m -> {
|
||||
Object payload = m.getPayload();
|
||||
if (payload instanceof String s) {
|
||||
if ("redraw".equals(s)) {
|
||||
display();
|
||||
}
|
||||
else if ("int".equals(s)) {
|
||||
this.terminal.raise(Signal.INT);
|
||||
}
|
||||
}
|
||||
})
|
||||
.subscribe());
|
||||
|
||||
eventLoop.onDestroy(eventLoop.keyEvents()
|
||||
.doOnNext(m -> {
|
||||
handleKeyEvent(m);
|
||||
})
|
||||
.subscribe());
|
||||
|
||||
eventLoop.onDestroy(eventLoop.mouseEvents()
|
||||
.doOnNext(m -> {
|
||||
handleMouseEvent(m);
|
||||
})
|
||||
.subscribe());
|
||||
}
|
||||
|
||||
private void handleKeyEvent(KeyEvent event) {
|
||||
log.trace("handleKeyEvent {}", event);
|
||||
if (rootView != null && rootView.hasFocus()) {
|
||||
KeyHandler handler = rootView.getKeyHandler();
|
||||
if (handler != null) {
|
||||
KeyHandlerResult result = handler.handle(KeyHandler.argsOf(event));
|
||||
if (result.focus() != null) {
|
||||
setFocus(result.focus());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMouseEvent(MouseEvent event) {
|
||||
log.trace("handleMouseEvent {}", event);
|
||||
if (rootView != null) {
|
||||
MouseHandler handler = rootView.getMouseHandler();
|
||||
if (handler != null) {
|
||||
MouseHandlerResult result = handler.handle(MouseHandler.argsOf(event));
|
||||
if (result.focus() != null) {
|
||||
setFocus(result.focus());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loop() {
|
||||
Attributes attr = terminal.enterRawMode();
|
||||
registerEventHandling();
|
||||
|
||||
terminal.handle(Signal.WINCH, signal -> {
|
||||
log.debug("Handling signal {}", signal);
|
||||
dispatchWinch();
|
||||
});
|
||||
|
||||
try {
|
||||
if (fullScreen) {
|
||||
terminal.puts(Capability.enter_ca_mode);
|
||||
}
|
||||
terminal.puts(Capability.keypad_xmit);
|
||||
terminal.puts(Capability.cursor_invisible);
|
||||
terminal.trackMouse(Terminal.MouseTracking.Normal);
|
||||
terminal.writer().flush();
|
||||
size.copy(terminal.getSize());
|
||||
display.clear();
|
||||
display.reset();
|
||||
|
||||
while (true) {
|
||||
display();
|
||||
boolean exit = read(bindingReader, keyMap);
|
||||
if (exit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
eventLoop.destroy();
|
||||
terminal.setAttributes(attr);
|
||||
log.debug("Setting cursor visible");
|
||||
terminal.puts(Capability.cursor_visible);
|
||||
if (fullScreen) {
|
||||
display.update(Collections.emptyList(), 0);
|
||||
}
|
||||
terminal.trackMouse(Terminal.MouseTracking.Off);
|
||||
if (fullScreen) {
|
||||
terminal.puts(Capability.exit_ca_mode);
|
||||
}
|
||||
terminal.puts(Capability.keypad_local);
|
||||
if (!fullScreen) {
|
||||
display.update(Collections.emptyList(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void bindKeyMap(KeyMap<Integer> keyMap) {
|
||||
keyBinder.bindAll(keyMap);
|
||||
}
|
||||
|
||||
private boolean read(BindingReader bindingReader, KeyMap<Integer> keyMap) {
|
||||
Thread readThread = Thread.currentThread();
|
||||
terminal.handle(Signal.INT, signal -> {
|
||||
log.debug("Handling signal {}", signal);
|
||||
readThread.interrupt();
|
||||
});
|
||||
|
||||
Integer operation = null;
|
||||
try {
|
||||
operation = bindingReader.readBinding(keyMap);
|
||||
log.debug("Read got operation {}", operation);
|
||||
} catch (IOError e) {
|
||||
// Ignore Ctrl+C interrupts and just exit the loop
|
||||
log.trace("Read binding error {}", e);
|
||||
}
|
||||
if (operation == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (operation == KeyEvent.Key.Char) {
|
||||
String lastBinding = bindingReader.getLastBinding();
|
||||
if (StringUtils.hasLength(lastBinding)) {
|
||||
dispatchKeyEvent(KeyEvent.of(lastBinding.charAt(0)));
|
||||
}
|
||||
}
|
||||
else if (operation == KeyEvent.Key.Unicode) {
|
||||
String lastBinding = bindingReader.getLastBinding();
|
||||
if (StringUtils.hasLength(lastBinding)) {
|
||||
dispatchKeyEvent(KeyEvent.of(lastBinding.charAt(0)));
|
||||
}
|
||||
}
|
||||
else if (operation == KeyEvent.Key.Mouse) {
|
||||
mouseEvent();
|
||||
}
|
||||
else {
|
||||
dispatchKeyEvent(KeyEvent.of(operation));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void dispatchKeyEvent(KeyEvent event) {
|
||||
Message<KeyEvent> message = MessageBuilder
|
||||
.withPayload(event)
|
||||
.setHeader(ShellMessageHeaderAccessor.EVENT_TYPE, EventLoop.Type.KEY)
|
||||
.build();
|
||||
eventLoop.dispatch(message);
|
||||
}
|
||||
|
||||
private void dispatchMouse(MouseEvent event) {
|
||||
log.debug("Dispatch mouse event: {}", event);
|
||||
eventLoop.dispatch(ShellMessageBuilder.ofMouseEvent(event));
|
||||
}
|
||||
|
||||
private void mouseEvent() {
|
||||
dispatchMouse(MouseEvent.of(terminal.readMouseEvent()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
/*
|
||||
* 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 java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import reactor.core.Disposable;
|
||||
import reactor.core.Disposables;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.shell.component.view.event.EventLoop;
|
||||
import org.springframework.shell.component.view.event.KeyBindingConsumer;
|
||||
import org.springframework.shell.component.view.event.KeyBindingConsumerArgs;
|
||||
import org.springframework.shell.component.view.event.KeyEvent;
|
||||
import org.springframework.shell.component.view.event.KeyHandler;
|
||||
import org.springframework.shell.component.view.event.MouseBindingConsumerArgs;
|
||||
import org.springframework.shell.component.view.event.MouseBindingConsumer;
|
||||
import org.springframework.shell.component.view.event.MouseEvent;
|
||||
import org.springframework.shell.component.view.event.MouseHandler;
|
||||
import org.springframework.shell.component.view.geom.Rectangle;
|
||||
import org.springframework.shell.component.view.message.ShellMessageBuilder;
|
||||
import org.springframework.shell.component.view.screen.Screen;
|
||||
|
||||
/**
|
||||
* Base implementation of a {@link View} and its parent interface
|
||||
* {@link Control} providing some common functionality for implementations.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public abstract class AbstractView implements View {
|
||||
|
||||
private final static Logger log = LoggerFactory.getLogger(AbstractView.class);
|
||||
private final Disposable.Composite disposables = Disposables.composite();
|
||||
private int x = 0;
|
||||
private int y = 0;
|
||||
private int width = 0;
|
||||
private int height = 0;
|
||||
private BiFunction<Screen, Rectangle, Rectangle> drawFunction;
|
||||
private boolean hasFocus;
|
||||
private int layer;
|
||||
private EventLoop eventLoop;
|
||||
private Map<Integer, KeyBindingValue> keyBindings = new HashMap<>();
|
||||
private Map<Integer, MouseBindingValue> mouseBindings = new HashMap<>();
|
||||
|
||||
public AbstractView() {
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register {@link Disposable} to get disposed when view terminates.
|
||||
*
|
||||
* @param disposable a disposable to dispose
|
||||
*/
|
||||
protected void onDestroy(Disposable disposable) {
|
||||
disposables.add(disposable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans running state of a {@link View} so that it can be left to get garbage
|
||||
* collected.
|
||||
*/
|
||||
public void destroy() {
|
||||
disposables.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a view. Mostly reserved for future use and simply calls
|
||||
* {@link #initInternal()}.
|
||||
*
|
||||
* @see #initInternal()
|
||||
*/
|
||||
protected final void init() {
|
||||
initInternal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal init method called from {@link #init()}. Override to do something
|
||||
* usefull. Typically key and mousebindings are registered from this method.
|
||||
*/
|
||||
protected void initInternal() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRect(int x, int y, int width, int height) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Rectangle getRect() {
|
||||
return new Rectangle(x, y, width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLayer(int index) {
|
||||
this.layer = index;
|
||||
}
|
||||
|
||||
protected int getLayer() {
|
||||
return layer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void draw(Screen screen) {
|
||||
drawInternal(screen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component internal drawing method. Implementing classes needs to define this
|
||||
* method to draw something into a {@link Screen}.
|
||||
*
|
||||
* @param screen the screen
|
||||
*/
|
||||
protected abstract void drawInternal(Screen screen);
|
||||
|
||||
@Override
|
||||
public void focus(View view, boolean focus) {
|
||||
log.debug("Focus view={} focus={}", view, focus);
|
||||
if (view == this && focus) {
|
||||
hasFocus = true;
|
||||
}
|
||||
if (!focus) {
|
||||
hasFocus = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasFocus() {
|
||||
return hasFocus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mouse events by dispatching registered consumers into an event loop.
|
||||
* Override to change default behaviour.
|
||||
*/
|
||||
@Override
|
||||
public MouseHandler getMouseHandler() {
|
||||
log.trace("getMouseHandler() {}", this);
|
||||
MouseHandler handler = args -> {
|
||||
MouseEvent event = args.event();
|
||||
int mouse = event.mouse();
|
||||
View view = null;
|
||||
boolean consumed = false;
|
||||
MouseBindingValue mouseBindingValue = getMouseBindings().get(mouse);
|
||||
if (mouseBindingValue != null) {
|
||||
if (mouseBindingValue.mousePredicate().test(event)) {
|
||||
view = this;
|
||||
consumed = dispatchMouseRunCommand(event, mouseBindingValue);
|
||||
}
|
||||
}
|
||||
return MouseHandler.resultOf(args.event(), consumed, view, this);
|
||||
};
|
||||
return handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles keys by dispatching registered command runnable into an event loop.
|
||||
* Override to change default behaviour.
|
||||
*/
|
||||
@Override
|
||||
public KeyHandler getKeyHandler() {
|
||||
log.trace("getKeyHandler() {}", this);
|
||||
KeyHandler handler = args -> {
|
||||
KeyEvent event = args.event();
|
||||
boolean consumed = false;
|
||||
Integer key = event.key();
|
||||
if (key != null) {
|
||||
KeyBindingValue keyBindingValue = getKeyBindings().get(key);
|
||||
if (keyBindingValue != null) {
|
||||
consumed = dispatchRunCommand(event, keyBindingValue);
|
||||
}
|
||||
|
||||
}
|
||||
return KeyHandler.resultOf(event, consumed, null);
|
||||
};
|
||||
return handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a callback function which is invoked after a {@link View} has been
|
||||
* drawn.
|
||||
*
|
||||
* @param drawFunction the draw function
|
||||
*/
|
||||
public void setDrawFunction(BiFunction<Screen, Rectangle, Rectangle> drawFunction) {
|
||||
this.drawFunction = drawFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a draw function.
|
||||
*
|
||||
* @return null if function is not set
|
||||
* @see #setDrawFunction(BiFunction)
|
||||
*/
|
||||
public BiFunction<Screen, Rectangle, Rectangle> getDrawFunction() {
|
||||
return drawFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an {@link EventLoop}.
|
||||
*
|
||||
* @param eventLoop the event loop
|
||||
*/
|
||||
public void setEventLoop(@Nullable EventLoop eventLoop) {
|
||||
this.eventLoop = eventLoop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an {@link EventLoop}.
|
||||
*
|
||||
* @return event loop
|
||||
*/
|
||||
protected EventLoop getEventLoop() {
|
||||
return eventLoop;
|
||||
}
|
||||
|
||||
protected void registerKeyBinding(Integer keyType, String keyCommand) {
|
||||
registerKeyBinding(keyType, keyCommand, null, null);
|
||||
}
|
||||
|
||||
protected void registerKeyBinding(Integer keyType, KeyBindingConsumer keyConsumer) {
|
||||
registerKeyBinding(keyType, null, keyConsumer, null);
|
||||
}
|
||||
|
||||
protected void registerKeyBinding(Integer keyType, Runnable keyRunnable) {
|
||||
registerKeyBinding(keyType, null, null, keyRunnable);
|
||||
}
|
||||
|
||||
private void registerKeyBinding(Integer keyType, String keyCommand, KeyBindingConsumer keyConsumer, Runnable keyRunnable) {
|
||||
keyBindings.compute(keyType, (key, old) -> {
|
||||
return KeyBindingValue.of(old, keyCommand, keyConsumer, keyRunnable);
|
||||
});
|
||||
}
|
||||
|
||||
record KeyBindingValue(String keyCommand, KeyBindingConsumer keyConsumer, Runnable keyRunnable) {
|
||||
static KeyBindingValue of(KeyBindingValue old, String keyCommand, KeyBindingConsumer keyConsumer,
|
||||
Runnable keyRunnable) {
|
||||
if (old == null) {
|
||||
return new KeyBindingValue(keyCommand, keyConsumer, keyRunnable);
|
||||
}
|
||||
return new KeyBindingValue(keyCommand != null ? keyCommand : old.keyCommand(),
|
||||
keyConsumer != null ? keyConsumer : old.keyConsumer(),
|
||||
keyRunnable != null ? keyRunnable : old.keyRunnable());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get key bindings.
|
||||
*
|
||||
* @return key bindings
|
||||
*/
|
||||
protected Map<Integer, KeyBindingValue> getKeyBindings() {
|
||||
return keyBindings;
|
||||
}
|
||||
|
||||
record MouseBindingValue(String mouseCommand, MouseBindingConsumer mouseConsumer, Runnable mouseRunnable,
|
||||
Predicate<MouseEvent> mousePredicate) {
|
||||
static MouseBindingValue of(MouseBindingValue old, String mouseCommand, MouseBindingConsumer mouseConsumer,
|
||||
Runnable mouseRunnable, Predicate<MouseEvent> mousePredicate) {
|
||||
if (old == null) {
|
||||
return new MouseBindingValue(mouseCommand, mouseConsumer, mouseRunnable, mousePredicate);
|
||||
}
|
||||
return new MouseBindingValue(mouseCommand != null ? mouseCommand : old.mouseCommand(),
|
||||
mouseConsumer != null ? mouseConsumer : old.mouseConsumer(),
|
||||
mouseRunnable != null ? mouseRunnable : old.mouseRunnable(),
|
||||
mousePredicate != null ? mousePredicate : old.mousePredicate());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mouse bindings.
|
||||
*
|
||||
* @return mouse bindings
|
||||
*/
|
||||
protected Map<Integer, MouseBindingValue> getMouseBindings() {
|
||||
return mouseBindings;
|
||||
}
|
||||
|
||||
protected void registerMouseBinding(Integer keyType, String mouseCommand) {
|
||||
registerMouseBinding(keyType, mouseCommand, null, null);
|
||||
}
|
||||
|
||||
protected void registerMouseBinding(Integer keyType, MouseBindingConsumer mouseConsumer) {
|
||||
registerMouseBinding(keyType, null, mouseConsumer, null);
|
||||
}
|
||||
|
||||
protected void registerMouseBinding(Integer keyType, Runnable mouseRunnable) {
|
||||
registerMouseBinding(keyType, null, null, mouseRunnable);
|
||||
}
|
||||
|
||||
private void registerMouseBinding(Integer mouseType, String mouseCommand, MouseBindingConsumer mouseConsumer, Runnable mouseRunnable) {
|
||||
Predicate<MouseEvent> mousePredicate = event -> {
|
||||
int x = event.x();
|
||||
int y = event.y();
|
||||
return getRect().contains(x, y);
|
||||
};
|
||||
mouseBindings.compute(mouseType, (key, old) -> {
|
||||
return MouseBindingValue.of(old, mouseCommand, mouseConsumer, mouseRunnable, mousePredicate);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a {@link Message} into an event loop.
|
||||
*
|
||||
* @param message the message to dispatch
|
||||
*/
|
||||
protected void dispatch(Message<?> message) {
|
||||
if (eventLoop != null) {
|
||||
eventLoop.dispatch(message);
|
||||
}
|
||||
else {
|
||||
log.warn("Can't dispatch message {} as eventloop is not set", message);
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean dispatchRunnable(Runnable runnable) {
|
||||
if (eventLoop == null) {
|
||||
return false;
|
||||
}
|
||||
Message<Runnable> message = ShellMessageBuilder
|
||||
.withPayload(runnable)
|
||||
.setEventType(EventLoop.Type.TASK)
|
||||
.build();
|
||||
dispatch(message);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean dispatchRunCommand(KeyEvent event, KeyBindingValue command) {
|
||||
if (eventLoop == null) {
|
||||
return false;
|
||||
}
|
||||
Runnable runnable = command.keyRunnable();
|
||||
if (runnable != null) {
|
||||
Message<Runnable> message = ShellMessageBuilder
|
||||
.withPayload(runnable)
|
||||
.setEventType(EventLoop.Type.TASK)
|
||||
.build();
|
||||
dispatch(message);
|
||||
return true;
|
||||
}
|
||||
KeyBindingConsumer keyConsumer = command.keyConsumer();
|
||||
if (keyConsumer != null) {
|
||||
Message<KeyBindingConsumerArgs> message = ShellMessageBuilder
|
||||
.withPayload(new KeyBindingConsumerArgs(keyConsumer, event))
|
||||
.setEventType(EventLoop.Type.TASK)
|
||||
.build();
|
||||
dispatch(message);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected boolean dispatchMouseRunCommand(MouseEvent event, MouseBindingValue command) {
|
||||
if (eventLoop == null) {
|
||||
return false;
|
||||
}
|
||||
Runnable runnable = command.mouseRunnable();
|
||||
if (runnable != null) {
|
||||
Message<Runnable> message = ShellMessageBuilder
|
||||
.withPayload(runnable)
|
||||
.setEventType(EventLoop.Type.TASK)
|
||||
.build();
|
||||
dispatch(message);
|
||||
return true;
|
||||
}
|
||||
MouseBindingConsumer mouseConsumer = command.mouseConsumer();
|
||||
if (mouseConsumer != null) {
|
||||
Message<MouseBindingConsumerArgs> message = ShellMessageBuilder
|
||||
.withPayload(new MouseBindingConsumerArgs(mouseConsumer, event))
|
||||
.setEventType(EventLoop.Type.TASK)
|
||||
.build();
|
||||
dispatch(message);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* 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.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
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;
|
||||
import org.springframework.shell.component.view.event.MouseHandler;
|
||||
import org.springframework.shell.component.view.geom.Rectangle;
|
||||
import org.springframework.shell.component.view.message.ShellMessageBuilder;
|
||||
import org.springframework.shell.component.view.screen.Screen;
|
||||
|
||||
/**
|
||||
* {@link AppView} provides an opinionated terminal UI application view
|
||||
* controlling main viewing area, menubar, statusbar and modal window system.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class AppView extends BoxView {
|
||||
|
||||
private final static Logger log = LoggerFactory.getLogger(AppView.class);
|
||||
private View main;
|
||||
private View modal;
|
||||
|
||||
@Override
|
||||
protected void drawInternal(Screen screen) {
|
||||
Rectangle rect = getInnerRect();
|
||||
if (main != null) {
|
||||
main.setRect(rect.x(), rect.y(), rect.width(), rect.height());
|
||||
main.draw(screen);
|
||||
}
|
||||
if (modal != null) {
|
||||
modal.setLayer(1);
|
||||
modal.setRect(rect.x() + 5, rect.y() + 5, rect.width() - 10, rect.height() - 10);
|
||||
modal.draw(screen);
|
||||
}
|
||||
super.drawInternal(screen);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MouseHandler getMouseHandler() {
|
||||
log.trace("getMouseHandler()");
|
||||
if (main != null) {
|
||||
MouseHandler handler = main.getMouseHandler();
|
||||
if (handler != null) {
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
return super.getMouseHandler();
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyHandler getKeyHandler() {
|
||||
KeyHandler handler1 = args -> {
|
||||
KeyEvent event = args.event();
|
||||
boolean consumed = false;
|
||||
if (event.isKey(Key.CursorLeft)) {
|
||||
dispatch(ShellMessageBuilder.ofView(this, AppViewEvent.of(this, AppViewEventArgs.Direction.PREVIOUS)));
|
||||
consumed = true;
|
||||
}
|
||||
else if (event.isKey(Key.CursorRight)) {
|
||||
dispatch(ShellMessageBuilder.ofView(this, AppViewEvent.of(this, AppViewEventArgs.Direction.NEXT)));
|
||||
consumed = true;
|
||||
}
|
||||
return KeyHandler.resultOf(event, consumed, null);
|
||||
};
|
||||
|
||||
KeyHandler handler2 = main != null ? main.getKeyHandler() : super.getKeyHandler();
|
||||
return handler2.thenIfNotConsumed(handler1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasFocus() {
|
||||
if (main != null) {
|
||||
return main.hasFocus();
|
||||
}
|
||||
return super.hasFocus();
|
||||
}
|
||||
|
||||
public void setMain(View main) {
|
||||
this.main = main;
|
||||
}
|
||||
|
||||
public void setModal(View modal) {
|
||||
this.modal = modal;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ViewEventArgs} for {@link AppViewEvent}.
|
||||
*
|
||||
* @param direction the direction enumeration
|
||||
*/
|
||||
public record AppViewEventArgs(Direction direction) implements ViewEventArgs {
|
||||
|
||||
/**
|
||||
* Direction where next selection should go.
|
||||
*/
|
||||
public enum Direction {
|
||||
PREVIOUS,
|
||||
NEXT
|
||||
}
|
||||
|
||||
public static AppViewEventArgs of(Direction direction) {
|
||||
return new AppViewEventArgs(direction);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ViewEvent} indicating direction for a next selection.
|
||||
*
|
||||
* @param view the view sending an event
|
||||
* @param args the event args
|
||||
*/
|
||||
public record AppViewEvent(View view, AppViewEventArgs args) implements ViewEvent {
|
||||
|
||||
public static AppViewEvent of(View view, AppViewEventArgs.Direction direction) {
|
||||
return new AppViewEvent(view, AppViewEventArgs.of(direction));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* 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.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.shell.component.view.geom.HorizontalAlign;
|
||||
import org.springframework.shell.component.view.geom.Rectangle;
|
||||
import org.springframework.shell.component.view.geom.VerticalAlign;
|
||||
import org.springframework.shell.component.view.screen.Screen;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@code BoxView} is a {@link View} with an empty background and optional
|
||||
* border and title. All "boxed" views can use this as their base
|
||||
* implementation by either subclassing or wrapping.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class BoxView extends AbstractView {
|
||||
|
||||
private final static Logger log = LoggerFactory.getLogger(BoxView.class);
|
||||
private String title = null;
|
||||
private boolean showBorder = false;
|
||||
private int innerX = -1;
|
||||
private int innerY;
|
||||
private int innerWidth;
|
||||
private int innerHeight;
|
||||
private int paddingTop;
|
||||
private int paddingBottom;
|
||||
private int paddingLeft;
|
||||
private int paddingRight;
|
||||
private Integer backgroundColor = -1;
|
||||
private int titleColor = -1;
|
||||
private int titleStyle = -1;
|
||||
private HorizontalAlign titleAlign;
|
||||
|
||||
@Override
|
||||
public void setRect(int x, int y, int width, int height) {
|
||||
this.innerX = -1;
|
||||
super.setRect(x, y, width, height);
|
||||
}
|
||||
|
||||
// @Override
|
||||
// public MouseHandler getMouseHandler() {
|
||||
// log.trace("getMouseHandler() {}", this);
|
||||
// return args -> {
|
||||
// // box view only handles "mouse click on button 1"
|
||||
// View view = null;
|
||||
// MouseEvent event = args.event();
|
||||
// if (event.getModifiers().isEmpty() && event.getType() == MouseEvent.Type.Released
|
||||
// && event.getButton() == MouseEvent.Button.Button1) {
|
||||
// int x = event.getX();
|
||||
// int y = event.getY();
|
||||
// if (getRect().contains(x, y)) {
|
||||
// view = this;
|
||||
// }
|
||||
// }
|
||||
// return MouseHandler.resultOf(args.event(), view != null, view, this);
|
||||
// };
|
||||
// }
|
||||
|
||||
/**
|
||||
* Sets a paddings for this view.
|
||||
*
|
||||
* @param paddingTop the top padding
|
||||
* @param paddingBottom the bottom padding
|
||||
* @param paddingLeft the left padding
|
||||
* @param paddingRight the right padding
|
||||
* @return a BoxView for chaining
|
||||
*/
|
||||
public BoxView setBorderPadding(int paddingTop, int paddingBottom, int paddingLeft, int paddingRight) {
|
||||
this.paddingTop = paddingTop;
|
||||
this.paddingBottom = paddingBottom;
|
||||
this.paddingLeft = paddingLeft;
|
||||
this.paddingRight = paddingRight;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines if border is shown.
|
||||
*
|
||||
* @param showBorder the flag showing border
|
||||
*/
|
||||
public void setShowBorder(boolean showBorder) {
|
||||
this.showBorder = showBorder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if border is shown.
|
||||
*
|
||||
* @return true if border is shown
|
||||
*/
|
||||
public boolean isShowBorder() {
|
||||
return showBorder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a title. {@code title} is shown within a top-level border boundary and
|
||||
* will not be visible if border itself is not visible.
|
||||
*
|
||||
* @param title the border title
|
||||
*/
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a background color. If color is set to {@code null} it indicates that
|
||||
* background should be set to be {@code empty} causing possible layer to be
|
||||
* non-transparent.
|
||||
*
|
||||
* @param backgroundColor the background color
|
||||
*/
|
||||
public void setBackgroundColor(@Nullable Integer backgroundColor) {
|
||||
this.backgroundColor = backgroundColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a title color.
|
||||
*
|
||||
* @param titleColor the title color
|
||||
*/
|
||||
public void setTitleColor(int titleColor) {
|
||||
this.titleColor = titleColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a title style.
|
||||
*
|
||||
* @param titleStyle the title style
|
||||
*/
|
||||
public void setTitleStyle(int titleStyle) {
|
||||
this.titleStyle = titleStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a title align.
|
||||
*
|
||||
* @param titleAlign the title align
|
||||
*/
|
||||
public void setTitleAlign(HorizontalAlign titleAlign) {
|
||||
this.titleAlign = titleAlign;
|
||||
}
|
||||
|
||||
/**
|
||||
* Possibly draws a box around this view and title in a box top boundary. Also
|
||||
* calls a {@code draw function} if defined.
|
||||
*
|
||||
* @param screen the screen
|
||||
*/
|
||||
protected void drawInternal(Screen screen) {
|
||||
log.trace("drawInternal() {}", this);
|
||||
Rectangle rect = getRect();
|
||||
if (rect.width() <= 0 || rect.height() <= 0) {
|
||||
return;
|
||||
}
|
||||
if (backgroundColor == null) {
|
||||
screen.writerBuilder().layer(getLayer()).build().background(rect, -1);
|
||||
}
|
||||
else if (backgroundColor > -1) {
|
||||
screen.writerBuilder().layer(getLayer()).build().background(rect, backgroundColor);
|
||||
}
|
||||
if (showBorder && rect.width() >= 2 && rect.height() >= 2) {
|
||||
screen.writerBuilder().layer(getLayer()).build().border(rect.x(), rect.y(), rect.width(), rect.height());
|
||||
if (StringUtils.hasText(title)) {
|
||||
Rectangle r = new Rectangle(rect.x() + 1, rect.y(), rect.width() - 2, 1);
|
||||
if (titleColor > -1) {
|
||||
screen.writerBuilder().layer(getLayer()).color(titleColor).style(titleStyle).build().text(title, r, titleAlign, VerticalAlign.TOP);
|
||||
}
|
||||
else {
|
||||
screen.writerBuilder().layer(getLayer()).build().text(title, r, titleAlign, VerticalAlign.TOP);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (getDrawFunction() != null) {
|
||||
Rectangle r = getDrawFunction().apply(screen, rect);
|
||||
innerX = r.x();
|
||||
innerY = r.y();
|
||||
innerWidth = r.width();
|
||||
innerHeight = r.height();
|
||||
}
|
||||
else {
|
||||
Rectangle r = getInnerRect();
|
||||
innerX = r.x();
|
||||
innerY = r.y();
|
||||
innerWidth = r.width();
|
||||
innerHeight = r.height();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an inner rectangle of this view.
|
||||
*
|
||||
* @return an inner rectangle of this view
|
||||
*/
|
||||
protected Rectangle getInnerRect() {
|
||||
if (innerX >= 0) {
|
||||
return new Rectangle(innerX, innerY, innerWidth, innerHeight);
|
||||
}
|
||||
Rectangle rect = getRect();
|
||||
int x = rect.x();
|
||||
int y = rect.y();
|
||||
int width = rect.width();
|
||||
int height = rect.height();
|
||||
if (isShowBorder()) {
|
||||
x++;
|
||||
y++;
|
||||
width -= 2;
|
||||
height -= 2;
|
||||
}
|
||||
x = x + paddingLeft;
|
||||
y = y + paddingTop;
|
||||
width = width - paddingLeft - paddingRight;
|
||||
height = height - paddingTop - paddingBottom;
|
||||
if (width < 0) {
|
||||
width = 0;
|
||||
}
|
||||
if (height < 0) {
|
||||
height = 0;
|
||||
}
|
||||
return new Rectangle(x, y, width, height);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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.springframework.shell.component.view.screen.Screen;
|
||||
|
||||
/**
|
||||
* Base interface for all cells. Typically a {@link Cell} is a building block in
|
||||
* a {@link View} not needing to be aware of how it is drawn into a {@link Screen}
|
||||
* but needs to aware of its "item", bounds via {@link Control} and other
|
||||
* properties like {@code background}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface Cell<T> extends Control {
|
||||
|
||||
/**
|
||||
* Get item bound to a cell.
|
||||
*
|
||||
* @return item bound to a cell
|
||||
*/
|
||||
T getItem();
|
||||
|
||||
/**
|
||||
* Sets an item to bound into a cell.
|
||||
*
|
||||
* @param item item to bound into a cell
|
||||
*/
|
||||
void setItem(T item);
|
||||
|
||||
/**
|
||||
* Sets a style.
|
||||
*
|
||||
* @param style the style
|
||||
*/
|
||||
void setStyle(int style);
|
||||
|
||||
/**
|
||||
* Sets a foreground color.
|
||||
*
|
||||
* @param foregroundColor the background color
|
||||
*/
|
||||
void setForegroundColor(int foregroundColor);
|
||||
|
||||
/**
|
||||
* Sets a background color.
|
||||
*
|
||||
* @param backgroundColor the background color
|
||||
*/
|
||||
void setBackgroundColor(int backgroundColor);
|
||||
|
||||
/**
|
||||
* Return if cell is selected.
|
||||
*
|
||||
* @return true if cell is selected
|
||||
*/
|
||||
boolean isSelected();
|
||||
|
||||
/**
|
||||
* Update selected status.
|
||||
*
|
||||
* @param selected true if cell is selected
|
||||
*/
|
||||
void updateSelected(boolean selected);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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.springframework.shell.component.view.geom.Rectangle;
|
||||
import org.springframework.shell.component.view.screen.Screen;
|
||||
|
||||
/**
|
||||
* Base interface for all controls. {@link Control} is able to define a
|
||||
* {@link Rectangle} it is bound to and draw into a {@link Screen}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
* @see View
|
||||
* @see Cell
|
||||
*/
|
||||
public interface Control {
|
||||
|
||||
/**
|
||||
* Draw {@link Control} into a {@link Screen}.
|
||||
*
|
||||
* @param screen the screen
|
||||
*/
|
||||
void draw(Screen screen);
|
||||
|
||||
/**
|
||||
* Gets rectanle of a bounded box for this {@link View}.
|
||||
*
|
||||
* @return the rectanle of a bounded box
|
||||
*/
|
||||
Rectangle getRect();
|
||||
|
||||
/**
|
||||
* Sets bounds where this {@link Control} should operate.
|
||||
*
|
||||
* @param x a x coord of a bounded box
|
||||
* @param y an y coord of a bounded box
|
||||
* @param width a width of a bounded box
|
||||
* @param height a height of a bounded box
|
||||
*/
|
||||
void setRect(int x, int y, int width, int height);
|
||||
}
|
||||
@@ -0,0 +1,541 @@
|
||||
/*
|
||||
* 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 java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
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;
|
||||
import org.springframework.shell.component.view.geom.Rectangle;
|
||||
import org.springframework.shell.component.view.screen.Screen;
|
||||
|
||||
/**
|
||||
* {@code GridView} is a layout container with no initial {@link View views}.
|
||||
*
|
||||
* Loosely based on ideas from other grid layouts having features like rows and
|
||||
* columns, column and row spanning, dynamic layouts based on container size
|
||||
* using "CSS media queries" type of structure.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class GridView extends BoxView {
|
||||
|
||||
private final static Logger log = LoggerFactory.getLogger(GridView.class);
|
||||
private List<GridItem> gridItems = new ArrayList<>();
|
||||
private int[] columnSize;
|
||||
private int[] rowSize;
|
||||
private int minWidth;
|
||||
private int minHeight;
|
||||
private int gapRows;
|
||||
private int gapColumns;
|
||||
private int rowOffset;
|
||||
private int columnOffset;
|
||||
private boolean showBorders;
|
||||
|
||||
/**
|
||||
* Defines how the columns of the grid are distributed. Each value
|
||||
* defines the size of one column, starting with the leftmost column. Values
|
||||
* greater 0 represent absolute column widths (gaps not included). Values less
|
||||
* or equal 0 represent proportional column widths or fractions of the remaining
|
||||
* free space, where 0 is treated the same as -1. That is, a column with a value
|
||||
* of -3 will have three times the width of a column with a value of -1 (or 0).
|
||||
* The minimum width set with SetMinSize() is always observed.
|
||||
*
|
||||
* Views may extend beyond the columns defined explicitly with this
|
||||
* function. A value of 0 is assumed for any undefined column. In fact, if you
|
||||
* never call this function, all columns occupied by Views will have the
|
||||
* same width. On the other hand, unoccupied columns defined with this function
|
||||
* will always take their place.
|
||||
*
|
||||
* Assuming a total width of the grid of 100 cells and a minimum width of 0, the
|
||||
* following call will result in columns with widths of 30, 10, 15, 15, and 30
|
||||
* cells:
|
||||
*
|
||||
* grid.SetColumns(30, 10, -1, -1, -2)
|
||||
*
|
||||
* If a primitive were then placed in the 6th and 7th column, the resulting
|
||||
* widths would be: 30, 10, 10, 10, 20, 10, and 10 cells.
|
||||
*
|
||||
* If you then called SetMinSize() as follows:
|
||||
*
|
||||
* grid.SetMinSize(15, 20)
|
||||
*
|
||||
* The resulting widths would be: 30, 15, 15, 15, 20, 15, and 15 cells, a total
|
||||
* of 125 cells, 25 cells wider than the available grid width.
|
||||
*
|
||||
* @param columns
|
||||
* @return
|
||||
*/
|
||||
public GridView setColumnSize(int... columns) {
|
||||
this.columnSize = columns;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param rows
|
||||
* @return
|
||||
* @see #setColumnSize(int...)
|
||||
*/
|
||||
public GridView setRowSize(int... rows) {
|
||||
this.rowSize = rows;
|
||||
return this;
|
||||
}
|
||||
|
||||
public GridView addItem(View view, int row, int column, int rowSpan, int colSpan, int minGridHeight,
|
||||
int minGridWidth) {
|
||||
GridItem gridItem = new GridItem(view, row, column, colSpan, rowSpan, minGridHeight,
|
||||
minGridWidth, false);
|
||||
gridItems.add(gridItem);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines if borders is shown.
|
||||
*
|
||||
* @param showBorders the flag showing borders
|
||||
*/
|
||||
public void setShowBorders(boolean showBorders) {
|
||||
this.showBorders = showBorders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if borders is shown.
|
||||
*
|
||||
* @return true if borders is shown
|
||||
*/
|
||||
public boolean isShowBorders() {
|
||||
return showBorders;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MouseHandler getMouseHandler() {
|
||||
log.trace("getMouseHandler()");
|
||||
return args -> {
|
||||
View focus = null;
|
||||
for (GridItem i : gridItems) {
|
||||
MouseHandlerResult r = i.view.getMouseHandler().handle(args);
|
||||
if (r.focus() != null) {
|
||||
focus = r.focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return MouseHandler.resultOf(args.event(), true, focus, null);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyHandler getKeyHandler() {
|
||||
log.trace("getKeyHandler()");
|
||||
for (GridItem i : gridItems) {
|
||||
if (i.view.hasFocus()) {
|
||||
return i.view.getKeyHandler();
|
||||
}
|
||||
}
|
||||
return super.getKeyHandler();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasFocus() {
|
||||
for (GridItem i : gridItems) {
|
||||
if (i.view.hasFocus()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return super.hasFocus();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawInternal(Screen screen) {
|
||||
super.drawInternal(screen);
|
||||
Rectangle rect = getInnerRect();
|
||||
int x = rect.x();
|
||||
int y = rect.y();
|
||||
int width = rect.width();
|
||||
int height = rect.height();
|
||||
|
||||
Map<View, GridItem> items = new HashMap<>();
|
||||
for (GridItem item : gridItems) {
|
||||
item.visible = false;
|
||||
if (item.width <= 0 || item.height <= 0 || width < item.minGridWidth || height < item.minGridHeight) {
|
||||
continue;
|
||||
}
|
||||
GridItem previousItem = items.get(item.view);
|
||||
if (previousItem != null && item.minGridWidth < previousItem.minGridWidth
|
||||
&& item.minGridHeight < previousItem.minGridHeight) {
|
||||
continue;
|
||||
}
|
||||
items.put(item.view, item);
|
||||
}
|
||||
|
||||
// How many rows and columns do we have?
|
||||
int rows = rowSize.length;
|
||||
int columns = columnSize.length;
|
||||
for (GridItem item : items.values()) {
|
||||
int rowEnd = item.row + item.height;
|
||||
if (rowEnd > rows) {
|
||||
rows = rowEnd;
|
||||
}
|
||||
int columnEnd = item.column + item.width;
|
||||
if (columnEnd > columns) {
|
||||
columns = columnEnd;
|
||||
}
|
||||
}
|
||||
if (rows == 0 || columns == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Where are they located?
|
||||
int[] rowPos = new int[rows];
|
||||
int[] rowHeight = new int[rows];
|
||||
int[] columnPos = new int[columns];
|
||||
int[] columnWidth = new int[columns];
|
||||
|
||||
// How much space do we distribute?
|
||||
int remainingWidth = width;
|
||||
int remainingHeight = height;
|
||||
int proportionalWidth = 0;
|
||||
int proportionalHeight = 0;
|
||||
|
||||
|
||||
for (int index = 0; index < rowSize.length; index++) {
|
||||
int row = rowSize[index];
|
||||
if (row > 0) {
|
||||
if (row < this.minHeight) {
|
||||
row = this.minHeight;
|
||||
}
|
||||
remainingHeight -= row;
|
||||
rowHeight[index] = row;
|
||||
}
|
||||
else if (row == 0) {
|
||||
proportionalHeight++;
|
||||
}
|
||||
else {
|
||||
proportionalHeight += -row;
|
||||
}
|
||||
}
|
||||
|
||||
for (int index = 0; index < columnSize.length; index++) {
|
||||
int column = columnSize[index];
|
||||
if (column > 0) {
|
||||
if (column < this.minWidth) {
|
||||
column = this.minWidth;
|
||||
}
|
||||
remainingWidth -= column;
|
||||
columnWidth[index] = column;
|
||||
}
|
||||
else if (column == 0) {
|
||||
proportionalWidth++;
|
||||
}
|
||||
else {
|
||||
proportionalWidth += -column;
|
||||
}
|
||||
}
|
||||
|
||||
if (isShowBorders()) {
|
||||
remainingHeight -= rows + 1;
|
||||
remainingWidth -= columns + 1;
|
||||
}
|
||||
else {
|
||||
remainingHeight -= (rows - 1) * this.gapRows;
|
||||
remainingWidth -= (columns - 1) * this.gapColumns;
|
||||
}
|
||||
if (rows > this.rowSize.length) {
|
||||
proportionalHeight += rows - this.rowSize.length;
|
||||
}
|
||||
if (columns > this.columnSize.length) {
|
||||
proportionalWidth += columns - this.columnSize.length;
|
||||
}
|
||||
|
||||
// Distribute proportional rows/columns.
|
||||
for (int index = 0; index < rows; index++) {
|
||||
int row = 0;
|
||||
if (index < this.rowSize.length) {
|
||||
row = this.rowSize[index];
|
||||
}
|
||||
if (row > 0) {
|
||||
continue;
|
||||
}
|
||||
else if (row == 0) {
|
||||
row = 1;
|
||||
}
|
||||
else {
|
||||
row = -row;
|
||||
}
|
||||
int rowAbs = row * remainingHeight / proportionalHeight;
|
||||
remainingHeight -= rowAbs;
|
||||
proportionalHeight -= row;
|
||||
if (rowAbs < this.minHeight) {
|
||||
rowAbs = this.minHeight;
|
||||
}
|
||||
rowHeight[index] = rowAbs;
|
||||
}
|
||||
|
||||
for (int index = 0; index < columns; index++) {
|
||||
int column = 0;
|
||||
if (index < this.columnSize.length) {
|
||||
column = this.columnSize[index];
|
||||
}
|
||||
if (column > 0) {
|
||||
continue;
|
||||
}
|
||||
else if (column == 0) {
|
||||
column = 1;
|
||||
}
|
||||
else {
|
||||
column = -column;
|
||||
}
|
||||
int columnAbs = column * remainingWidth / proportionalWidth;
|
||||
remainingWidth -= columnAbs;
|
||||
proportionalWidth -= column;
|
||||
if (columnAbs < this.minWidth) {
|
||||
columnAbs = this.minWidth;
|
||||
}
|
||||
columnWidth[index] = columnAbs;
|
||||
}
|
||||
|
||||
// Calculate row/column positions.
|
||||
int columnX = 0, rowY = 0;
|
||||
if (isShowBorders()) {
|
||||
columnX++;
|
||||
rowY++;
|
||||
}
|
||||
for (int index = 0; index < rowHeight.length; index++) {
|
||||
int row = rowHeight[index];
|
||||
rowPos[index] = rowY;
|
||||
int gap = this.gapRows;
|
||||
if (isShowBorders()) {
|
||||
gap = 1;
|
||||
}
|
||||
rowY += row + gap;
|
||||
}
|
||||
for (int index = 0; index < columnWidth.length; index++) {
|
||||
int column = columnWidth[index];
|
||||
columnPos[index] = columnX;
|
||||
int gap = this.gapColumns;
|
||||
if (isShowBorders()) {
|
||||
gap = 1;
|
||||
}
|
||||
columnX += column + gap;
|
||||
}
|
||||
|
||||
// Calculate primitive positions.
|
||||
GridItem focus = null;
|
||||
for (Entry<View, GridItem> entry : items.entrySet()) {
|
||||
View primitive = entry.getKey();
|
||||
GridItem item = entry.getValue();
|
||||
int px = columnPos[item.column];
|
||||
int py = rowPos[item.row];
|
||||
int pw = 0, ph = 0;
|
||||
for (int index = 0; index < item.height; index++) {
|
||||
ph += rowHeight[item.row + index];
|
||||
}
|
||||
for (int index = 0; index < item.width; index++) {
|
||||
pw += columnWidth[item.column + index];
|
||||
}
|
||||
if (isShowBorders()) {
|
||||
pw += item.width - 1;
|
||||
ph += item.height - 1;
|
||||
}
|
||||
else {
|
||||
pw += (item.width - 1) * this.gapColumns;
|
||||
ph += (item.height - 1) * this.gapRows;
|
||||
}
|
||||
item.x = px;
|
||||
item.y = py;
|
||||
item.w = pw;
|
||||
item.h = ph;
|
||||
item.visible = true;
|
||||
if (primitive.hasFocus()) {
|
||||
focus = item;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate screen offsets.
|
||||
int offsetX = 0;
|
||||
int offsetY = 0;
|
||||
int add = 1;
|
||||
if (!isShowBorders()) {
|
||||
add = this.gapRows;
|
||||
}
|
||||
for (int index = 0; index < rowHeight.length; index++) {
|
||||
int height2 = rowHeight[index];
|
||||
if (index >= this.rowOffset) {
|
||||
break;
|
||||
}
|
||||
offsetY += height2 + add;
|
||||
}
|
||||
if (!isShowBorders()) {
|
||||
add = this.gapColumns;
|
||||
}
|
||||
for (int index = 0; index < columnWidth.length; index++) {
|
||||
int width2 = columnWidth[index];
|
||||
if (index >= this.columnOffset) {
|
||||
break;
|
||||
}
|
||||
offsetX += width2 + add;
|
||||
}
|
||||
|
||||
// Line up the last row/column with the end of the available area.
|
||||
int border = 0;
|
||||
if (isShowBorders()) {
|
||||
border = 1;
|
||||
}
|
||||
int last = rowPos.length - 1;
|
||||
if (rowPos[last] + rowHeight[last] + border - offsetY < height) {
|
||||
offsetY = rowPos[last] - height + rowHeight[last] + border;
|
||||
}
|
||||
last = columnPos.length - 1;
|
||||
if (columnPos[last] + columnWidth[last] + border - offsetX < width) {
|
||||
offsetX = columnPos[last] - width + columnWidth[last] + border;
|
||||
}
|
||||
|
||||
// The focused item must be within the visible area.
|
||||
if (focus != null) {
|
||||
if (focus.y + focus.h - offsetY >= height) {
|
||||
offsetY = focus.y - height + focus.h;
|
||||
}
|
||||
if (focus.y - offsetY < 0) {
|
||||
offsetY = focus.y;
|
||||
}
|
||||
if (focus.x + focus.w - offsetX >= width) {
|
||||
offsetX = focus.x - width + focus.w;
|
||||
}
|
||||
if (focus.x - offsetX < 0) {
|
||||
offsetX = focus.x;
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust row/column offsets based on this value.
|
||||
int from = 0;
|
||||
int to = 0;
|
||||
for (int index = 0; index < rowPos.length; index ++) {
|
||||
int pos = rowPos[index];
|
||||
if (pos - offsetY < 0) {
|
||||
from = index + 1;
|
||||
}
|
||||
if (pos - offsetY < height) {
|
||||
to = index;
|
||||
}
|
||||
}
|
||||
if (this.rowOffset < from) {
|
||||
this.rowOffset = from;
|
||||
}
|
||||
if (this.rowOffset > to) {
|
||||
this.rowOffset = to;
|
||||
}
|
||||
|
||||
from = 0;
|
||||
to = 0;
|
||||
for (int index = 0; index < columnPos.length; index ++) {
|
||||
int pos = columnPos[index];
|
||||
if (pos - offsetX < 0) {
|
||||
from = index + 1;
|
||||
}
|
||||
if (pos - offsetX < width) {
|
||||
to = index;
|
||||
}
|
||||
}
|
||||
if (this.columnOffset < from) {
|
||||
this.columnOffset = from;
|
||||
}
|
||||
if (this.columnOffset > to) {
|
||||
this.columnOffset = to;
|
||||
}
|
||||
|
||||
// Draw primitives and borders.
|
||||
for (Entry<View, GridItem> entry : items.entrySet()) {
|
||||
View view = entry.getKey();
|
||||
GridItem item = entry.getValue();
|
||||
|
||||
// Final primitive position.
|
||||
if (!item.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
item.x -= offsetX;
|
||||
item.y -= offsetY;
|
||||
if (item.x >= width || item.x + item.w <= 0 || item.y >= height || item.y + item.h <= 0) {
|
||||
item.visible = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.x + item.w > width) {
|
||||
item.w = width - item.x;
|
||||
}
|
||||
if (item.y + item.h > height) {
|
||||
item.h = height - item.y;
|
||||
}
|
||||
if (item.x < 0) {
|
||||
item.w += item.x;
|
||||
item.x = 0;
|
||||
}
|
||||
if (item.y < 0) {
|
||||
item.h += item.y;
|
||||
item.y = 0;
|
||||
}
|
||||
if (item.w <= 0 || item.h <= 0) {
|
||||
item.visible = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
item.x += x;
|
||||
item.y += y;
|
||||
view.setRect(item.x, item.y, item.w, item.h);
|
||||
|
||||
view.draw(screen);
|
||||
|
||||
// Draw border around primitive.
|
||||
if (isShowBorders()) {
|
||||
screen.writerBuilder().build().border(item.x - 1, item.y - 1, item.w + 2, item.h + 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class GridItem {
|
||||
View view;
|
||||
int row;
|
||||
int column;
|
||||
int width;
|
||||
int height;
|
||||
int minGridHeight;
|
||||
int minGridWidth;
|
||||
boolean visible;
|
||||
// The last position of the item relative to the top-left
|
||||
// corner of the grid. Undefined if visible is false.
|
||||
int x, y, w, h;
|
||||
|
||||
GridItem(View view, int row, int column, int width, int height, int minGridHeight, int minGridWidth,
|
||||
boolean visible) {
|
||||
this.view = view;
|
||||
this.row = row;
|
||||
this.column = column;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.minGridHeight = minGridHeight;
|
||||
this.minGridWidth = minGridWidth;
|
||||
this.visible = visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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.springframework.shell.component.view.event.KeyEvent;
|
||||
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.screen.Screen;
|
||||
|
||||
/**
|
||||
* {@code InputView} is used as a text input.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class InputView extends BoxView {
|
||||
|
||||
private final StringBuilder text = new StringBuilder();
|
||||
private int cursorPosition = 0;
|
||||
|
||||
@Override
|
||||
protected void initInternal() {
|
||||
registerKeyBinding(Key.CursorLeft, event -> left());
|
||||
registerKeyBinding(Key.CursorRight, event -> right());
|
||||
registerKeyBinding(Key.Delete, () -> delete());
|
||||
registerKeyBinding(Key.Backspace, () -> backspace());
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyHandler getKeyHandler() {
|
||||
KeyHandler handler = args -> {
|
||||
KeyEvent event = args.event();
|
||||
boolean consumed = false;
|
||||
if (event.isKey()) {
|
||||
consumed = true;
|
||||
int plainKey = event.getPlainKey();
|
||||
add(new String(new char[]{(char)plainKey}));
|
||||
}
|
||||
return KeyHandler.resultOf(event, consumed, null);
|
||||
};
|
||||
return handler.thenIfNotConsumed(super.getKeyHandler());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawInternal(Screen screen) {
|
||||
Rectangle rect = getInnerRect();
|
||||
String s = text.toString();
|
||||
screen.writerBuilder().build().text(s, rect.x(), rect.y());
|
||||
screen.setShowCursor(hasFocus());
|
||||
screen.setCursorPosition(new Position(rect.x() + cursorPosition, rect.y()));
|
||||
super.drawInternal(screen);
|
||||
}
|
||||
|
||||
public String getInputText() {
|
||||
return text.toString();
|
||||
}
|
||||
|
||||
// private void enter(KeyEvent event) {
|
||||
// // getShellMessageListener().onMessage(ShellMessageBuilder.ofViewFocus("enter", this));
|
||||
// }
|
||||
|
||||
// private void leave(KeyEvent event) {
|
||||
// // getShellMessageListener().onMessage(ShellMessageBuilder.ofViewFocus("leave", this));
|
||||
// }
|
||||
|
||||
private void add(String data) {
|
||||
text.append(data);
|
||||
right();
|
||||
}
|
||||
|
||||
private void backspace() {
|
||||
if (cursorPosition > 0) {
|
||||
text.deleteCharAt(cursorPosition - 1);
|
||||
}
|
||||
left();
|
||||
}
|
||||
|
||||
private void delete() {
|
||||
text.deleteCharAt(cursorPosition);
|
||||
}
|
||||
|
||||
private void left() {
|
||||
cursorPosition--;
|
||||
}
|
||||
|
||||
private void right() {
|
||||
cursorPosition++;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* 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 java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.shell.component.view.control.cell.ListCell;
|
||||
import org.springframework.shell.component.view.event.KeyEvent.Key;
|
||||
import org.springframework.shell.component.view.event.MouseEvent;
|
||||
import org.springframework.shell.component.view.geom.Rectangle;
|
||||
import org.springframework.shell.component.view.message.ShellMessageBuilder;
|
||||
import org.springframework.shell.component.view.screen.Screen;
|
||||
import org.springframework.shell.component.view.screen.ScreenItem;
|
||||
|
||||
/**
|
||||
* {@link ListView} shows {@code list items} vertically.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class ListView<T> extends BoxView {
|
||||
|
||||
private final static Logger log = LoggerFactory.getLogger(ListView.class);
|
||||
|
||||
private final List<T> items = new ArrayList<>();
|
||||
private int selected = -1;
|
||||
|
||||
private final List<ListCell<T>> cells = new ArrayList<>();
|
||||
private Function<ListView<T>, ListCell<T>> factory = listView -> new ListCell<>();
|
||||
|
||||
/**
|
||||
* Construct list view with no initial items.
|
||||
*/
|
||||
public ListView() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawInternal(Screen screen) {
|
||||
Rectangle rect = getInnerRect();
|
||||
int y = rect.y();
|
||||
|
||||
int i = 0;
|
||||
for (ListCell<T> c : cells) {
|
||||
c.setRect(rect.x(), y++, rect.width(), 1);
|
||||
if (i == selected) {
|
||||
c.updateSelected(true);
|
||||
c.setStyle(ScreenItem.STYLE_BOLD);
|
||||
}
|
||||
else {
|
||||
c.updateSelected(false);
|
||||
c.setBackgroundColor(-1);
|
||||
c.setStyle(-1);
|
||||
}
|
||||
c.updateSelected(i == selected);
|
||||
c.draw(screen);
|
||||
i++;
|
||||
}
|
||||
super.drawInternal(screen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a cell factory.
|
||||
*
|
||||
* @param factory the cell factory
|
||||
*/
|
||||
public void setCellFactory(Function<ListView<T>, ListCell<T>> factory) {
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
public void setItems(List<T> items) {
|
||||
this.items.clear();
|
||||
this.items.addAll(items);
|
||||
this.cells.clear();
|
||||
for (T i : items) {
|
||||
ListCell<T> c = factory.apply(this);
|
||||
cells.add(c);
|
||||
c.updateItem(i);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initInternal() {
|
||||
registerKeyBinding(Key.CursorUp, () -> up());
|
||||
registerKeyBinding(Key.CursorDown, () -> down());
|
||||
registerKeyBinding(Key.Enter, () -> enter());
|
||||
|
||||
registerMouseBinding(MouseEvent.Type.Wheel | MouseEvent.Button.WheelUp, () -> up());
|
||||
registerMouseBinding(MouseEvent.Type.Wheel | MouseEvent.Button.WheelDown, () -> down());
|
||||
registerMouseBinding(MouseEvent.Type.Released | MouseEvent.Button.Button1, () -> {});
|
||||
}
|
||||
|
||||
private void up() {
|
||||
updateIndex(-1);
|
||||
dispatch(ShellMessageBuilder.ofView(this, ListViewSelectedItemChangedEvent.of(this, selectedItem())));
|
||||
}
|
||||
|
||||
private void down() {
|
||||
updateIndex(1);
|
||||
dispatch(ShellMessageBuilder.ofView(this, ListViewSelectedItemChangedEvent.of(this, selectedItem())));
|
||||
}
|
||||
|
||||
private void enter() {
|
||||
log.info("XXX enter");
|
||||
dispatch(ShellMessageBuilder.ofView(this, ListViewOpenSelectedItemEvent.of(this, selectedItem())));
|
||||
}
|
||||
|
||||
public void setSelected(int selected) {
|
||||
if (this.selected != selected) {
|
||||
this.selected = selected;
|
||||
dispatch(ShellMessageBuilder.ofView(this, ListViewSelectedItemChangedEvent.of(this, selectedItem())));
|
||||
}
|
||||
}
|
||||
|
||||
private T selectedItem() {
|
||||
T selectedItem = null;
|
||||
if (selected >= 0 && selected < items.size()) {
|
||||
selectedItem = items.get(selected);
|
||||
}
|
||||
return selectedItem;
|
||||
}
|
||||
|
||||
private void updateIndex(int step) {
|
||||
int size = items.size();
|
||||
if (step > 0) {
|
||||
if (selected + step < size) {
|
||||
selected += step;
|
||||
}
|
||||
}
|
||||
else if (step < 0) {
|
||||
if (selected - step > 0) {
|
||||
selected += step;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ViewEventArgs} for {@link ListViewOpenSelectedItemEvent} and
|
||||
* {@link ListViewSelectedItemChangedEvent}.
|
||||
*
|
||||
* @param item the list view item
|
||||
*/
|
||||
public record ListViewItemEventArgs<T>(T item) implements ViewEventArgs {
|
||||
|
||||
public static <T> ListViewItemEventArgs<T> of(T item) {
|
||||
return new ListViewItemEventArgs<T>(item);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ViewEvent} indicating that selected item has been requested to open.
|
||||
*
|
||||
* @param view the view sending an event
|
||||
* @param args the event args
|
||||
*/
|
||||
public record ListViewOpenSelectedItemEvent<T>(View view, ListViewItemEventArgs<T> args) implements ViewEvent {
|
||||
|
||||
public static <T> ListViewOpenSelectedItemEvent<T> of(View view, T item) {
|
||||
return new ListViewOpenSelectedItemEvent<T>(view, ListViewItemEventArgs.of(item));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ViewEvent} indicating that selected item has changed.
|
||||
*
|
||||
* @param view the view sending an event
|
||||
* @param args the event args
|
||||
*/
|
||||
public record ListViewSelectedItemChangedEvent<T>(View view, ListViewItemEventArgs<T> args) implements ViewEvent {
|
||||
|
||||
public static <T> ListViewSelectedItemChangedEvent<T> of(View view, T item) {
|
||||
return new ListViewSelectedItemChangedEvent<T>(view, ListViewItemEventArgs.of(item));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
* 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 java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.shell.component.view.control.MenuView.MenuItem;
|
||||
import org.springframework.shell.component.view.control.MenuView.MenuViewOpenSelectedItemEvent;
|
||||
import org.springframework.shell.component.view.event.KeyEvent.Key;
|
||||
import org.springframework.shell.component.view.event.KeyHandler;
|
||||
import org.springframework.shell.component.view.event.MouseEvent;
|
||||
import org.springframework.shell.component.view.event.MouseHandler;
|
||||
import org.springframework.shell.component.view.geom.Dimension;
|
||||
import org.springframework.shell.component.view.geom.Rectangle;
|
||||
import org.springframework.shell.component.view.screen.Screen;
|
||||
import org.springframework.shell.component.view.screen.Screen.Writer;
|
||||
import org.springframework.shell.component.view.screen.ScreenItem;
|
||||
|
||||
/**
|
||||
* {@link MenuBarView} shows {@link MenuBarItem items} horizontally and is
|
||||
* typically used in layouts which builds complete terminal UI's.
|
||||
*
|
||||
* Internally {@link MenuView} is used to show the menus.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class MenuBarView extends BoxView {
|
||||
|
||||
private final Logger log = LoggerFactory.getLogger(MenuBarView.class);
|
||||
private final List<MenuBarItem> items = new ArrayList<>();
|
||||
private MenuView currentMenuView;
|
||||
private int activeItemIndex = -1;
|
||||
|
||||
/**
|
||||
* Construct menubar view with menubar items.
|
||||
*
|
||||
* @param items the menubar items
|
||||
*/
|
||||
public MenuBarView(MenuBarItem[] items) {
|
||||
setItems(Arrays.asList(items));
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct menubar view with menubar items.
|
||||
*
|
||||
* @param items the menubar items
|
||||
*/
|
||||
public static MenuBarView of(MenuBarItem... items) {
|
||||
return new MenuBarView(items);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawInternal(Screen screen) {
|
||||
Rectangle rect = getInnerRect();
|
||||
log.debug("Drawing menu bar to {}", rect);
|
||||
Writer writer1 = screen.writerBuilder().build();
|
||||
Writer writer2 = screen.writerBuilder().style(ScreenItem.STYLE_BOLD).build();
|
||||
int x = rect.x();
|
||||
ListIterator<MenuBarItem> iter = items.listIterator();
|
||||
while (iter.hasNext()) {
|
||||
MenuBarItem item = iter.next();
|
||||
int index = iter.previousIndex();
|
||||
Writer writer = activeItemIndex == index ? writer2 : writer1;
|
||||
String text = String.format(" %s%s", item.getTitle(), iter.hasNext() ? " " : "");
|
||||
writer.text(text, x, rect.y());
|
||||
x += text.length();
|
||||
}
|
||||
if (currentMenuView != null) {
|
||||
currentMenuView.draw(screen);
|
||||
}
|
||||
super.drawInternal(screen);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initInternal() {
|
||||
registerKeyBinding(Key.CursorLeft, () -> left());
|
||||
registerKeyBinding(Key.CursorRight, () -> right());
|
||||
|
||||
registerMouseBinding(MouseEvent.Type.Released | MouseEvent.Button.Button1, event -> select(event));
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyHandler getKeyHandler() {
|
||||
// check if possible menuview handles an event
|
||||
KeyHandler handler = currentMenuView != null ? currentMenuView.getKeyHandler() : KeyHandler.neverConsume();
|
||||
return handler.thenIfNotConsumed(super.getKeyHandler());
|
||||
}
|
||||
|
||||
@Override
|
||||
public MouseHandler getMouseHandler() {
|
||||
// check if possible menuview handles an event
|
||||
MouseHandler handler = currentMenuView != null ? currentMenuView.getMouseHandler()
|
||||
: MouseHandler.neverConsume();
|
||||
return handler.thenIfNotConsumed(super.getMouseHandler());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a menubar items.
|
||||
*
|
||||
* @return menubar items
|
||||
*/
|
||||
public List<MenuBarItem> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a selected index. If given index is not within bounds of size of items,
|
||||
* selection is set to {@code -1} effectively un-selecting active item.
|
||||
*
|
||||
* @param index the new index
|
||||
*/
|
||||
public void setSelected(int index) {
|
||||
if (index >= items.size() || index < 0) {
|
||||
activeItemIndex = -1;
|
||||
}
|
||||
else {
|
||||
activeItemIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
private void left() {
|
||||
if (activeItemIndex > 0) {
|
||||
setSelected(activeItemIndex - 1);
|
||||
checkMenuView();
|
||||
}
|
||||
}
|
||||
|
||||
private void right() {
|
||||
if (activeItemIndex + 1 < items.size()) {
|
||||
setSelected(activeItemIndex + 1);
|
||||
checkMenuView();
|
||||
}
|
||||
}
|
||||
|
||||
private int indexAtPosition(int x, int y) {
|
||||
Rectangle rect = getRect();
|
||||
if (!rect.contains(x, y)) {
|
||||
return -1;
|
||||
}
|
||||
int i = 0;
|
||||
int p = 1;
|
||||
for (MenuBarItem item : items) {
|
||||
p += item.getTitle().length() + 1;
|
||||
if (x < p) {
|
||||
return i;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int positionAtIndex(int index) {
|
||||
int i = 0;
|
||||
int x = 1;
|
||||
for (MenuBarItem item : items) {
|
||||
if (i == index) {
|
||||
return x;
|
||||
}
|
||||
x += item.getTitle().length() + 1;
|
||||
i++;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
private void select(MouseEvent event) {
|
||||
int x = event.x();
|
||||
int y = event.y();
|
||||
int i = indexAtPosition(x, y);
|
||||
if (i > -1) {
|
||||
if (i == activeItemIndex) {
|
||||
setSelected(-1);
|
||||
}
|
||||
else {
|
||||
setSelected(i);
|
||||
}
|
||||
}
|
||||
checkMenuView();
|
||||
}
|
||||
|
||||
private void checkMenuView() {
|
||||
if (activeItemIndex < 0) {
|
||||
closeCurrentMenuView();
|
||||
}
|
||||
else {
|
||||
MenuBarItem item = items.get(activeItemIndex);
|
||||
currentMenuView = buildMenuView(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void closeCurrentMenuView() {
|
||||
if (currentMenuView != null) {
|
||||
currentMenuView.destroy();
|
||||
}
|
||||
currentMenuView = null;
|
||||
}
|
||||
|
||||
private MenuView buildMenuView(MenuBarItem item) {
|
||||
MenuView menuView = new MenuView(item.getItems());
|
||||
menuView.setEventLoop(getEventLoop());
|
||||
menuView.setShowBorder(true);
|
||||
menuView.setBackgroundColor(null);
|
||||
menuView.setLayer(1);
|
||||
Rectangle rect = getInnerRect();
|
||||
int x = positionAtIndex(activeItemIndex);
|
||||
Dimension dim = menuView.getPreferredDimension();
|
||||
menuView.setRect(rect.x() + x, rect.y() + 1, dim.width(), dim.height());
|
||||
menuView.onDestroy(getEventLoop().viewEvents(MenuViewOpenSelectedItemEvent.class, menuView)
|
||||
.subscribe(event -> {
|
||||
closeCurrentMenuView();
|
||||
}));
|
||||
|
||||
return menuView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets items.
|
||||
*
|
||||
* @param items status items
|
||||
*/
|
||||
public void setItems(List<MenuBarItem> items) {
|
||||
this.items.clear();
|
||||
this.items.addAll(items);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link MenuBarItem} represents an item in a {@link MenuBarView}.
|
||||
*/
|
||||
public static class MenuBarItem {
|
||||
|
||||
private String title;
|
||||
private List<MenuItem> items;
|
||||
|
||||
public MenuBarItem(String title) {
|
||||
this(title, null);
|
||||
}
|
||||
|
||||
public MenuBarItem(String title, MenuItem[] items) {
|
||||
this.title = title;
|
||||
this.items = Arrays.asList(items);
|
||||
}
|
||||
|
||||
public static MenuBarItem of(String title, MenuItem... items) {
|
||||
return new MenuBarItem(title, items);
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public List<MenuItem> getItems() {
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,474 @@
|
||||
/*
|
||||
* 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 java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.shell.component.view.event.KeyEvent.Key;
|
||||
import org.springframework.shell.component.view.event.MouseEvent;
|
||||
import org.springframework.shell.component.view.geom.Dimension;
|
||||
import org.springframework.shell.component.view.geom.Rectangle;
|
||||
import org.springframework.shell.component.view.message.ShellMessageBuilder;
|
||||
import org.springframework.shell.component.view.screen.Screen;
|
||||
import org.springframework.shell.component.view.screen.Screen.Writer;
|
||||
import org.springframework.shell.component.view.screen.ScreenItem;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@link MenuView} shows {@link MenuView} items vertically and is
|
||||
* typically used in layouts which builds complete terminal UI's.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class MenuView extends BoxView {
|
||||
|
||||
private final Logger log = LoggerFactory.getLogger(MenuView.class);
|
||||
private final List<MenuItem> items = new ArrayList<>();
|
||||
private int activeItemIndex = -1;
|
||||
|
||||
/**
|
||||
* Construct menu view with no initial menu items.
|
||||
*/
|
||||
public MenuView() {
|
||||
this(new MenuItem[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct menu view with menu items.
|
||||
*
|
||||
* @param items the menu items
|
||||
*/
|
||||
public MenuView(MenuItem[] items) {
|
||||
this(items != null ? Arrays.asList(items) : Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct menu view with menu items.
|
||||
*
|
||||
* @param items the menu items
|
||||
*/
|
||||
public MenuView(@Nullable List<MenuItem> items) {
|
||||
setItems(items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new menu items. Will always clear existing items and if {@code null}
|
||||
* is passed this effectively keeps items empty.
|
||||
*
|
||||
* @param items the menu items
|
||||
*/
|
||||
public void setItems(@Nullable List<MenuItem> items) {
|
||||
this.items.clear();
|
||||
activeItemIndex = -1;
|
||||
if (items != null) {
|
||||
this.items.addAll(items);
|
||||
if (!items.isEmpty()) {
|
||||
activeItemIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a menu items.
|
||||
*
|
||||
* @return the menu items
|
||||
*/
|
||||
public List<MenuItem> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a preferred dimension menu needs to show it's content.
|
||||
*
|
||||
* @return preferred dimension
|
||||
*/
|
||||
public Dimension getPreferredDimension() {
|
||||
int width = 0;
|
||||
int height = items.size();
|
||||
if (isShowBorder()) {
|
||||
height += 2;
|
||||
}
|
||||
for (MenuItem item : items) {
|
||||
int l = item.getTitle().length();
|
||||
if (item.getCheckStyle() != MenuItemCheckStyle.CHECKED) {
|
||||
l += 4;
|
||||
if (isShowBorder()) {
|
||||
l += 2;
|
||||
}
|
||||
}
|
||||
width = Math.max(width, l);
|
||||
}
|
||||
return new Dimension(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawInternal(Screen screen) {
|
||||
Rectangle rect = getInnerRect();
|
||||
int y = rect.y();
|
||||
Writer writer = screen.writerBuilder().layer(getLayer()).build();
|
||||
Writer writer2 = screen.writerBuilder().layer(getLayer()).style(ScreenItem.STYLE_BOLD).build();
|
||||
int i = 0;
|
||||
boolean hasCheck = false;
|
||||
for (MenuItem item : items) {
|
||||
if (item.getCheckStyle() != MenuItemCheckStyle.NOCHECK) {
|
||||
hasCheck = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (MenuItem item : items) {
|
||||
String prefix = hasCheck
|
||||
? (item.getCheckStyle() != MenuItemCheckStyle.NOCHECK
|
||||
? (item.isChecked() ? "[x] " : "[ ] ")
|
||||
: " ")
|
||||
: "";
|
||||
String text = prefix + item.getTitle();
|
||||
if (activeItemIndex == i) {
|
||||
writer2.text(text, rect.x(), y);
|
||||
}
|
||||
else {
|
||||
writer.text(text, rect.x(), y);
|
||||
}
|
||||
i++;
|
||||
y++;
|
||||
}
|
||||
super.drawInternal(screen);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initInternal() {
|
||||
registerKeyBinding(Key.CursorUp, () -> move(-1));
|
||||
registerKeyBinding(Key.CursorDown, () -> move(1));
|
||||
registerKeyBinding(Key.Enter, () -> keySelect());
|
||||
|
||||
registerMouseBinding(MouseEvent.Type.Released | MouseEvent.Button.Button1, event -> mouseSelect(event));
|
||||
registerMouseBinding(MouseEvent.Type.Wheel | MouseEvent.Button.WheelDown, () -> move(1));
|
||||
registerMouseBinding(MouseEvent.Type.Wheel | MouseEvent.Button.WheelUp, () -> move(-1));
|
||||
}
|
||||
|
||||
private void keySelect() {
|
||||
MenuItem item = items.get(activeItemIndex);
|
||||
dispatch(ShellMessageBuilder.ofView(this, MenuViewOpenSelectedItemEvent.of(this, item)));
|
||||
if (item.getAction() != null) {
|
||||
dispatchRunnable(item.getAction());
|
||||
}
|
||||
}
|
||||
|
||||
private void move(int count) {
|
||||
log.trace("move({})", count);
|
||||
setSelected(activeItemIndex + count);
|
||||
}
|
||||
|
||||
private void setSelected(int index) {
|
||||
if (index >= items.size()) {
|
||||
activeItemIndex = 0;
|
||||
}
|
||||
else if(index < 0) {
|
||||
activeItemIndex = items.size() - 1;
|
||||
}
|
||||
else {
|
||||
if (activeItemIndex != index) {
|
||||
activeItemIndex = index;
|
||||
MenuItem item = items.get(index);
|
||||
dispatch(ShellMessageBuilder.ofView(this, MenuViewSelectedItemChangedEvent.of(this, item)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void mouseSelect(MouseEvent event) {
|
||||
log.trace("select({})", event);
|
||||
int x = event.x();
|
||||
int y = event.y();
|
||||
setSelected(indexAtPosition(x, y));
|
||||
keySelect();
|
||||
}
|
||||
|
||||
private int indexAtPosition(int x, int y) {
|
||||
Rectangle rect = getRect();
|
||||
if (!rect.contains(x, y)) {
|
||||
return -1;
|
||||
}
|
||||
int pos = y - rect.y() - 1;
|
||||
if (pos > -1 && pos < items.size()) {
|
||||
MenuItem i = items.get(pos);
|
||||
if (i != null) {
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies how a {@link MenuItem} shows selection state.
|
||||
*/
|
||||
public enum MenuItemCheckStyle {
|
||||
|
||||
/**
|
||||
* The menu item will be shown normally, with no check indicator. The default.
|
||||
*/
|
||||
NOCHECK,
|
||||
|
||||
/**
|
||||
* The menu item will indicate checked/un-checked state.
|
||||
*/
|
||||
CHECKED,
|
||||
|
||||
/**
|
||||
* The menu item is part of a menu radio group and will indicate selected state.
|
||||
*/
|
||||
RADIO
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link MenuItem} represents an item in a {@link MenuView}.
|
||||
*
|
||||
* @see Menu
|
||||
*/
|
||||
public static class MenuItem {
|
||||
|
||||
private final String title;
|
||||
private final MenuItemCheckStyle checkStyle;
|
||||
private final List<MenuItem> items;
|
||||
private boolean checked;
|
||||
private Runnable action;
|
||||
|
||||
/**
|
||||
* Construct menu item with a title.
|
||||
*
|
||||
* @param title the title
|
||||
*/
|
||||
public MenuItem(String title) {
|
||||
this(title, MenuItemCheckStyle.NOCHECK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct menu item with a title and a check style.
|
||||
*
|
||||
* @param title the title
|
||||
* @param checkStyle the check style
|
||||
*/
|
||||
public MenuItem(String title, MenuItemCheckStyle checkStyle) {
|
||||
this(title, checkStyle, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct menu item with a title and a check style.
|
||||
*
|
||||
* @param title the title
|
||||
* @param checkStyle the check style
|
||||
* @param action the action to run when item is chosen
|
||||
*/
|
||||
public MenuItem(String title, MenuItemCheckStyle checkStyle, Runnable action) {
|
||||
Assert.state(StringUtils.hasText(title), "title must have text");
|
||||
Assert.notNull(checkStyle, "check style cannot be null");
|
||||
this.title = title;
|
||||
this.checkStyle = checkStyle;
|
||||
this.action = action;
|
||||
this.items = null;
|
||||
}
|
||||
|
||||
protected MenuItem(String title, MenuItem[] items) {
|
||||
this(title, Arrays.asList(items));
|
||||
}
|
||||
|
||||
protected MenuItem(String title, List<MenuItem> items) {
|
||||
Assert.state(StringUtils.hasText(title), "title must have text");
|
||||
Assert.notNull(items, "Sub items cannot be null");
|
||||
this.title = title;
|
||||
this.items = items;
|
||||
this.checkStyle = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link MenuItem} with a given {@code title}.
|
||||
*
|
||||
* @param title the title
|
||||
* @return a menu item
|
||||
*/
|
||||
public static MenuItem of(String title) {
|
||||
return new MenuItem(title);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link MenuItem} with a given {@code title} and a
|
||||
* {@code check style}.
|
||||
*
|
||||
* @param title the title
|
||||
* @param checkStyle the check style
|
||||
* @return a menu item
|
||||
*/
|
||||
public static MenuItem of(String title, MenuItemCheckStyle checkStyle) {
|
||||
return new MenuItem(title, checkStyle);
|
||||
}
|
||||
|
||||
public static MenuItem of(String title, MenuItemCheckStyle checkStyle, Runnable action) {
|
||||
return new MenuItem(title, checkStyle, action);
|
||||
}
|
||||
|
||||
public Runnable getAction() {
|
||||
return action;
|
||||
}
|
||||
|
||||
public void setAction(Runnable action) {
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@code title}. Never null, empty or just having white spaces.
|
||||
*
|
||||
* @return a title
|
||||
*/
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a check style. This will be {@code null} if constructed via
|
||||
* {@link Menu} as style doesn't apply items being a sub-menu.
|
||||
*
|
||||
* @return a check style
|
||||
*/
|
||||
@Nullable
|
||||
public MenuItemCheckStyle getCheckStyle() {
|
||||
return checkStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a checked state.
|
||||
*
|
||||
* @param checked checked state
|
||||
*/
|
||||
public void setChecked(boolean checked) {
|
||||
this.checked = checked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a checked state.
|
||||
*
|
||||
* @return checked state
|
||||
*/
|
||||
public boolean isChecked() {
|
||||
return checked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets sub menu items. This will be {@code null} if not constructed via
|
||||
* {@link Menu} as plain {@link MenuItem} can't have other items.
|
||||
*
|
||||
* @return a menu items
|
||||
*/
|
||||
@Nullable
|
||||
public List<MenuItem> getItems() {
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Menu} represents an item in a {@link MenuView} being a specialisation
|
||||
* of {@link MenuItem} indicating it having a sub-menu.
|
||||
*
|
||||
* @see MenuItem
|
||||
*/
|
||||
public static class Menu extends MenuItem {
|
||||
|
||||
/**
|
||||
* Construct menu with a title.
|
||||
*
|
||||
* @param title the title
|
||||
*/
|
||||
public Menu(String title) {
|
||||
super(title);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct menu with a title and a menu items.
|
||||
*
|
||||
* @param title the title
|
||||
* @param items the menu items
|
||||
*/
|
||||
public Menu(String title, MenuItem[] items) {
|
||||
super(title, items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct menu with a title and a menu items.
|
||||
*
|
||||
* @param title the title
|
||||
* @param items the menu items
|
||||
*/
|
||||
public Menu(String title, List<MenuItem> items) {
|
||||
super(title, items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link Menu} with a given {@code title} and {@link MenuItem}s.
|
||||
*
|
||||
* @param title the title
|
||||
* @param items the menu items
|
||||
* @return a menu
|
||||
*/
|
||||
public static Menu of(String title, MenuItem... items) {
|
||||
return new Menu(title, items);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ViewEventArgs} for {@link MenuViewOpenSelectedItemEvent} and
|
||||
* {@link MenuViewSelectedItemChangedEvent}.
|
||||
*
|
||||
* @param item the menu view item
|
||||
*/
|
||||
public record MenuViewItemEventArgs(MenuItem item) implements ViewEventArgs {
|
||||
|
||||
public static MenuViewItemEventArgs of(MenuItem item) {
|
||||
return new MenuViewItemEventArgs(item);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ViewEvent} indicating that selected item has been requested to open.
|
||||
*
|
||||
* @param view the view sending an event
|
||||
* @param args the event args
|
||||
*/
|
||||
public record MenuViewOpenSelectedItemEvent(View view, MenuViewItemEventArgs args) implements ViewEvent {
|
||||
|
||||
public static MenuViewOpenSelectedItemEvent of(View view, MenuItem item) {
|
||||
return new MenuViewOpenSelectedItemEvent(view, MenuViewItemEventArgs.of(item));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ViewEvent} indicating that selected item has changed.
|
||||
*
|
||||
* @param view the view sending an event
|
||||
* @param args the event args
|
||||
*/
|
||||
public record MenuViewSelectedItemChangedEvent(View view, MenuViewItemEventArgs args) implements ViewEvent {
|
||||
|
||||
public static MenuViewSelectedItemChangedEvent of(View view, MenuItem item) {
|
||||
return new MenuViewSelectedItemChangedEvent(view, MenuViewItemEventArgs.of(item));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* 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 java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.shell.component.view.event.MouseEvent;
|
||||
import org.springframework.shell.component.view.event.MouseHandler;
|
||||
import org.springframework.shell.component.view.geom.Rectangle;
|
||||
import org.springframework.shell.component.view.message.ShellMessageBuilder;
|
||||
import org.springframework.shell.component.view.screen.Screen;
|
||||
import org.springframework.shell.component.view.screen.Screen.Writer;
|
||||
|
||||
/**
|
||||
* {@link StatusBarView} shows {@link StatusItem items} horizontally and is
|
||||
* typically used in layouts which builds complete terminal UI's.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class StatusBarView extends BoxView {
|
||||
|
||||
private final Logger log = LoggerFactory.getLogger(StatusBarView.class);
|
||||
private final List<StatusItem> items = new ArrayList<>();
|
||||
|
||||
public StatusBarView() {
|
||||
this(new StatusItem[0]);
|
||||
}
|
||||
|
||||
public StatusBarView(StatusItem[] items) {
|
||||
this(Arrays.asList(items));
|
||||
}
|
||||
|
||||
public StatusBarView(List<StatusItem> items) {
|
||||
setItems(items);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawInternal(Screen screen) {
|
||||
Rectangle rect = getInnerRect();
|
||||
log.debug("Drawing status bar to {}", rect);
|
||||
Writer writer = screen.writerBuilder().build();
|
||||
int x = rect.x();
|
||||
ListIterator<StatusItem> iter = items.listIterator();
|
||||
while (iter.hasNext()) {
|
||||
StatusItem item = iter.next();
|
||||
String text = String.format(" %s%s", item.getTitle(), iter.hasNext() ? " |" : "");
|
||||
writer.text(text, x, rect.y());
|
||||
x += text.length();
|
||||
}
|
||||
super.drawInternal(screen);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MouseHandler getMouseHandler() {
|
||||
log.trace("getMouseHandler()");
|
||||
return args -> {
|
||||
MouseEvent event = args.event();
|
||||
boolean consumed = false;
|
||||
if (!event.hasModifier() && event.has(MouseEvent.Type.Released) && event.has(MouseEvent.Button.Button1)) {
|
||||
int x = event.x();
|
||||
int y = event.y();
|
||||
StatusItem item = itemAt(x, y);
|
||||
if (item != null) {
|
||||
dispatch(ShellMessageBuilder.ofView(this, StatusBarViewOpenSelectedItemEvent.of(this, item)));
|
||||
if (item.getAction() != null) {
|
||||
dispatchRunnable(item.getAction());
|
||||
}
|
||||
}
|
||||
consumed = true;
|
||||
}
|
||||
// status bar don't request focus
|
||||
return MouseHandler.resultOf(args.event(), consumed, null, null);
|
||||
};
|
||||
}
|
||||
|
||||
private StatusItem itemAt(int x, int y) {
|
||||
Rectangle rect = getRect();
|
||||
if (!rect.contains(x, y)) {
|
||||
return null;
|
||||
}
|
||||
int ix = 0;
|
||||
for (StatusItem item : items) {
|
||||
if (x < ix + item.getTitle().length()) {
|
||||
return item;
|
||||
}
|
||||
ix += item.getTitle().length();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets items.
|
||||
*
|
||||
* @param items status items
|
||||
*/
|
||||
public void setItems(List<StatusItem> items) {
|
||||
this.items.clear();
|
||||
this.items.addAll(items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a status items.
|
||||
*
|
||||
* @return the status items
|
||||
*/
|
||||
public List<StatusItem> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link StatusItem} represents an item in a {@link StatusBarView}.
|
||||
*/
|
||||
public static class StatusItem {
|
||||
|
||||
private String title;
|
||||
private Runnable action;
|
||||
|
||||
public StatusItem(String title) {
|
||||
this(title, null);
|
||||
}
|
||||
|
||||
public StatusItem(String title, Runnable action) {
|
||||
this.title = title;
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public Runnable getAction() {
|
||||
return action;
|
||||
}
|
||||
|
||||
public void setAction(Runnable action) {
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ViewEventArgs} for {@link StatusBarViewOpenSelectedItemEvent}.
|
||||
*
|
||||
* @param item the status bar view item
|
||||
*/
|
||||
public record StatusBarViewItemEventArgs(StatusItem item) implements ViewEventArgs {
|
||||
|
||||
public static StatusBarViewItemEventArgs of(StatusItem item) {
|
||||
return new StatusBarViewItemEventArgs(item);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ViewEvent} indicating that selected item has been requested to open.
|
||||
*
|
||||
* @param view the view sending an event
|
||||
* @param args the event args
|
||||
*/
|
||||
public record StatusBarViewOpenSelectedItemEvent(View view, StatusBarViewItemEventArgs args) implements ViewEvent {
|
||||
|
||||
public static StatusBarViewOpenSelectedItemEvent of(View view, StatusItem item) {
|
||||
return new StatusBarViewOpenSelectedItemEvent(view, StatusBarViewItemEventArgs.of(item));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.springframework.lang.Nullable;
|
||||
import org.springframework.shell.component.view.event.KeyHandler;
|
||||
import org.springframework.shell.component.view.event.MouseHandler;
|
||||
|
||||
/**
|
||||
* Base interface for all views. Represents a visible element that can render
|
||||
* itself and contains zero or more nested {@code Views}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface View extends Control {
|
||||
|
||||
/**
|
||||
* Sets a layer index this {@code View} operates on.
|
||||
*
|
||||
* @param index the layer index
|
||||
*/
|
||||
void setLayer(int index);
|
||||
|
||||
/**
|
||||
* Called when {@code View} gets or loses a focus.
|
||||
*
|
||||
* @param view the view receiving focus
|
||||
* @param focus flag if focus is received
|
||||
*/
|
||||
void focus(View view, boolean focus);
|
||||
|
||||
/**
|
||||
* Gets if this {@code View} has a focus.
|
||||
*
|
||||
* @return true if view has a focus
|
||||
*/
|
||||
boolean hasFocus();
|
||||
|
||||
/**
|
||||
* Gets a {@link View} mouse {@link MouseHandler}. Can be {@code null} which
|
||||
* indicates view will not handle any mouse events.
|
||||
*
|
||||
* @return a view mouse handler
|
||||
* @see MouseHandler
|
||||
*/
|
||||
@Nullable
|
||||
MouseHandler getMouseHandler();
|
||||
|
||||
/**
|
||||
* Gets a {@link View} mouse {@link KeyHandler}. Can be {@code null} which
|
||||
* indicates view will not handle any key events.
|
||||
*
|
||||
* @return a view mouse handler
|
||||
* @see KeyHandler
|
||||
*/
|
||||
@Nullable
|
||||
KeyHandler getKeyHandler();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Commands which can be performed by the application or bound to keys in a
|
||||
* {@link View}. This class is a placeholder for constants of types usually
|
||||
* needed in a views. We've chosen this not to be enumerable so that there
|
||||
* would not be restrictions in an api's to use these types.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public final class ViewCommand {
|
||||
|
||||
/**
|
||||
* Move line up. Generic use in something where selection needs to be moved up.
|
||||
*/
|
||||
public static String LINE_UP = "LineUp";
|
||||
|
||||
/**
|
||||
* Move line down. Generic use in something where selection needs to be moved
|
||||
* down.
|
||||
*/
|
||||
public static String LINE_DOWN = "LineDown";
|
||||
|
||||
/**
|
||||
* Open selected item. In a some sort of view where something can be selected
|
||||
* and that active selected should be opened.
|
||||
*/
|
||||
public static String OPEN_SELECTED_ITEM = "OpenSelectedItem";
|
||||
|
||||
public static String SELECT = "Select";
|
||||
public static String LEFT = "Left";
|
||||
public static String RIGHT = "Right";
|
||||
public static String SELECTION_CHANGED = "SelectionChanged";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Base interface for view events.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface ViewEvent {
|
||||
|
||||
View view();
|
||||
|
||||
default ViewEventArgs args() {
|
||||
return ViewEventArgs.EMPTY;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Base interface for view event args.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface ViewEventArgs {
|
||||
|
||||
public static final ViewEventArgs EMPTY = new ViewEventArgs() {
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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.cell;
|
||||
|
||||
import org.springframework.shell.component.view.control.Cell;
|
||||
|
||||
public abstract class AbstractCell<T> extends AbstractControl implements Cell<T> {
|
||||
|
||||
private boolean selected;
|
||||
private T item;
|
||||
private int style = -1;
|
||||
private int foregroundColor = -1;
|
||||
private int backgroundColor = -1;
|
||||
|
||||
@Override
|
||||
public T getItem() {
|
||||
return item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItem(T item) {
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSelected() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSelected(boolean selected) {
|
||||
this.selected = selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStyle(int style) {
|
||||
this.style = style;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setForegroundColor(int foregroundColor) {
|
||||
this.foregroundColor = foregroundColor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBackgroundColor(int backgroundColor) {
|
||||
this.backgroundColor = backgroundColor;
|
||||
}
|
||||
|
||||
public int getStyle() {
|
||||
return style;
|
||||
}
|
||||
|
||||
public int getForegroundColor() {
|
||||
return foregroundColor;
|
||||
}
|
||||
|
||||
public int getBackgroundColor() {
|
||||
return backgroundColor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.cell;
|
||||
|
||||
import org.springframework.shell.component.view.control.Control;
|
||||
import org.springframework.shell.component.view.geom.Rectangle;
|
||||
|
||||
/**
|
||||
* Base implementation of a {@link Control}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public abstract class AbstractControl implements Control {
|
||||
|
||||
private int x = 0;
|
||||
private int y = 0;
|
||||
private int width = 0;
|
||||
private int height = 0;
|
||||
|
||||
@Override
|
||||
public void setRect(int x, int y, int width, int height) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Rectangle getRect() {
|
||||
return new Rectangle(x, y, width, height);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.cell;
|
||||
|
||||
import org.springframework.shell.component.view.control.Cell;
|
||||
import org.springframework.shell.component.view.control.ListView;
|
||||
import org.springframework.shell.component.view.geom.Rectangle;
|
||||
import org.springframework.shell.component.view.screen.Screen;
|
||||
import org.springframework.shell.component.view.screen.Screen.Writer;
|
||||
|
||||
/**
|
||||
* The {@link Cell} type used within {@link ListView} instances
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class ListCell<T> extends AbstractCell<T> {
|
||||
|
||||
protected String text;
|
||||
|
||||
@Override
|
||||
public void draw(Screen screen) {
|
||||
Rectangle rect = getRect();
|
||||
Writer writer = screen.writerBuilder().style(getStyle()).color(getForegroundColor()).build();
|
||||
writer.text(text, rect.x(), rect.y());
|
||||
writer.background(rect, getBackgroundColor());
|
||||
}
|
||||
|
||||
public void updateItem(T item) {
|
||||
setItem(item);
|
||||
this.text = item.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
/*
|
||||
* 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.event;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.PriorityQueue;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import reactor.core.Disposable;
|
||||
import reactor.core.Disposables;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.Sinks;
|
||||
import reactor.core.publisher.Sinks.Many;
|
||||
import reactor.core.scheduler.Scheduler;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.shell.component.view.control.View;
|
||||
import org.springframework.shell.component.view.control.ViewEvent;
|
||||
import org.springframework.shell.component.view.event.processor.AnimationEventLoopProcessor;
|
||||
import org.springframework.shell.component.view.event.processor.TaskEventLoopProcessor;
|
||||
import org.springframework.shell.component.view.message.ShellMessageHeaderAccessor;
|
||||
import org.springframework.shell.component.view.message.StaticShellMessageHeaderAccessor;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Default implementation of an {@link EventLoop}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class DefaultEventLoop implements EventLoop {
|
||||
|
||||
private final static Logger log = LoggerFactory.getLogger(DefaultEventLoop.class);
|
||||
private final Queue<Message<?>> messageQueue = new PriorityQueue<>(MessageComparator.comparingPriority());
|
||||
private final Many<Message<?>> many = Sinks.many().unicast().onBackpressureBuffer(messageQueue);
|
||||
private Flux<Message<?>> sink;
|
||||
// private final Sinks.Many<Boolean> subscribedSignal = Sinks.many().replay().limit(1);
|
||||
private final Disposable.Composite disposables = Disposables.composite();
|
||||
private final Scheduler scheduler = Schedulers.boundedElastic();
|
||||
private volatile boolean active = true;
|
||||
private final List<EventLoopProcessor> processors;
|
||||
|
||||
public DefaultEventLoop() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public DefaultEventLoop(List<EventLoopProcessor> processors) {
|
||||
this.processors = new ArrayList<>();
|
||||
if (processors != null) {
|
||||
this.processors.addAll(processors);
|
||||
}
|
||||
this.processors.add(new AnimationEventLoopProcessor());
|
||||
this.processors.add(new TaskEventLoopProcessor());
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
sink = many.asFlux()
|
||||
.flatMap(m -> {
|
||||
Flux<? extends Message<?>> pm = null;
|
||||
for (EventLoopProcessor processor : processors) {
|
||||
if (processor.canProcess(m)) {
|
||||
pm = processor.process(m);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (pm != null) {
|
||||
return pm;
|
||||
}
|
||||
return Mono.just(m);
|
||||
})
|
||||
.share();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispatch(Message<?> message) {
|
||||
log.debug("dispatch {}", message);
|
||||
if (!doSend(message, 1000)) {
|
||||
log.warn("Failed to send message: {}", message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispatch(Publisher<? extends Message<?>> messages) {
|
||||
subscribeTo(messages);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<Message<?>> events() {
|
||||
return sink
|
||||
// .doFinally((s) -> subscribedSignal.tryEmitNext(sink.currentSubscriberCount() > 0))
|
||||
;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Flux<T> events(EventLoop.Type type, Class<T> clazz) {
|
||||
return events()
|
||||
.filter(m -> type.equals(StaticShellMessageHeaderAccessor.getEventType(m)))
|
||||
.map(m -> m.getPayload())
|
||||
.ofType(clazz);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> Flux<T> events(Type type, ParameterizedTypeReference<T> typeRef) {
|
||||
ResolvableType resolvableType = ResolvableType.forType(typeRef);
|
||||
return (Flux<T>) events()
|
||||
.filter(m -> type.equals(StaticShellMessageHeaderAccessor.getEventType(m)))
|
||||
.map(m -> m.getPayload())
|
||||
.ofType(resolvableType.getRawClass());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<KeyEvent> keyEvents() {
|
||||
return events()
|
||||
.filter(m -> EventLoop.Type.KEY.equals(StaticShellMessageHeaderAccessor.getEventType(m)))
|
||||
.map(m -> m.getPayload())
|
||||
.ofType(KeyEvent.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<MouseEvent> mouseEvents() {
|
||||
return events()
|
||||
.filter(m -> EventLoop.Type.MOUSE.equals(StaticShellMessageHeaderAccessor.getEventType(m)))
|
||||
.map(m -> m.getPayload())
|
||||
.ofType(MouseEvent.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends ViewEvent> Flux<T> viewEvents(Class<T> clazz) {
|
||||
return events(EventLoop.Type.VIEW, clazz);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends ViewEvent> Flux<T> viewEvents(ParameterizedTypeReference<T> typeRef) {
|
||||
return events(EventLoop.Type.VIEW, typeRef);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends ViewEvent> Flux<T> viewEvents(Class<T> clazz, View filterBy) {
|
||||
return events(EventLoop.Type.VIEW, clazz).filter(args -> args.view() == filterBy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends ViewEvent> Flux<T> viewEvents(ParameterizedTypeReference<T> typeRef, View filterBy) {
|
||||
return events(EventLoop.Type.VIEW, typeRef).filter(args -> args.view() == filterBy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(Disposable disposable) {
|
||||
disposables.add(disposable);
|
||||
}
|
||||
|
||||
|
||||
// @Override
|
||||
// public void subcribe(Flux<? extends Message<?>> messages) {
|
||||
// upstreamSubscriptions.add(
|
||||
// messages
|
||||
// // .delaySubscription(subscribedSignal.asFlux().filter(Boolean::booleanValue).next())
|
||||
// .subscribe()
|
||||
// );
|
||||
// }
|
||||
|
||||
private boolean doSend(Message<?> message, long timeout) {
|
||||
Assert.state(this.active && this.many.currentSubscriberCount() > 0,
|
||||
() -> "The [" + this + "] doesn't have subscribers to accept messages");
|
||||
long remainingTime = 0;
|
||||
if (timeout > 0) {
|
||||
remainingTime = timeout;
|
||||
}
|
||||
long parkTimeout = 10;
|
||||
long parkTimeoutNs = TimeUnit.MILLISECONDS.toNanos(parkTimeout);
|
||||
while (this.active && !tryEmitMessage(message)) {
|
||||
remainingTime -= parkTimeout;
|
||||
if (timeout >= 0 && remainingTime <= 0) {
|
||||
return false;
|
||||
}
|
||||
LockSupport.parkNanos(parkTimeoutNs);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean tryEmitMessage(Message<?> message) {
|
||||
return switch (many.tryEmitNext(message)) {
|
||||
case OK -> true;
|
||||
case FAIL_NON_SERIALIZED, FAIL_OVERFLOW -> false;
|
||||
case FAIL_ZERO_SUBSCRIBER ->
|
||||
throw new IllegalStateException("The [" + this + "] doesn't have subscribers to accept messages");
|
||||
case FAIL_TERMINATED, FAIL_CANCELLED ->
|
||||
throw new IllegalStateException("Cannot emit messages into the cancelled or terminated sink: " + many);
|
||||
};
|
||||
}
|
||||
|
||||
// public void subscribe(Subscriber<? super Message<?>> subscriber) {
|
||||
// sink.asFlux()
|
||||
// .doFinally((s) -> subscribedSignal.tryEmitNext(sink.currentSubscriberCount() > 0))
|
||||
// .share()
|
||||
// .subscribe(subscriber);
|
||||
|
||||
// upstreamSubscriptions.add(
|
||||
// Mono.fromCallable(() -> sink.currentSubscriberCount() > 0)
|
||||
// .filter(Boolean::booleanValue)
|
||||
// .doOnNext(subscribedSignal::tryEmitNext)
|
||||
// .repeatWhenEmpty((repeat) ->
|
||||
// active ? repeat.delayElements(Duration.ofMillis(100)) : repeat)
|
||||
// .subscribe());
|
||||
// }
|
||||
|
||||
public void subscribeTo(Publisher<? extends Message<?>> publisher) {
|
||||
disposables.add(
|
||||
Flux.from(publisher)
|
||||
// .delaySubscription(subscribedSignal.asFlux().filter(Boolean::booleanValue).next())
|
||||
.publishOn(scheduler)
|
||||
.flatMap((message) ->
|
||||
Mono.just(message)
|
||||
.handle((messageToHandle, syncSink) -> sendReactiveMessage(messageToHandle))
|
||||
.contextWrite(StaticShellMessageHeaderAccessor.getReactorContext(message))
|
||||
)
|
||||
.contextCapture()
|
||||
.subscribe());
|
||||
}
|
||||
|
||||
private void sendReactiveMessage(Message<?> message) {
|
||||
Message<?> messageToSend = message;
|
||||
// We have just restored Reactor context, so no need in a header anymore.
|
||||
if (messageToSend.getHeaders().containsKey(ShellMessageHeaderAccessor.REACTOR_CONTEXT)) {
|
||||
messageToSend =
|
||||
MessageBuilder.fromMessage(message)
|
||||
.removeHeader(ShellMessageHeaderAccessor.REACTOR_CONTEXT)
|
||||
.build();
|
||||
}
|
||||
try {
|
||||
dispatch(messageToSend);
|
||||
// if (!doSend(messageToSend, 1000)) {
|
||||
// log.warn("Failed to send message: {}", messageToSend);
|
||||
// }
|
||||
}
|
||||
catch (Exception ex) {
|
||||
log.warn("Error during processing event: {}", messageToSend);
|
||||
}
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
this.active = false;
|
||||
this.disposables.dispose();
|
||||
// this.subscribedSignal.emitComplete(Sinks.EmitFailureHandler.FAIL_FAST);
|
||||
this.many.emitComplete(Sinks.EmitFailureHandler.FAIL_FAST);
|
||||
this.scheduler.dispose();
|
||||
}
|
||||
|
||||
private static class MessageComparator implements Comparator<Message<?>> {
|
||||
|
||||
@Override
|
||||
public int compare(Message<?> left, Message<?> right) {
|
||||
Integer l = StaticShellMessageHeaderAccessor.getPriority(left);
|
||||
Integer r = StaticShellMessageHeaderAccessor.getPriority(right);
|
||||
if (l != null && r != null) {
|
||||
return l.compareTo(r);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Comparator<Message<?>> comparingPriority() {
|
||||
return new MessageComparator();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* 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.event;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.Disposable;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.shell.component.view.control.View;
|
||||
import org.springframework.shell.component.view.control.ViewEvent;
|
||||
|
||||
/**
|
||||
* {@code EventLoop} is a central place where all eventing will be orchestrated
|
||||
* for a lifecycle of a component. Orchestration is usually needed around timings
|
||||
* of redraws and and component state updates.
|
||||
*
|
||||
* Generic message type is a Spring {@link Message} and it's up to an {@code EventLoop}
|
||||
* implementation how those are processed.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface EventLoop {
|
||||
|
||||
/**
|
||||
* Return a {@link Flux} of {@link Message} events. When subscribed events will
|
||||
* be received until disposed or {@code EventLoop} terminates.
|
||||
*
|
||||
* @return the events from an event loop
|
||||
*/
|
||||
Flux<? extends Message<?>> events();
|
||||
|
||||
/**
|
||||
* Specialisation of {@link #events()} which returns type safe
|
||||
* {@link KeyEvent}s.
|
||||
*
|
||||
* @return the key events from an event loop
|
||||
*/
|
||||
Flux<KeyEvent> keyEvents();
|
||||
|
||||
/**
|
||||
* Specialisation of {@link #events()} which returns type safe
|
||||
* {@link MouseEvent}s.
|
||||
*
|
||||
* @return the mouse events from an event loop
|
||||
*/
|
||||
Flux<MouseEvent> mouseEvents();
|
||||
|
||||
/**
|
||||
* Specialisation of {@link #events()} which returns type safe {@link ViewEvent}s.
|
||||
*
|
||||
* @param <T> the type to expect
|
||||
* @param clazz the type class to filter
|
||||
* @return the filtered events from an event loop
|
||||
*/
|
||||
<T extends ViewEvent> Flux<T> viewEvents(Class<T> clazz);
|
||||
|
||||
/**
|
||||
* Specialisation of {@link #events()} which returns type safe {@link ViewEvent}s.
|
||||
*
|
||||
* @param <T> the type to expect
|
||||
* @param typeRef the parameterized type to filter
|
||||
* @return the filtered events from an event loop
|
||||
*/
|
||||
<T extends ViewEvent> Flux<T> viewEvents(ParameterizedTypeReference<T> typeRef);
|
||||
|
||||
/**
|
||||
* Specialisation of {@link #events()} which returns type safe {@link ViewEvent}s.
|
||||
*
|
||||
* @param <T> the type to expect
|
||||
* @param clazz the type class to filter
|
||||
* @param filterBy the view to filter
|
||||
* @return the filtered events from an event loop
|
||||
*/
|
||||
<T extends ViewEvent> Flux<T> viewEvents(Class<T> clazz, View filterBy);
|
||||
|
||||
/**
|
||||
* Specialisation of {@link #events()} which returns type safe {@link ViewEvent}s.
|
||||
*
|
||||
* @param <T> the type to expect
|
||||
* @param typeRef the parameterized type to filter
|
||||
* @param filterBy the view to filter
|
||||
* @return the filtered events from an event loop
|
||||
*/
|
||||
<T extends ViewEvent> Flux<T> viewEvents(ParameterizedTypeReference<T> typeRef, View filterBy);
|
||||
|
||||
/**
|
||||
* Specialisation of {@link #events()} which returns type safe
|
||||
* stream filtered by given eventloop message type and message
|
||||
* payload class type.
|
||||
*
|
||||
* @param <T> the type to expect
|
||||
* @param type the eventloop message type to filter
|
||||
* @param clazz the type class to filter
|
||||
* @return the filtered events from an event loop
|
||||
*/
|
||||
<T> Flux<T> events(EventLoop.Type type, Class<T> clazz);
|
||||
|
||||
/**
|
||||
* Specialisation of {@link #events()} which returns type safe
|
||||
* stream filtered by given eventloop message type and message
|
||||
* payload class type.
|
||||
*
|
||||
* @param <T> the type to expect
|
||||
* @param type the eventloop message type to filter
|
||||
* @param typeRef the parameterized type to filter
|
||||
* @return the filtered events from an event loop
|
||||
*/
|
||||
<T> Flux<T> events(EventLoop.Type type, ParameterizedTypeReference<T> typeRef);
|
||||
|
||||
/**
|
||||
* Dispatch {@link Message}s into an {@code EventLoop} from a {@link Publisher}.
|
||||
* Usually type is either {@link Mono} or {@link Flux}.
|
||||
*
|
||||
* @param messages the messages to dispatch
|
||||
*/
|
||||
void dispatch(Publisher<? extends Message<?>> messages);
|
||||
|
||||
/**
|
||||
* Dispatch a {@link Message} into an {@code EventLoop}.
|
||||
*
|
||||
* @param message the message to dispatch
|
||||
*/
|
||||
void dispatch(Message<?> message);
|
||||
|
||||
/**
|
||||
* Register {@link Disposable} to get disposed when event loop terminates.
|
||||
*
|
||||
* @param disposable a disposable to dispose
|
||||
*/
|
||||
void onDestroy(Disposable disposable);
|
||||
|
||||
/**
|
||||
* Type of an events handled by an {@code EventLoop}.
|
||||
*/
|
||||
enum Type {
|
||||
|
||||
/**
|
||||
* Signals dispatched from a terminal.
|
||||
*/
|
||||
SIGNAL,
|
||||
|
||||
/**
|
||||
* Key bindings from a terminal.
|
||||
*/
|
||||
KEY,
|
||||
|
||||
/**
|
||||
* Mouse bindings from a terminal.
|
||||
*/
|
||||
MOUSE,
|
||||
|
||||
/**
|
||||
* System bindinds like redraw.
|
||||
*/
|
||||
SYSTEM,
|
||||
|
||||
/**
|
||||
* View bindinds from views.
|
||||
*/
|
||||
VIEW,
|
||||
|
||||
/**
|
||||
* User bindinds for custom events.
|
||||
*/
|
||||
USER,
|
||||
|
||||
TASK
|
||||
}
|
||||
|
||||
/**
|
||||
* Contract to process event loop messages, possibly translating an event into
|
||||
* some other type of event or events.
|
||||
*/
|
||||
interface EventLoopProcessor {
|
||||
|
||||
/**
|
||||
* Checks if this processor can process an event. If this method returns {@code true}
|
||||
* it's quaranteed that {@link #process(Message)} is called to resolve translation
|
||||
* of a message.
|
||||
*
|
||||
* @param message the message
|
||||
* @return true if processor can process an event
|
||||
*/
|
||||
boolean canProcess(Message<?> message);
|
||||
|
||||
/**
|
||||
* Process a message and transform it into a new {@link Flux} of {@link Message}
|
||||
* instances.
|
||||
*
|
||||
* @param message the message to process
|
||||
* @return a flux of messages
|
||||
*/
|
||||
Flux<? extends Message<?>> process(Message<?> message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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.event;
|
||||
|
||||
import org.jline.keymap.KeyMap;
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.utils.InfoCmp.Capability;
|
||||
|
||||
import org.springframework.shell.component.view.event.KeyEvent.Key;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import static org.jline.keymap.KeyMap.alt;
|
||||
import static org.jline.keymap.KeyMap.ctrl;
|
||||
import static org.jline.keymap.KeyMap.del;
|
||||
import static org.jline.keymap.KeyMap.key;
|
||||
import static org.jline.keymap.KeyMap.translate;
|
||||
|
||||
public class KeyBinder {
|
||||
|
||||
private final Terminal terminal;
|
||||
|
||||
public KeyBinder(Terminal terminal) {
|
||||
Assert.notNull(terminal, "terminal must be set");
|
||||
this.terminal = terminal;
|
||||
}
|
||||
|
||||
public void bindAll(KeyMap<Integer> keyMap) {
|
||||
|
||||
keyMap.setUnicode(Key.Unicode);
|
||||
|
||||
for (char i = 32; i < KeyMap.KEYMAP_LENGTH - 1; i++) {
|
||||
keyMap.bind(KeyEvent.Key.Char, Character.toString(i));
|
||||
}
|
||||
|
||||
keyMap.bind(KeyEvent.Key.q | KeyEvent.KeyMask.CtrlMask, ctrl('q'));
|
||||
|
||||
keyMap.bind(KeyEvent.Key.Mouse, key(terminal, Capability.key_mouse));
|
||||
|
||||
keyMap.bind(KeyEvent.Key.Enter, "\r");
|
||||
keyMap.bind(KeyEvent.Key.Backspace, del());
|
||||
keyMap.bind(KeyEvent.Key.Delete, key(terminal, Capability.key_dc));
|
||||
keyMap.bind(KeyEvent.Key.Tab, "\t");
|
||||
keyMap.bind(KeyEvent.Key.Backtab, key(terminal, Capability.key_btab));
|
||||
|
||||
|
||||
keyMap.bind(KeyEvent.Key.CursorLeft, key(terminal, Capability.key_left));
|
||||
keyMap.bind(KeyEvent.Key.CursorRight, key(terminal, Capability.key_right));
|
||||
keyMap.bind(KeyEvent.Key.CursorUp, key(terminal, Capability.key_up));
|
||||
keyMap.bind(KeyEvent.Key.CursorDown, key(terminal, Capability.key_down));
|
||||
|
||||
keyMap.bind(KeyEvent.Key.CursorLeft | KeyEvent.KeyMask.AltMask, alt(key(terminal, Capability.key_left)));
|
||||
keyMap.bind(KeyEvent.Key.CursorRight | KeyEvent.KeyMask.AltMask, alt(key(terminal, Capability.key_right)));
|
||||
keyMap.bind(KeyEvent.Key.CursorUp | KeyEvent.KeyMask.AltMask, alt(key(terminal, Capability.key_up)));
|
||||
keyMap.bind(KeyEvent.Key.CursorDown | KeyEvent.KeyMask.AltMask, alt(key(terminal, Capability.key_down)));
|
||||
|
||||
keyMap.bind(KeyEvent.Key.CursorLeft | KeyEvent.KeyMask.CtrlMask, translate("^[[1;5D"));
|
||||
keyMap.bind(KeyEvent.Key.CursorRight | KeyEvent.KeyMask.CtrlMask, translate("^[[1;5C"));
|
||||
keyMap.bind(KeyEvent.Key.CursorUp | KeyEvent.KeyMask.CtrlMask, translate("^[[1;5A"));
|
||||
keyMap.bind(KeyEvent.Key.CursorDown | KeyEvent.KeyMask.CtrlMask, translate("^[[1;5B"));
|
||||
|
||||
keyMap.bind(KeyEvent.Key.CursorLeft | KeyEvent.KeyMask.ShiftMask, translate("^[[1;2D"));
|
||||
keyMap.bind(KeyEvent.Key.CursorRight | KeyEvent.KeyMask.ShiftMask, translate("^[[1;2C"));
|
||||
keyMap.bind(KeyEvent.Key.CursorUp | KeyEvent.KeyMask.ShiftMask, translate("^[[1;2A"));
|
||||
keyMap.bind(KeyEvent.Key.CursorDown | KeyEvent.KeyMask.ShiftMask, translate("^[[1;2B"));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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.event;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface KeyBindingConsumer extends Consumer<KeyEvent> {
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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.event;
|
||||
|
||||
public record KeyBindingConsumerArgs(KeyBindingConsumer consumer, KeyEvent event) {
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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.event;
|
||||
|
||||
/**
|
||||
*
|
||||
* mask special keys unicode keys ascii keys
|
||||
* [ ] [ ] [ ] [ ]
|
||||
* 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
|
||||
*
|
||||
*
|
||||
*/
|
||||
public record KeyEvent(int key) {
|
||||
|
||||
public static KeyEvent of(int key) {
|
||||
return new KeyEvent(key);
|
||||
}
|
||||
|
||||
|
||||
public boolean hasCtrl() {
|
||||
return ((key >> 30) & 1) == 1;
|
||||
}
|
||||
|
||||
public int getPlainKey() {
|
||||
return key & ~0xFFF00000;
|
||||
}
|
||||
|
||||
public boolean isKey(int match) {
|
||||
return (key & ~0xF0000000) == match;
|
||||
}
|
||||
|
||||
public boolean isKey() {
|
||||
return (key & ~KeyMask.CharMask) == 0;
|
||||
}
|
||||
|
||||
public static class Key {
|
||||
|
||||
public static final int A = 65;
|
||||
public static final int B = 66;
|
||||
public static final int C = 67;
|
||||
public static final int D = 68;
|
||||
public static final int E = 69;
|
||||
public static final int F = 70;
|
||||
public static final int G = 71;
|
||||
public static final int H = 72;
|
||||
public static final int I = 73;
|
||||
public static final int J = 74;
|
||||
public static final int K = 75;
|
||||
public static final int L = 76;
|
||||
public static final int M = 77;
|
||||
public static final int N = 78;
|
||||
public static final int O = 79;
|
||||
public static final int P = 80;
|
||||
public static final int Q = 81;
|
||||
public static final int R = 82;
|
||||
public static final int S = 83;
|
||||
public static final int T = 84;
|
||||
public static final int U = 85;
|
||||
public static final int V = 86;
|
||||
public static final int W = 87;
|
||||
public static final int X = 88;
|
||||
public static final int Y = 89;
|
||||
public static final int Z = 90;
|
||||
|
||||
public static final int a = 97;
|
||||
public static final int b = 98;
|
||||
public static final int c = 99;
|
||||
public static final int d = 100;
|
||||
public static final int e = 101;
|
||||
public static final int f = 102;
|
||||
public static final int g = 103;
|
||||
public static final int h = 104;
|
||||
public static final int i = 105;
|
||||
public static final int j = 106;
|
||||
public static final int k = 107;
|
||||
public static final int l = 108;
|
||||
public static final int m = 109;
|
||||
public static final int n = 110;
|
||||
public static final int o = 111;
|
||||
public static final int p = 112;
|
||||
public static final int q = 113;
|
||||
public static final int r = 114;
|
||||
public static final int s = 115;
|
||||
public static final int t = 116;
|
||||
public static final int u = 117;
|
||||
public static final int v = 118;
|
||||
public static final int w = 119;
|
||||
public static final int x = 120;
|
||||
public static final int y = 121;
|
||||
public static final int z = 122;
|
||||
|
||||
public static final int CursorUp = 0x100000;
|
||||
public static final int CursorDown = 0x100001;
|
||||
public static final int CursorLeft = 0x100002;
|
||||
public static final int CursorRight = 0x100003;
|
||||
public static final int Enter = 0x100004;
|
||||
public static final int Backspace = 0x100005;
|
||||
public static final int Delete = 0x100006;
|
||||
public static final int Tab = 0x100007;
|
||||
public static final int Backtab = 0x100008;
|
||||
|
||||
public static final int Char = 0x1000000;
|
||||
public static final int Mouse = 0x1000001;
|
||||
public static final int Unicode = 0x1000002;
|
||||
|
||||
}
|
||||
|
||||
public static class KeyMask {
|
||||
public static final int CharMask = 0x000fffff;
|
||||
public static final int SpecialMask = 0xfff00000;
|
||||
public static final int ShiftMask = 0x10000000;
|
||||
public static final int CtrlMask = 0x40000000;
|
||||
public static final int AltMask = 0x80000000;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* 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.event;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.shell.component.view.control.View;
|
||||
|
||||
/**
|
||||
* Handles Key events in a form of {@link KeyHandlerArgs} and returns
|
||||
* {@link KeyHandlerResult}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface KeyHandler {
|
||||
|
||||
/**
|
||||
* Handle Key event wrapped in a {@link KeyHandlerArgs}.
|
||||
*
|
||||
* @param args the Key handler arguments
|
||||
* @return a handler result
|
||||
*/
|
||||
KeyHandlerResult handle(KeyHandlerArgs args);
|
||||
|
||||
/**
|
||||
* Returns a composed handler that first handles {@code this} handler and then
|
||||
* handles {@code other} handler if {@code predicate} against result from
|
||||
* {@code this} matches.
|
||||
*
|
||||
* @param other the handler to handle after this handler
|
||||
* @param predicate the predicate test against results from this
|
||||
* @return a composed handler
|
||||
*/
|
||||
default KeyHandler thenConditionally(KeyHandler other, Predicate<KeyHandlerResult> predicate) {
|
||||
return args -> {
|
||||
KeyHandlerResult result = handle(args);
|
||||
if (predicate.test(result)) {
|
||||
return other.handle(args);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a composed handler that first handles {@code this} handler and then
|
||||
* handles {@code other} if {@code this} consumed an event.
|
||||
*
|
||||
* @param other the handler to handle after this handler
|
||||
* @return a composed handler
|
||||
*/
|
||||
default KeyHandler thenIfConsumed(KeyHandler other) {
|
||||
return thenConditionally(other, result -> result.consumed());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a composed handler that first handles {@code this} handler and then
|
||||
* handles {@code other} if {@code this} did not consume an event.
|
||||
*
|
||||
* @param other the handler to handle after this handler
|
||||
* @return a composed handler
|
||||
*/
|
||||
default KeyHandler thenIfNotConsumed(KeyHandler other) {
|
||||
return thenConditionally(other, result -> !result.consumed());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a handler that always returns a non-consumed result.
|
||||
*
|
||||
* @return a handler that always returns a non-consumed result
|
||||
*/
|
||||
static KeyHandler neverConsume() {
|
||||
return args -> resultOf(args.event(), false, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct {@link KeyHandlerArgs} from a {@link KeyEvent}.
|
||||
*
|
||||
* @param event the Key event
|
||||
* @return a Key handler args
|
||||
*/
|
||||
static KeyHandlerArgs argsOf(KeyEvent event) {
|
||||
return new KeyHandlerArgs(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct {@link KeyHandlerResult} from a {@link KeyEvent} and a
|
||||
* {@link View}.
|
||||
*
|
||||
* @param event the Key event
|
||||
* @param focus the view
|
||||
* @return a Key handler result
|
||||
*/
|
||||
static KeyHandlerResult resultOf(KeyEvent event, boolean consumed, View focus) {
|
||||
return new KeyHandlerResult(event, consumed, focus, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Arguments for a {@link KeyHandler}.
|
||||
*
|
||||
* @param event the Key event
|
||||
*/
|
||||
record KeyHandlerArgs(KeyEvent event) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Result from a {@link KeyHandler}.
|
||||
*
|
||||
* @param event the Key event
|
||||
* @param consumed flag telling if event was consumed
|
||||
* @param focus the view which consumed an event
|
||||
* @param capture the view which captured an event
|
||||
*/
|
||||
record KeyHandlerResult(@Nullable KeyEvent event, boolean consumed, @Nullable View focus,
|
||||
@Nullable View capture) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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.event;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface MouseBindingConsumer extends Consumer<MouseEvent> {
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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.event;
|
||||
|
||||
public record MouseBindingConsumerArgs(MouseBindingConsumer consumer, MouseEvent event) {
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* 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.event;
|
||||
|
||||
/**
|
||||
*
|
||||
* unused modifier button type
|
||||
* [ ] [ ] [ ] [ ]
|
||||
* 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
|
||||
*
|
||||
*
|
||||
*/
|
||||
public record MouseEvent(int x, int y, int mouse) {
|
||||
|
||||
public static MouseEvent of(int x, int y, int mouse) {
|
||||
return new MouseEvent(x, y, mouse);
|
||||
}
|
||||
|
||||
public static MouseEvent of(org.jline.terminal.MouseEvent event) {
|
||||
int type = switch (event.getType()) {
|
||||
case Released -> Type.Released;
|
||||
case Pressed -> Type.Pressed;
|
||||
case Wheel -> Type.Wheel;
|
||||
case Moved -> Type.Moved;
|
||||
case Dragged -> Type.Dragged;
|
||||
default -> 0;
|
||||
};
|
||||
int button = switch (event.getButton()) {
|
||||
case NoButton -> Button.NoButton;
|
||||
case Button1 -> Button.Button1;
|
||||
case Button2 -> Button.Button2;
|
||||
case Button3 -> Button.Button3;
|
||||
case WheelUp -> Button.WheelUp;
|
||||
case WheelDown -> Button.WheelDown;
|
||||
default -> 0;
|
||||
};
|
||||
int modifier = 0;
|
||||
if (event.getModifiers() != null) {
|
||||
if (event.getModifiers().contains(org.jline.terminal.MouseEvent.Modifier.Shift)) {
|
||||
modifier |= Modifier.Shift;
|
||||
}
|
||||
if (event.getModifiers().contains(org.jline.terminal.MouseEvent.Modifier.Alt)) {
|
||||
modifier |= Modifier.Alt;
|
||||
}
|
||||
if (event.getModifiers().contains(org.jline.terminal.MouseEvent.Modifier.Control)) {
|
||||
modifier |= Modifier.Control;
|
||||
}
|
||||
}
|
||||
return of(event.getX(), event.getY(), type | button | modifier);
|
||||
}
|
||||
|
||||
public boolean hasType() {
|
||||
return (mouse & MouseMask.TypeMask) != 0;
|
||||
}
|
||||
|
||||
public boolean hasButton() {
|
||||
return (mouse & MouseMask.ButtonMask) != 0;
|
||||
}
|
||||
|
||||
public boolean hasModifier() {
|
||||
return (mouse & MouseMask.ModifierMask) != 0;
|
||||
}
|
||||
|
||||
public boolean has(int mask) {
|
||||
return (mouse & mask) == mask;
|
||||
}
|
||||
|
||||
public static class Type {
|
||||
public static final int Released = 0x00000001;
|
||||
public static final int Pressed = 0x00000002;
|
||||
public static final int Wheel = 0x00000004;
|
||||
public static final int Moved = 0x00000008;
|
||||
public static final int Dragged = 0x00000010;
|
||||
}
|
||||
|
||||
public static class Button {
|
||||
public static final int NoButton = 0x00000020;
|
||||
public static final int Button1 = 0x00000040;
|
||||
public static final int Button2 = 0x00000080;
|
||||
public static final int Button3 = 0x00000100;
|
||||
public static final int WheelUp = 0x00000200;
|
||||
public static final int WheelDown = 0x00000400;
|
||||
}
|
||||
|
||||
public static class Modifier {
|
||||
public static final int Shift = 0x00000800;
|
||||
public static final int Alt = 0x00001000;
|
||||
public static final int Control = 0x00002000;
|
||||
}
|
||||
|
||||
public static class MouseMask {
|
||||
// bits 0-4
|
||||
public static final int TypeMask = 0x0000001f;
|
||||
// bits 5-10
|
||||
public static final int ButtonMask = 0x000007e0;
|
||||
// bits 11-13
|
||||
public static final int ModifierMask = 0x00003800;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* 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.event;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.shell.component.view.control.View;
|
||||
|
||||
/**
|
||||
* Handles mouse events in a form of {@link MouseHandlerArgs} and returns
|
||||
* {@link MouseHandlerResult}. Typically used in a {@link View}.
|
||||
*
|
||||
* {@link MouseHandler} itself don't define any restrictions how it's used.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface MouseHandler {
|
||||
|
||||
/**
|
||||
* Handle mouse event wrapped in a {@link MouseHandlerArgs}.
|
||||
*
|
||||
* @param args the mouse handler arguments
|
||||
* @return a handler result
|
||||
*/
|
||||
MouseHandlerResult handle(MouseHandlerArgs args);
|
||||
|
||||
/**
|
||||
* Returns a composed handler that first handles {@code this} handler and then
|
||||
* handles {@code other} handler if {@code predicate} against result from
|
||||
* {@code this} matches.
|
||||
*
|
||||
* @param other the handler to handle after this handler
|
||||
* @param predicate the predicate test against results from this
|
||||
* @return a composed handler
|
||||
*/
|
||||
default MouseHandler thenConditionally(MouseHandler other, Predicate<MouseHandlerResult> predicate) {
|
||||
return args -> {
|
||||
MouseHandlerResult result = handle(args);
|
||||
if (predicate.test(result)) {
|
||||
return other.handle(args);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a composed handler that first handles {@code this} handler and then
|
||||
* handles {@code other} if {@code this} consumed an event.
|
||||
*
|
||||
* @param other the handler to handle after this handler
|
||||
* @return a composed handler
|
||||
*/
|
||||
default MouseHandler thenIfConsumed(MouseHandler other) {
|
||||
return thenConditionally(other, result -> result.consumed());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a composed handler that first handles {@code this} handler and then
|
||||
* handles {@code other} if {@code this} did not consume an event.
|
||||
*
|
||||
* @param other the handler to handle after this handler
|
||||
* @return a composed handler
|
||||
*/
|
||||
default MouseHandler thenIfNotConsumed(MouseHandler other) {
|
||||
return thenConditionally(other, result -> !result.consumed());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a handler that always returns a non-consumed result.
|
||||
*
|
||||
* @return a handler that always returns a non-consumed result
|
||||
*/
|
||||
static MouseHandler neverConsume() {
|
||||
return args -> resultOf(args.event(), false, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct {@link MouseHandlerArgs} from a {@link MouseEvent}.
|
||||
*
|
||||
* @param event the mouse event
|
||||
* @return a mouse handler args
|
||||
*/
|
||||
static MouseHandlerArgs argsOf(MouseEvent event) {
|
||||
return new MouseHandlerArgs(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Arguments for a {@link MouseHandler}.
|
||||
*
|
||||
* @param event the mouse event
|
||||
*/
|
||||
record MouseHandlerArgs(MouseEvent event) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct {@link MouseHandlerResult} from a {@link MouseEvent} and a
|
||||
* {@link View}.
|
||||
*
|
||||
* @param event the mouse event
|
||||
* @param consumed flag telling if event was consumed
|
||||
* @param focus the view which is requesting focus
|
||||
* @param capture the view which captured an event
|
||||
* @return a mouse handler result
|
||||
*/
|
||||
static MouseHandlerResult resultOf(MouseEvent event, boolean consumed, View focus, View capture) {
|
||||
return new MouseHandlerResult(event, consumed, focus, capture);
|
||||
}
|
||||
|
||||
/**
|
||||
* Result from a {@link MouseHandler}.
|
||||
*
|
||||
* @param event the mouse event
|
||||
* @param consumed flag telling if event was consumed
|
||||
* @param focus the view which is requesting focus
|
||||
* @param capture the view which captured an event
|
||||
*/
|
||||
record MouseHandlerResult(@Nullable MouseEvent event, boolean consumed, @Nullable View focus,
|
||||
@Nullable View capture) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.event.processor;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.shell.component.view.event.EventLoop;
|
||||
import org.springframework.shell.component.view.event.EventLoop.EventLoopProcessor;
|
||||
import org.springframework.shell.component.view.message.ShellMessageHeaderAccessor;
|
||||
import org.springframework.shell.component.view.message.StaticShellMessageHeaderAccessor;
|
||||
|
||||
/**
|
||||
* {@link EventLoopProcessor} converting incoming message into animation tick
|
||||
* messages.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class AnimationEventLoopProcessor implements EventLoopProcessor {
|
||||
|
||||
@Override
|
||||
public boolean canProcess(Message<?> message) {
|
||||
if (EventLoop.Type.SYSTEM.equals(StaticShellMessageHeaderAccessor.getEventType(message))) {
|
||||
if (message.getHeaders().containsKey("animationstart")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<? extends Message<?>> process(Message<?> message) {
|
||||
return Flux.range(0, 40)
|
||||
.delayElements(Duration.ofMillis(100))
|
||||
.map(i -> {
|
||||
return MessageBuilder
|
||||
.withPayload(i)
|
||||
.setHeader(ShellMessageHeaderAccessor.EVENT_TYPE, EventLoop.Type.SYSTEM)
|
||||
.setHeader("animationtick", true)
|
||||
.setHeader("animationfrom", 0)
|
||||
.setHeader("animationto", 9)
|
||||
.build();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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.event.processor;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.shell.component.view.event.EventLoop;
|
||||
import org.springframework.shell.component.view.event.KeyBindingConsumerArgs;
|
||||
import org.springframework.shell.component.view.event.EventLoop.EventLoopProcessor;
|
||||
import org.springframework.shell.component.view.event.MouseBindingConsumerArgs;
|
||||
import org.springframework.shell.component.view.message.StaticShellMessageHeaderAccessor;
|
||||
|
||||
public class TaskEventLoopProcessor implements EventLoopProcessor {
|
||||
|
||||
@Override
|
||||
public boolean canProcess(Message<?> message) {
|
||||
if (EventLoop.Type.TASK.equals(StaticShellMessageHeaderAccessor.getEventType(message))) {
|
||||
Object payload = message.getPayload();
|
||||
if (payload instanceof Runnable r) {
|
||||
return true;
|
||||
}
|
||||
else if(payload instanceof KeyBindingConsumerArgs) {
|
||||
return true;
|
||||
}
|
||||
else if(payload instanceof MouseBindingConsumerArgs) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<? extends Message<?>> process(Message<?> message) {
|
||||
Object payload = message.getPayload();
|
||||
if (payload instanceof Runnable r) {
|
||||
return processRunnable(message);
|
||||
}
|
||||
else if(payload instanceof KeyBindingConsumerArgs) {
|
||||
return processKeyConsumer(message);
|
||||
}
|
||||
else if(payload instanceof MouseBindingConsumerArgs) {
|
||||
return processMouseConsumer(message);
|
||||
}
|
||||
// should not happen
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
private Flux<? extends Message<?>> processRunnable(Message<?> message) {
|
||||
return Mono.just(message.getPayload())
|
||||
.ofType(Runnable.class)
|
||||
.flatMap(Mono::fromRunnable)
|
||||
.then(Mono.just(MessageBuilder.withPayload(new Object()).build()))
|
||||
.flux();
|
||||
}
|
||||
|
||||
private Flux<? extends Message<?>> processMouseConsumer(Message<?> message) {
|
||||
return Mono.just(message.getPayload())
|
||||
.ofType(MouseBindingConsumerArgs.class)
|
||||
.flatMap(args -> Mono.fromRunnable(() -> {
|
||||
args.consumer().accept(args.event());
|
||||
}))
|
||||
.then(Mono.just(MessageBuilder.withPayload(new Object()).build()))
|
||||
.flux();
|
||||
}
|
||||
|
||||
private Flux<? extends Message<?>> processKeyConsumer(Message<?> message) {
|
||||
return Mono.just(message.getPayload())
|
||||
.ofType(KeyBindingConsumerArgs.class)
|
||||
.flatMap(args -> Mono.fromRunnable(() -> {
|
||||
args.consumer().accept(args.event());
|
||||
}))
|
||||
.then(Mono.just(MessageBuilder.withPayload(new Object()).build()))
|
||||
.flux();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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.geom;
|
||||
|
||||
/**
|
||||
* Record representing dimensions {@code width} and {@code height}.
|
||||
*/
|
||||
public record Dimension(int width, int height) {
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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.geom;
|
||||
|
||||
public enum HorizontalAlign {
|
||||
LEFT, CENTER, RIGHT,
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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.geom;
|
||||
|
||||
/**
|
||||
* Record representing position {@code x} and {@code y}.
|
||||
*/
|
||||
public record Position(int x, int y) {
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.geom;
|
||||
|
||||
/**
|
||||
* Record representing coordinates {@code x}, {@code y} and its {@code width}
|
||||
* and {@code height}.
|
||||
*/
|
||||
public record Rectangle(int x, int y, int width, int height) {
|
||||
|
||||
public boolean contains(int X, int Y) {
|
||||
int w = this.width;
|
||||
int h = this.height;
|
||||
if ((w | h) < 0) {
|
||||
return false;
|
||||
}
|
||||
int x = this.x;
|
||||
int y = this.y;
|
||||
if (X < x || Y < y) {
|
||||
return false;
|
||||
}
|
||||
w += x;
|
||||
h += y;
|
||||
return ((w < x || w > X) && (h < y || h > Y));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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.geom;
|
||||
|
||||
public enum VerticalAlign {
|
||||
TOP, CENTER, BOTTOM
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* 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.message;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.shell.component.view.control.View;
|
||||
import org.springframework.shell.component.view.event.EventLoop;
|
||||
import org.springframework.shell.component.view.event.MouseEvent;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Shell spesific message builder.
|
||||
*
|
||||
* @param <T> the payload type.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public final class ShellMessageBuilder<T> {
|
||||
|
||||
private final T payload;
|
||||
private final ShellMessageHeaderAccessor headerAccessor;
|
||||
@Nullable
|
||||
private final Message<T> originalMessage;
|
||||
|
||||
|
||||
private ShellMessageBuilder(T payload, @Nullable Message<T> originalMessage) {
|
||||
Assert.notNull(payload, "payload must not be null");
|
||||
this.payload = payload;
|
||||
this.originalMessage = originalMessage;
|
||||
this.headerAccessor = new ShellMessageHeaderAccessor(originalMessage);
|
||||
// if (originalMessage != null) {
|
||||
// this.modified = (!this.payload.equals(originalMessage.getPayload()));
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a builder for a new {@link Message} instance with the provided payload.
|
||||
*
|
||||
* @param <T> The type of the payload.
|
||||
* @param payload the payload for the new message
|
||||
* @return A ShellMessageBuilder.
|
||||
*/
|
||||
public static <T> ShellMessageBuilder<T> withPayload(T payload) {
|
||||
return new ShellMessageBuilder<>(payload, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@code redraw} message.
|
||||
*
|
||||
* @return a redraw message
|
||||
*/
|
||||
public static Message<String> ofRedraw() {
|
||||
return new ShellMessageBuilder<>("redraw", null)
|
||||
.setEventType(EventLoop.Type.SYSTEM)
|
||||
.setPriority(0)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a message of a {@link MouseEvent}.
|
||||
*
|
||||
* @param event the event type
|
||||
* @return a message with {@link MouseEvent} as a payload
|
||||
*/
|
||||
public static Message<MouseEvent> ofMouseEvent(MouseEvent event) {
|
||||
return new ShellMessageBuilder<>(event, null)
|
||||
.setEventType(EventLoop.Type.MOUSE)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Message<?> ofView(View view, Object args) {
|
||||
return new ShellMessageBuilder<>(args, null)
|
||||
.setEventType(EventLoop.Type.VIEW)
|
||||
.setView(view)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Message<String> ofViewFocus(String action, View view) {
|
||||
return new ShellMessageBuilder<>(action, null)
|
||||
.setEventType(EventLoop.Type.SYSTEM)
|
||||
.setView(view)
|
||||
.build();
|
||||
}
|
||||
|
||||
public ShellMessageBuilder<T> setPriority(Integer priority) {
|
||||
return setHeader(ShellMessageHeaderAccessor.PRIORITY, priority);
|
||||
}
|
||||
|
||||
public ShellMessageBuilder<T> setView(View view) {
|
||||
return setHeader(ShellMessageHeaderAccessor.VIEW, view);
|
||||
}
|
||||
|
||||
public ShellMessageBuilder<T> setEventType(EventLoop.Type type) {
|
||||
return setHeader(ShellMessageHeaderAccessor.EVENT_TYPE, type);
|
||||
}
|
||||
|
||||
public ShellMessageBuilder<T> setHeader(String headerName, @Nullable Object headerValue) {
|
||||
this.headerAccessor.setHeader(headerName, headerValue);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Message<T> build() {
|
||||
return new GenericMessage<>(this.payload, this.headerAccessor.toMap());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* 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.message;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import reactor.util.context.ContextView;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.MessageHeaderAccessor;
|
||||
import org.springframework.shell.component.view.control.View;
|
||||
import org.springframework.shell.component.view.event.EventLoop;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Adds standard shell Headers.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class ShellMessageHeaderAccessor extends MessageHeaderAccessor {
|
||||
|
||||
public static final String PRIORITY = "priority";
|
||||
|
||||
public static final String VIEW = "view";
|
||||
|
||||
/**
|
||||
* Raw source message.
|
||||
*/
|
||||
public static final String REACTOR_CONTEXT = "reactorContext";
|
||||
|
||||
/**
|
||||
* Raw source message.
|
||||
*/
|
||||
public static final String EVENT_TYPE = "eventType";
|
||||
|
||||
private static final BiFunction<String, String, String> TYPE_VERIFY_MESSAGE_FUNCTION =
|
||||
(name, trailer) -> "The '" + name + trailer;
|
||||
|
||||
private Set<String> readOnlyHeaders = new HashSet<>();
|
||||
|
||||
public ShellMessageHeaderAccessor(@Nullable Message<?> message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify a list of headers which should be considered as read only and prohibited
|
||||
* from being populated in the message.
|
||||
*
|
||||
* @param readOnlyHeaders the list of headers for {@code readOnly} mode. Defaults to
|
||||
* {@link org.springframework.messaging.MessageHeaders#ID} and
|
||||
* {@link org.springframework.messaging.MessageHeaders#TIMESTAMP}.
|
||||
* @see #isReadOnly(String)
|
||||
*/
|
||||
public void setReadOnlyHeaders(String... readOnlyHeaders) {
|
||||
Assert.noNullElements(readOnlyHeaders, "'readOnlyHeaders' must not be contain null items.");
|
||||
if (!ObjectUtils.isEmpty(readOnlyHeaders)) {
|
||||
this.readOnlyHeaders = new HashSet<>(Arrays.asList(readOnlyHeaders));
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Integer getPriority() {
|
||||
Number priority = getHeader(PRIORITY, Number.class);
|
||||
return (priority != null ? priority.intValue() : null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public View getView() {
|
||||
View view = getHeader(VIEW, View.class);
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link ContextView} header if present.
|
||||
*
|
||||
* @return the {@link ContextView} header if present.
|
||||
*/
|
||||
@Nullable
|
||||
public ContextView getReactorContext() {
|
||||
return getHeader(REACTOR_CONTEXT, ContextView.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link EventLoop.Type} header if present.
|
||||
*
|
||||
* @return the {@link EventLoop.Type} header if present.
|
||||
*/
|
||||
@Nullable
|
||||
public EventLoop.Type getEventType() {
|
||||
return getHeader(EVENT_TYPE, EventLoop.Type.class);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
public <T> T getHeader(String key, Class<T> type) {
|
||||
Object value = getHeader(key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (!type.isAssignableFrom(value.getClass())) {
|
||||
throw new IllegalArgumentException("Incorrect type specified for header '" + key + "'. Expected [" + type
|
||||
+ "] but actual type is [" + value.getClass() + "]");
|
||||
}
|
||||
return (T) value;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void verifyType(String headerName, Object headerValue) {
|
||||
if (headerName != null && headerValue != null) {
|
||||
super.verifyType(headerName, headerValue);
|
||||
if (ShellMessageHeaderAccessor.PRIORITY.equals(headerName)) {
|
||||
Assert.isTrue(Number.class.isAssignableFrom(headerValue.getClass()),
|
||||
TYPE_VERIFY_MESSAGE_FUNCTION.apply(headerName, "' header value must be a Number."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadOnly(String headerName) {
|
||||
return super.isReadOnly(headerName) || this.readOnlyHeaders.contains(headerName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> toMap() {
|
||||
if (ObjectUtils.isEmpty(this.readOnlyHeaders)) {
|
||||
return super.toMap();
|
||||
}
|
||||
else {
|
||||
Map<String, Object> headers = super.toMap();
|
||||
for (String header : this.readOnlyHeaders) {
|
||||
headers.remove(header);
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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.message;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import reactor.util.context.Context;
|
||||
import reactor.util.context.ContextView;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.shell.component.view.control.View;
|
||||
import org.springframework.shell.component.view.event.EventLoop;
|
||||
|
||||
/**
|
||||
* Lightweight type-safe header accessor avoiding object creation just to access
|
||||
* a header.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*
|
||||
* @see ShellMessageHeaderAccessor
|
||||
*/
|
||||
public final class StaticShellMessageHeaderAccessor {
|
||||
|
||||
private StaticShellMessageHeaderAccessor() {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static UUID getId(Message<?> message) {
|
||||
Object value = message.getHeaders().get(MessageHeaders.ID);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return (value instanceof UUID ? (UUID) value : UUID.fromString(value.toString()));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Long getTimestamp(Message<?> message) {
|
||||
Object value = message.getHeaders().get(MessageHeaders.TIMESTAMP);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return (value instanceof Long ? (Long) value : Long.parseLong(value.toString()));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Integer getPriority(Message<?> message) {
|
||||
Number priority = message.getHeaders().get(ShellMessageHeaderAccessor.PRIORITY, Number.class);
|
||||
return (priority != null ? priority.intValue() : null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static View getView(Message<?> message) {
|
||||
View view = message.getHeaders().get(ShellMessageHeaderAccessor.VIEW, View.class);
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link ContextView} header if present.
|
||||
*
|
||||
* @param message the message to get a header from.
|
||||
* @return the {@link ContextView} header if present.
|
||||
*/
|
||||
public static ContextView getReactorContext(Message<?> message) {
|
||||
ContextView reactorContext = message.getHeaders()
|
||||
.get(ShellMessageHeaderAccessor.REACTOR_CONTEXT, ContextView.class);
|
||||
if (reactorContext == null) {
|
||||
reactorContext = Context.empty();
|
||||
}
|
||||
return reactorContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link EventLoop.Type} header if present.
|
||||
*
|
||||
* @param message the message to get a header from.
|
||||
* @return the {@link EventLoop.Type} header if present.
|
||||
*/
|
||||
public static EventLoop.Type getEventType(Message<?> message) {
|
||||
EventLoop.Type eventType = message.getHeaders()
|
||||
.get(ShellMessageHeaderAccessor.EVENT_TYPE, EventLoop.Type.class);
|
||||
return eventType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
/*
|
||||
* 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.screen;
|
||||
|
||||
public class Color {
|
||||
|
||||
// https://en.wikipedia.org/wiki/X11_color_names#Numbered_variants
|
||||
public static final int ANTIQUEWHITE = 0xFAEBD7;
|
||||
public static final int AQUAMARINE = 0x7FFFD4;
|
||||
public static final int AZURE = 0xF0FFFF;
|
||||
public static final int BISQUE = 0xFFE4C4;
|
||||
public static final int BLUE = 0x0000FF;
|
||||
public static final int BROWN = 0xA52A2A;
|
||||
public static final int BURLYWOOD = 0xDEB887;
|
||||
public static final int CADETBLUE = 0x5F9EA0;
|
||||
public static final int CHARTREUSE = 0x7FFF00;
|
||||
public static final int CHOCOLATE = 0xD2691E;
|
||||
public static final int CORAL = 0xFF7F50;
|
||||
public static final int CORNSILK = 0xFFF8DC;
|
||||
public static final int CYAN = 0x00FFFF;
|
||||
public static final int DARKGOLDENROD = 0xB8860B;
|
||||
public static final int DARKOLIVE = 0x556B2F;
|
||||
public static final int DARKORANGE = 0xFF8C00;
|
||||
public static final int DARKORCHID = 0x9932CC;
|
||||
public static final int DARKSEAGREEN = 0x8FBC8F;
|
||||
public static final int DARKSLATEGRAY = 0x2F4F4F;
|
||||
public static final int DEEPPINK = 0xFF1493;
|
||||
public static final int DEEPSKYBLUE = 0x00BFFF;
|
||||
public static final int DODGERBLUE = 0x1E90FF;
|
||||
public static final int FIREBRICK = 0xB22222;
|
||||
public static final int GOLD = 0xFFD700;
|
||||
public static final int GOLDENROD = 0xDAA520;
|
||||
public static final int GREEN = 0x00FF00;
|
||||
public static final int HONEYDEW = 0xF0FFF0;
|
||||
public static final int HOTPINK = 0xFF69B4;
|
||||
public static final int INDIANRED = 0xCD5C5C;
|
||||
public static final int IVORY = 0xFFFFF0;
|
||||
public static final int KHAKI = 0xF0E68C;
|
||||
public static final int LAVENDERBLUSH = 0xFFF0F5;
|
||||
public static final int LEMONCHIFFON = 0xFFFACD;
|
||||
public static final int LIGHTBLUE = 0xADD8E6;
|
||||
public static final int LIGHTCYAN = 0xE0FFFF;
|
||||
public static final int LIGHTGOLDENROD = 0xEEDD82;
|
||||
public static final int LIGHTPINK = 0xFFB6C1;
|
||||
public static final int LIGHTSALMON = 0xFFA07A;
|
||||
public static final int LIGHTSKYBLUE = 0x87CEFA;
|
||||
public static final int LIGHTSTEELBLUE = 0xB0C4DE;
|
||||
public static final int LIGHTYELLOW = 0xFFFFE0;
|
||||
public static final int MAGENTA = 0xFF00FF;
|
||||
public static final int MAROON = 0xB03060;
|
||||
public static final int MEDIUMORCHID = 0xBA55D3;
|
||||
public static final int MEDIUMPURPLE = 0x9370DB;
|
||||
public static final int MISTYROSE = 0xFFE4E1;
|
||||
public static final int NAVAJOWHITE = 0xFFDEAD;
|
||||
public static final int OLIVEDRAB = 0x6B8E23;
|
||||
public static final int ORANGE = 0xFFA500;
|
||||
public static final int ORCHID = 0xDA70D6;
|
||||
public static final int PALEGREEN = 0x98FB98;
|
||||
public static final int PALETURQUOISE = 0xAFEEEE;
|
||||
public static final int PALEVIOLETRED = 0xDB7093;
|
||||
public static final int PEACHPUFF = 0xFFDAB9;
|
||||
public static final int PINK = 0xFFC0CB;
|
||||
public static final int PLUM = 0xDDA0DD;
|
||||
public static final int PURPLE = 0xA020F0;
|
||||
public static final int RED = 0xFF0000;
|
||||
public static final int ROSYBROWN = 0xBC8F8F;
|
||||
public static final int ROYALBLUE = 0x4169E1;
|
||||
public static final int SALMON = 0xFA8072;
|
||||
public static final int SEAGREEN = 0x2E8B57;
|
||||
public static final int SEASHELL = 0xFFF5EE;
|
||||
public static final int SIENNA = 0xA0522D;
|
||||
public static final int SKYBLUE = 0x87CEEB;
|
||||
public static final int SLATEBLUE = 0x6A5ACD;
|
||||
public static final int SLATEGRAY = 0x708090;
|
||||
public static final int SNOW = 0xFFFAFA;
|
||||
public static final int SPRINGGREEN = 0x00FF7F;
|
||||
public static final int STEELBLUE = 0x4682B4;
|
||||
public static final int TAN = 0xD2B48C;
|
||||
public static final int THISTLE = 0xD8BFD8;
|
||||
public static final int TOMATO = 0xFF6347;
|
||||
public static final int TURQUOISE = 0x40E0D0;
|
||||
public static final int VIOLETRED = 0xD02090;
|
||||
public static final int WHEAT = 0xF5DEB3;
|
||||
public static final int YELLOW = 0xFFFF00;
|
||||
public static final int ANTIQUEWHITE1 = 0xFFEFDB;
|
||||
public static final int AQUAMARINE1 = 0x7FFFD4;
|
||||
public static final int AZURE1 = 0xF0FFFF;
|
||||
public static final int BISQUE1 = 0xFFE4C4;
|
||||
public static final int BLUE1 = 0x0000FF;
|
||||
public static final int BROWN1 = 0xFF4040;
|
||||
public static final int BURLYWOOD1 = 0xFFD39B;
|
||||
public static final int CADETBLUE1 = 0x98F5FF;
|
||||
public static final int CHARTREUSE1 = 0x7FFF00;
|
||||
public static final int CHOCOLATE1 = 0xFF7F24;
|
||||
public static final int CORAL1 = 0xFF7256;
|
||||
public static final int CORNSILK1 = 0xFFF8DC;
|
||||
public static final int CYAN1 = 0x00FFFF;
|
||||
public static final int DARKGOLDENROD1 = 0xFFB90F;
|
||||
public static final int DARKOLIVE1 = 0xCAFF70;
|
||||
public static final int DARKORANGE1 = 0xFF7F00;
|
||||
public static final int DARKORCHID1 = 0xBF3EFF;
|
||||
public static final int DARKSEAGREEN1 = 0xC1FFC1;
|
||||
public static final int DARKSLATEGRAY1 = 0x97FFFF;
|
||||
public static final int DEEPPINK1 = 0xFF1493;
|
||||
public static final int DEEPSKYBLUE1 = 0x00BFFF;
|
||||
public static final int DODGERBLUE1 = 0x1E90FF;
|
||||
public static final int FIREBRICK1 = 0xFF3030;
|
||||
public static final int GOLD1 = 0xFFD700;
|
||||
public static final int GOLDENROD1 = 0xFFC125;
|
||||
public static final int GREEN1 = 0x00FF00;
|
||||
public static final int HONEYDEW1 = 0xF0FFF0;
|
||||
public static final int HOTPINK1 = 0xFF6EB4;
|
||||
public static final int INDIANRED1 = 0xFF6A6A;
|
||||
public static final int IVORY1 = 0xFFFFF0;
|
||||
public static final int KHAKI1 = 0xFFF68F;
|
||||
public static final int LAVENDERBLUSH1 = 0xFFF0F5;
|
||||
public static final int LEMONCHIFFON1 = 0xFFFACD;
|
||||
public static final int LIGHTBLUE1 = 0xBFEFFF;
|
||||
public static final int LIGHTCYAN1 = 0xE0FFFF;
|
||||
public static final int LIGHTGOLDENROD1 = 0xFFEC8B;
|
||||
public static final int LIGHTPINK1 = 0xFFAEB9;
|
||||
public static final int LIGHTSALMON1 = 0xFFA07A;
|
||||
public static final int LIGHTSKYBLUE1 = 0xB0E2FF;
|
||||
public static final int LIGHTSTEELBLUE1 = 0xCAE1FF;
|
||||
public static final int LIGHTYELLOW1 = 0xFFFFE0;
|
||||
public static final int MAGENTA1 = 0xFF00FF;
|
||||
public static final int MAROON1 = 0xFF34B3;
|
||||
public static final int MEDIUMORCHID1 = 0xE066FF;
|
||||
public static final int MEDIUMPURPLE1 = 0xAB82FF;
|
||||
public static final int MISTYROSE1 = 0xFFE4E1;
|
||||
public static final int NAVAJOWHITE1 = 0xFFDEAD;
|
||||
public static final int OLIVEDRAB1 = 0xC0FF3E;
|
||||
public static final int ORANGE1 = 0xFFA500;
|
||||
public static final int ORCHID1 = 0xFF83FA;
|
||||
public static final int PALEGREEN1 = 0x9AFF9A;
|
||||
public static final int PALETURQUOISE1 = 0xBBFFFF;
|
||||
public static final int PALEVIOLETRED1 = 0xFF82AB;
|
||||
public static final int PEACHPUFF1 = 0xFFDAB9;
|
||||
public static final int PINK1 = 0xFFB5C5;
|
||||
public static final int PLUM1 = 0xFFBBFF;
|
||||
public static final int PURPLE1 = 0x9B30FF;
|
||||
public static final int RED1 = 0xFF0000;
|
||||
public static final int ROSYBROWN1 = 0xFFC1C1;
|
||||
public static final int ROYALBLUE1 = 0x4876FF;
|
||||
public static final int SALMON1 = 0xFF8C69;
|
||||
public static final int SEAGREEN1 = 0x54FF9F;
|
||||
public static final int SEASHELL1 = 0xFFF5EE;
|
||||
public static final int SIENNA1 = 0xFF8247;
|
||||
public static final int SKYBLUE1 = 0x87CEFF;
|
||||
public static final int SLATEBLUE1 = 0x836FFF;
|
||||
public static final int SLATEGRAY1 = 0xC6E2FF;
|
||||
public static final int SNOW1 = 0xFFFAFA;
|
||||
public static final int SPRINGGREEN1 = 0x00FF7F;
|
||||
public static final int STEELBLUE1 = 0x63B8FF;
|
||||
public static final int TAN1 = 0xFFA54F;
|
||||
public static final int THISTLE1 = 0xFFE1FF;
|
||||
public static final int TOMATO1 = 0xFF6347;
|
||||
public static final int TURQUOISE1 = 0x00F5FF;
|
||||
public static final int VIOLETRED1 = 0xFF3E96;
|
||||
public static final int WHEAT1 = 0xFFE7BA;
|
||||
public static final int YELLOW1 = 0xFFFF00;
|
||||
public static final int ANTIQUEWHITE2 = 0xEEDFCC;
|
||||
public static final int AQUAMARINE2 = 0x76EEC6;
|
||||
public static final int AZURE2 = 0xE0EEEE;
|
||||
public static final int BISQUE2 = 0xEED5B7;
|
||||
public static final int BLUE2 = 0x0000EE;
|
||||
public static final int BROWN2 = 0xEE3B3B;
|
||||
public static final int BURLYWOOD2 = 0xEEC591;
|
||||
public static final int CADETBLUE2 = 0x8EE5EE;
|
||||
public static final int CHARTREUSE2 = 0x76EE00;
|
||||
public static final int CHOCOLATE2 = 0xEE7621;
|
||||
public static final int CORAL2 = 0xEE6A50;
|
||||
public static final int CORNSILK2 = 0xEEE8CD;
|
||||
public static final int CYAN2 = 0x00EEEE;
|
||||
public static final int DARKGOLDENROD2 = 0xEEAD0E;
|
||||
public static final int DARKOLIVE2 = 0xBCEE68;
|
||||
public static final int DARKORANGE2 = 0xEE7600;
|
||||
public static final int DARKORCHID2 = 0xB23AEE;
|
||||
public static final int DARKSEAGREEN2 = 0xB4EEB4;
|
||||
public static final int DARKSLATEGRAY2 = 0x8DEEEE;
|
||||
public static final int DEEPPINK2 = 0xEE1289;
|
||||
public static final int DEEPSKYBLUE2 = 0x00B2EE;
|
||||
public static final int DODGERBLUE2 = 0x1C86EE;
|
||||
public static final int FIREBRICK2 = 0xEE2C2C;
|
||||
public static final int GOLD2 = 0xEEC900;
|
||||
public static final int GOLDENROD2 = 0xEEB422;
|
||||
public static final int GREEN2 = 0x00EE00;
|
||||
public static final int HONEYDEW2 = 0xE0EEE0;
|
||||
public static final int HOTPINK2 = 0xEE6AA7;
|
||||
public static final int INDIANRED2 = 0xEE6363;
|
||||
public static final int IVORY2 = 0xEEEEE0;
|
||||
public static final int KHAKI2 = 0xEEE685;
|
||||
public static final int LAVENDERBLUSH2 = 0xEEE0E5;
|
||||
public static final int LEMONCHIFFON2 = 0xEEE9BF;
|
||||
public static final int LIGHTBLUE2 = 0xB2DFEE;
|
||||
public static final int LIGHTCYAN2 = 0xD1EEEE;
|
||||
public static final int LIGHTGOLDENROD2 = 0xEEDC82;
|
||||
public static final int LIGHTPINK2 = 0xEEA2AD;
|
||||
public static final int LIGHTSALMON2 = 0xEE9572;
|
||||
public static final int LIGHTSKYBLUE2 = 0xA4D3EE;
|
||||
public static final int LIGHTSTEELBLUE2 = 0xBCD2EE;
|
||||
public static final int LIGHTYELLOW2 = 0xEEEED1;
|
||||
public static final int MAGENTA2 = 0xEE00EE;
|
||||
public static final int MAROON2 = 0xEE30A7;
|
||||
public static final int MEDIUMORCHID2 = 0xD15FEE;
|
||||
public static final int MEDIUMPURPLE2 = 0x9F79EE;
|
||||
public static final int MISTYROSE2 = 0xEED5D2;
|
||||
public static final int NAVAJOWHITE2 = 0xEECFA1;
|
||||
public static final int OLIVEDRAB2 = 0xB3EE3A;
|
||||
public static final int ORANGE2 = 0xEE9A00;
|
||||
public static final int ORCHID2 = 0xEE7AE9;
|
||||
public static final int PALEGREEN2 = 0x90EE90;
|
||||
public static final int PALETURQUOISE2 = 0xAEEEEE;
|
||||
public static final int PALEVIOLETRED2 = 0xEE799F;
|
||||
public static final int PEACHPUFF2 = 0xEECBAD;
|
||||
public static final int PINK2 = 0xEEA9B8;
|
||||
public static final int PLUM2 = 0xEEAEEE;
|
||||
public static final int PURPLE2 = 0x912CEE;
|
||||
public static final int RED2 = 0xEE0000;
|
||||
public static final int ROSYBROWN2 = 0xEEB4B4;
|
||||
public static final int ROYALBLUE2 = 0x436EEE;
|
||||
public static final int SALMON2 = 0xEE8262;
|
||||
public static final int SEAGREEN2 = 0x4EEE94;
|
||||
public static final int SEASHELL2 = 0xEEE5DE;
|
||||
public static final int SIENNA2 = 0xEE7942;
|
||||
public static final int SKYBLUE2 = 0x7EC0EE;
|
||||
public static final int SLATEBLUE2 = 0x7A67EE;
|
||||
public static final int SLATEGRAY2 = 0xB9D3EE;
|
||||
public static final int SNOW2 = 0xEEE9E9;
|
||||
public static final int SPRINGGREEN2 = 0x00EE76;
|
||||
public static final int STEELBLUE2 = 0x5CACEE;
|
||||
public static final int TAN2 = 0xEE9A49;
|
||||
public static final int THISTLE2 = 0xEED2EE;
|
||||
public static final int TOMATO2 = 0xEE5C42;
|
||||
public static final int TURQUOISE2 = 0x00E5EE;
|
||||
public static final int VIOLETRED2 = 0xEE3A8C;
|
||||
public static final int WHEAT2 = 0xEED8AE;
|
||||
public static final int YELLOW2 = 0xEEEE00;
|
||||
public static final int ANTIQUEWHITE3 = 0xCDC0B0;
|
||||
public static final int AQUAMARINE3 = 0x66CDAA;
|
||||
public static final int AZURE3 = 0xC1CDCD;
|
||||
public static final int BISQUE3 = 0xCDB79E;
|
||||
public static final int BLUE3 = 0x0000CD;
|
||||
public static final int BROWN3 = 0xCD3333;
|
||||
public static final int BURLYWOOD3 = 0xCDAA7D;
|
||||
public static final int CADETBLUE3 = 0x7AC5CD;
|
||||
public static final int CHARTREUSE3 = 0x66CD00;
|
||||
public static final int CHOCOLATE3 = 0xCD661D;
|
||||
public static final int CORAL3 = 0xCD5B45;
|
||||
public static final int CORNSILK3 = 0xCDC8B1;
|
||||
public static final int CYAN3 = 0x00CDCD;
|
||||
public static final int DARKGOLDENROD3 = 0xCD950C;
|
||||
public static final int DARKOLIVE3 = 0xA2CD5A;
|
||||
public static final int DARKORANGE3 = 0xCD6600;
|
||||
public static final int DARKORCHID3 = 0x9A32CD;
|
||||
public static final int DARKSEAGREEN3 = 0x9BCD9B;
|
||||
public static final int DARKSLATEGRAY3 = 0x79CDCD;
|
||||
public static final int DEEPPINK3 = 0xCD1076;
|
||||
public static final int DEEPSKYBLUE3 = 0x009ACD;
|
||||
public static final int DODGERBLUE3 = 0x1874CD;
|
||||
public static final int FIREBRICK3 = 0xCD2626;
|
||||
public static final int GOLD3 = 0xCDAD00;
|
||||
public static final int GOLDENROD3 = 0xCD9B1D;
|
||||
public static final int GREEN3 = 0x00CD00;
|
||||
public static final int HONEYDEW3 = 0xC1CDC1;
|
||||
public static final int HOTPINK3 = 0xCD6090;
|
||||
public static final int INDIANRED3 = 0xCD5555;
|
||||
public static final int IVORY3 = 0xCDCDC1;
|
||||
public static final int KHAKI3 = 0xCDC673;
|
||||
public static final int LAVENDERBLUSH3 = 0xCDC1C5;
|
||||
public static final int LEMONCHIFFON3 = 0xCDC9A5;
|
||||
public static final int LIGHTBLUE3 = 0x9AC0CD;
|
||||
public static final int LIGHTCYAN3 = 0xB4CDCD;
|
||||
public static final int LIGHTGOLDENROD3 = 0xCDBE70;
|
||||
public static final int LIGHTPINK3 = 0xCD8C95;
|
||||
public static final int LIGHTSALMON3 = 0xCD8162;
|
||||
public static final int LIGHTSKYBLUE3 = 0x8DB6CD;
|
||||
public static final int LIGHTSTEELBLUE3 = 0xA2B5CD;
|
||||
public static final int LIGHTYELLOW3 = 0xCDCDB4;
|
||||
public static final int MAGENTA3 = 0xCD00CD;
|
||||
public static final int MAROON3 = 0xCD2990;
|
||||
public static final int MEDIUMORCHID3 = 0xB452CD;
|
||||
public static final int MEDIUMPURPLE3 = 0x8968CD;
|
||||
public static final int MISTYROSE3 = 0xCDB7B5;
|
||||
public static final int NAVAJOWHITE3 = 0xCDB38B;
|
||||
public static final int OLIVEDRAB3 = 0x9ACD32;
|
||||
public static final int ORANGE3 = 0xCD8500;
|
||||
public static final int ORCHID3 = 0xCD69C9;
|
||||
public static final int PALEGREEN3 = 0x7CCD7C;
|
||||
public static final int PALETURQUOISE3 = 0x96CDCD;
|
||||
public static final int PALEVIOLETRED3 = 0xCD6889;
|
||||
public static final int PEACHPUFF3 = 0xCDAF95;
|
||||
public static final int PINK3 = 0xCD919E;
|
||||
public static final int PLUM3 = 0xCD96CD;
|
||||
public static final int PURPLE3 = 0x7D26CD;
|
||||
public static final int RED3 = 0xCD0000;
|
||||
public static final int ROSYBROWN3 = 0xCD9B9B;
|
||||
public static final int ROYALBLUE3 = 0x3A5FCD;
|
||||
public static final int SALMON3 = 0xCD7054;
|
||||
public static final int SEAGREEN3 = 0x43CD80;
|
||||
public static final int SEASHELL3 = 0xCDC5BF;
|
||||
public static final int SIENNA3 = 0xCD6839;
|
||||
public static final int SKYBLUE3 = 0x6CA6CD;
|
||||
public static final int SLATEBLUE3 = 0x6959CD;
|
||||
public static final int SLATEGRAY3 = 0x9FB6CD;
|
||||
public static final int SNOW3 = 0xCDC9C9;
|
||||
public static final int SPRINGGREEN3 = 0x00CD66;
|
||||
public static final int STEELBLUE3 = 0x4F94CD;
|
||||
public static final int TAN3 = 0xCD853F;
|
||||
public static final int THISTLE3 = 0xCDB5CD;
|
||||
public static final int TOMATO3 = 0xCD4F39;
|
||||
public static final int TURQUOISE3 = 0x00C5CD;
|
||||
public static final int VIOLETRED3 = 0xCD3278;
|
||||
public static final int WHEAT3 = 0xCDBA96;
|
||||
public static final int YELLOW3 = 0xCDCD00;
|
||||
public static final int ANTIQUEWHITE4 = 0x8B8378;
|
||||
public static final int AQUAMARINE4 = 0x458B74;
|
||||
public static final int AZURE4 = 0x838B8B;
|
||||
public static final int BISQUE4 = 0x8B7D6B;
|
||||
public static final int BLUE4 = 0x00008B;
|
||||
public static final int BROWN4 = 0x8B2323;
|
||||
public static final int BURLYWOOD4 = 0x8B7355;
|
||||
public static final int CADETBLUE4 = 0x53868B;
|
||||
public static final int CHARTREUSE4 = 0x458B00;
|
||||
public static final int CHOCOLATE4 = 0x8B4513;
|
||||
public static final int CORAL4 = 0x8B3E2F;
|
||||
public static final int CORNSILK4 = 0x8B8878;
|
||||
public static final int CYAN4 = 0x008B8B;
|
||||
public static final int DARKGOLDENROD4 = 0x8B6508;
|
||||
public static final int DARKOLIVE4 = 0x6E8B3D;
|
||||
public static final int DARKORANGE4 = 0x8B4500;
|
||||
public static final int DARKORCHID4 = 0x68228B;
|
||||
public static final int DARKSEAGREEN4 = 0x698B69;
|
||||
public static final int DARKSLATEGRAY4 = 0x528B8B;
|
||||
public static final int DEEPPINK4 = 0x8B0A50;
|
||||
public static final int DEEPSKYBLUE4 = 0x00688B;
|
||||
public static final int DODGERBLUE4 = 0x104E8B;
|
||||
public static final int FIREBRICK4 = 0x8B1A1A;
|
||||
public static final int GOLD4 = 0x8B7500;
|
||||
public static final int GOLDENROD4 = 0x8B6914;
|
||||
public static final int GREEN4 = 0x008B00;
|
||||
public static final int HONEYDEW4 = 0x838B83;
|
||||
public static final int HOTPINK4 = 0x8B3A62;
|
||||
public static final int INDIANRED4 = 0x8B3A3A;
|
||||
public static final int IVORY4 = 0x8B8B83;
|
||||
public static final int KHAKI4 = 0x8B864E;
|
||||
public static final int LAVENDERBLUSH4 = 0x8B8386;
|
||||
public static final int LEMONCHIFFON4 = 0x8B8970;
|
||||
public static final int LIGHTBLUE4 = 0x68838B;
|
||||
public static final int LIGHTCYAN4 = 0x7A8B8B;
|
||||
public static final int LIGHTGOLDENROD4 = 0x8B814C;
|
||||
public static final int LIGHTPINK4 = 0x8B5F65;
|
||||
public static final int LIGHTSALMON4 = 0x8B5742;
|
||||
public static final int LIGHTSKYBLUE4 = 0x607B8B;
|
||||
public static final int LIGHTSTEELBLUE4 = 0x6E7B8B;
|
||||
public static final int LIGHTYELLOW4 = 0x8B8B7A;
|
||||
public static final int MAGENTA4 = 0x8B008B;
|
||||
public static final int MAROON4 = 0x8B1C62;
|
||||
public static final int MEDIUMORCHID4 = 0x7A378B;
|
||||
public static final int MEDIUMPURPLE4 = 0x5D478B;
|
||||
public static final int MISTYROSE4 = 0x8B7D7B;
|
||||
public static final int NAVAJOWHITE4 = 0x8B795E;
|
||||
public static final int OLIVEDRAB4 = 0x698B22;
|
||||
public static final int ORANGE4 = 0x8B5A00;
|
||||
public static final int ORCHID4 = 0x8B4789;
|
||||
public static final int PALEGREEN4 = 0x548B54;
|
||||
public static final int PALETURQUOISE4 = 0x668B8B;
|
||||
public static final int PALEVIOLETRED4 = 0x8B475D;
|
||||
public static final int PEACHPUFF4 = 0x8B7765;
|
||||
public static final int PINK4 = 0x8B636C;
|
||||
public static final int PLUM4 = 0x8B668B;
|
||||
public static final int PURPLE4 = 0x551A8B;
|
||||
public static final int RED4 = 0x8B0000;
|
||||
public static final int ROSYBROWN4 = 0x8B6969;
|
||||
public static final int ROYALBLUE4 = 0x27408B;
|
||||
public static final int SALMON4 = 0x8B4C39;
|
||||
public static final int SEAGREEN4 = 0x2E8B57;
|
||||
public static final int SEASHELL4 = 0x8B8682;
|
||||
public static final int SIENNA4 = 0x8B4726;
|
||||
public static final int SKYBLUE4 = 0x4A708B;
|
||||
public static final int SLATEBLUE4 = 0x473C8B;
|
||||
public static final int SLATEGRAY4 = 0x6C7B8B;
|
||||
public static final int SNOW4 = 0x8B8989;
|
||||
public static final int SPRINGGREEN4 = 0x008B45;
|
||||
public static final int STEELBLUE4 = 0x36648B;
|
||||
public static final int TAN4 = 0x8B5A2B;
|
||||
public static final int THISTLE4 = 0x8B7B8B;
|
||||
public static final int TOMATO4 = 0x8B3626;
|
||||
public static final int TURQUOISE4 = 0x00868B;
|
||||
public static final int VIOLETRED4 = 0x8B2252;
|
||||
public static final int WHEAT4 = 0x8B7E66;
|
||||
public static final int YELLOW4 = 0x8B8B00;
|
||||
|
||||
// basic color types
|
||||
public final static int WHITE = 0xffffff;
|
||||
public final static int BLACK = 0x000000;
|
||||
|
||||
// grays
|
||||
public final static int GREY0 = 0x000000;
|
||||
public final static int GREY3 = 0x080808;
|
||||
public final static int GREY7 = 0x121212;
|
||||
public final static int GREY11 = 0x1c1c1c;
|
||||
public final static int GREY15 = 0x262626;
|
||||
public final static int GREY19 = 0x303030;
|
||||
public final static int GREY23 = 0x3a3a3a;
|
||||
public final static int GREY27 = 0x444444;
|
||||
public final static int GREY30 = 0x4e4e4e;
|
||||
public final static int GREY35 = 0x585858;
|
||||
public final static int GREY37 = 0x5f5f5f;
|
||||
public final static int GREY39 = 0x626262;
|
||||
public final static int GREY42 = 0x6c6c6c;
|
||||
public final static int GREY46 = 0x767676;
|
||||
public final static int GREY50 = 0x808080;
|
||||
public final static int GREY53 = 0x878787;
|
||||
public final static int GREY54 = 0x8a8a8a;
|
||||
public final static int GREY58 = 0x949494;
|
||||
public final static int GREY62 = 0x9e9e9e;
|
||||
public final static int GREY63 = 0xaf87af;
|
||||
public final static int GREY66 = 0xa8a8a8;
|
||||
public final static int GREY69 = 0xafafaf;
|
||||
public final static int GREY70 = 0xb2b2b2;
|
||||
public final static int GREY74 = 0xbcbcbc;
|
||||
public final static int GREY78 = 0xc6c6c6;
|
||||
public final static int GREY82 = 0xd0d0d0;
|
||||
public final static int GREY84 = 0xd7d7d7;
|
||||
public final static int GREY85 = 0xdadada;
|
||||
public final static int GREY89 = 0xe4e4e4;
|
||||
public final static int GREY93 = 0xeeeeee;
|
||||
public final static int GREY100 = 0xffffff;
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
/*
|
||||
* 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.screen;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.jline.utils.AttributedString;
|
||||
import org.jline.utils.AttributedStringBuilder;
|
||||
import org.jline.utils.AttributedStyle;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.shell.component.view.geom.HorizontalAlign;
|
||||
import org.springframework.shell.component.view.geom.Position;
|
||||
import org.springframework.shell.component.view.geom.Rectangle;
|
||||
import org.springframework.shell.component.view.geom.VerticalAlign;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Default implementation of a {@link Screen}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class DefaultScreen implements Screen, DisplayLines {
|
||||
|
||||
private final static Logger log = LoggerFactory.getLogger(DefaultScreen.class);
|
||||
private boolean showCursor;
|
||||
private Position cursorPosition = new Position(0, 0);
|
||||
private int rows = 0;
|
||||
private int columns = 0;
|
||||
|
||||
public DefaultScreen() {
|
||||
this(0, 0);
|
||||
}
|
||||
|
||||
public DefaultScreen(int rows, int columns) {
|
||||
resize(rows, columns);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WriterBuilder writerBuilder() {
|
||||
return new DefaultWriterBuilder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setShowCursor(boolean showCursor) {
|
||||
this.showCursor = showCursor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isShowCursor() {
|
||||
return showCursor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCursorPosition(Position cursorPosition) {
|
||||
this.cursorPosition = cursorPosition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Position getCursorPosition() {
|
||||
return cursorPosition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resize(int rows, int columns) {
|
||||
Assert.isTrue(rows > -1, "Cannot have negative rows size");
|
||||
Assert.isTrue(columns > -1, "Cannot have negative columns size");
|
||||
this.rows = rows;
|
||||
this.columns = columns;
|
||||
reset();
|
||||
log.trace("Screen reset rows={} cols={}", this.rows, this.columns);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScreenItem[][] getItems() {
|
||||
return getScreenItems();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Screen clip(int x, int y, int width, int height) {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static char[] BOX_CHARS = new char[] { ' ', '╴', '╵', '┌', '╶', '─', '┐', '┬', '╷', '└', '│', '├', '┘', '┴',
|
||||
'┤', '┼' };
|
||||
|
||||
@Override
|
||||
public List<AttributedString> getScreenLines() {
|
||||
List<AttributedString> newLines = new ArrayList<>();
|
||||
ScreenItem[][] items = getScreenItems();
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
AttributedStringBuilder builder = new AttributedStringBuilder();
|
||||
for (int j = 0; j < items[i].length; j++) {
|
||||
DefaultScreenItem item = (DefaultScreenItem) items[i][j];
|
||||
if (item != null) {
|
||||
AttributedStyle s = new AttributedStyle(AttributedStyle.DEFAULT);
|
||||
if (item.background > -1) {
|
||||
s = s.backgroundRgb(item.getBackground());
|
||||
}
|
||||
if (item.foreground > -1) {
|
||||
s = s.foregroundRgb(item.getForeground());
|
||||
}
|
||||
if (item.style > -1) {
|
||||
if ((item.style & ScreenItem.STYLE_BOLD) == ScreenItem.STYLE_BOLD) {
|
||||
s = s.bold();
|
||||
}
|
||||
if ((item.style & ScreenItem.STYLE_FAINT) == ScreenItem.STYLE_FAINT) {
|
||||
s = s.faint();
|
||||
}
|
||||
if ((item.style & ScreenItem.STYLE_ITALIC) == ScreenItem.STYLE_ITALIC) {
|
||||
s = s.italic();
|
||||
}
|
||||
if ((item.style & ScreenItem.STYLE_UNDERLINE) == ScreenItem.STYLE_UNDERLINE) {
|
||||
s = s.underline();
|
||||
}
|
||||
if ((item.style & ScreenItem.STYLE_BLINK) == ScreenItem.STYLE_BLINK) {
|
||||
s = s.blink();
|
||||
}
|
||||
if ((item.style & ScreenItem.STYLE_INVERSE) == ScreenItem.STYLE_INVERSE) {
|
||||
s = s.inverse();
|
||||
}
|
||||
if ((item.style & ScreenItem.STYLE_CONCEAL) == ScreenItem.STYLE_CONCEAL) {
|
||||
s = s.conceal();
|
||||
}
|
||||
if ((item.style & ScreenItem.STYLE_CROSSEDOUT) == ScreenItem.STYLE_CROSSEDOUT) {
|
||||
s = s.crossedOut();
|
||||
}
|
||||
}
|
||||
if (item.getContent() != null){
|
||||
builder.append(item.getContent(), s);
|
||||
}
|
||||
else if (item.getBorder() > 0) {
|
||||
builder.append(Character.toString(BOX_CHARS[item.getBorder()]), s);
|
||||
}
|
||||
else {
|
||||
builder.append(Character.toString(' '), s);
|
||||
}
|
||||
}
|
||||
else {
|
||||
builder.append(' ');
|
||||
}
|
||||
}
|
||||
newLines.add(builder.toAttributedString());
|
||||
}
|
||||
return newLines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default private implementation of a {@link ScreenItem}.
|
||||
*/
|
||||
private static class DefaultScreenItem implements ScreenItem {
|
||||
|
||||
CharSequence content;
|
||||
int foreground = -1;
|
||||
int background = -1;
|
||||
int style = -1;
|
||||
int border;
|
||||
|
||||
@Override
|
||||
public CharSequence getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBorder() {
|
||||
return border;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBackground() {
|
||||
return background;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getForeground() {
|
||||
return foreground;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getStyle() {
|
||||
return style;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Default private implementation of a {@link WriterBuilder}.
|
||||
*/
|
||||
private class DefaultWriterBuilder implements WriterBuilder {
|
||||
|
||||
int layer;
|
||||
int color = -1;
|
||||
int style = -1;
|
||||
|
||||
@Override
|
||||
public Writer build() {
|
||||
return new DefaultWriter(layer, color, style);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WriterBuilder layer(int index) {
|
||||
this.layer = index;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WriterBuilder color(int color) {
|
||||
this.color = color;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WriterBuilder style(int style) {
|
||||
this.style = style;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
layers.clear();
|
||||
// DefaultScreenItem[][] layer0 = layerItems.computeIfAbsent(0, l -> {
|
||||
// return new DefaultScreenItem[rows][columns];
|
||||
// });
|
||||
// this.items = new DefaultScreenItem[rows][columns];
|
||||
for (int i = 0; i < rows; i++) {
|
||||
// this.items[i] = new DefaultScreenItem[columns];
|
||||
// layer0[i] = new DefaultScreenItem[columns];
|
||||
|
||||
for (int j = 0; j < columns; j++) {
|
||||
// this.items[i][j] = new DefaultScreenItem();
|
||||
// layer0[i][j] = new DefaultScreenItem();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class Layer {
|
||||
DefaultScreenItem[][] items = new DefaultScreenItem[rows][columns];
|
||||
|
||||
DefaultScreenItem getScreenItem(int x, int y) {
|
||||
// if (items[y] == null) {
|
||||
// items[y] = new DefaultScreenItem[columns];
|
||||
// }
|
||||
if (items[y][x] == null) {
|
||||
items[y][x] = new DefaultScreenItem();
|
||||
}
|
||||
return items[y][x];
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Integer, Layer> layers = new TreeMap<>();
|
||||
|
||||
private Layer getLayer(int index) {
|
||||
Layer layer = layers.computeIfAbsent(index, l -> {
|
||||
return new Layer();
|
||||
});
|
||||
return layer;
|
||||
}
|
||||
|
||||
// @Override
|
||||
public ScreenItem[][] getScreenItems() {
|
||||
DefaultScreenItem[][] projection = new DefaultScreenItem[rows][columns];
|
||||
layers.entrySet().stream().forEach(entry -> {
|
||||
Layer layer = entry.getValue();
|
||||
DefaultScreenItem[][] layerItems = layer.items;
|
||||
for (int i = 0; i < rows; i++) {
|
||||
// if (projection[i] == null) {
|
||||
// projection[i] = new DefaultScreenItem[columns];
|
||||
// }
|
||||
|
||||
for (int j = 0; j < columns; j++) {
|
||||
if (layerItems[i][j] != null) {
|
||||
projection[i][j] = layerItems[i][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
return projection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default private implementation of a {@link Writer}.
|
||||
*/
|
||||
private class DefaultWriter implements Writer {
|
||||
|
||||
int index;
|
||||
int color = -1;
|
||||
int style = -1;
|
||||
|
||||
DefaultWriter(int index, int color, int style) {
|
||||
this.index = index;
|
||||
this.color = color;
|
||||
this.style = style;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void text(String text, int x, int y) {
|
||||
Layer layer = getLayer(index);
|
||||
for (int i = 0; i < text.length() && i < columns; i++) {
|
||||
char c = text.charAt(i);
|
||||
DefaultScreenItem item = layer.getScreenItem(x + i, y);
|
||||
item.content = Character.toString(c);
|
||||
if (color > -1) {
|
||||
item.foreground = color;
|
||||
}
|
||||
if (style > -1) {
|
||||
item.style = style;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void border(int x, int y, int width, int height) {
|
||||
log.trace("PrintBorder rows={}, columns={}, x={}, y={}, width={}, height={}", rows, columns, x, y, width,
|
||||
height);
|
||||
printBorderHorizontal(x, y, width);
|
||||
printBorderHorizontal(x, y + height - 1, width);
|
||||
printBorderVertical(x, y, height);
|
||||
printBorderVertical(x + width - 1, y, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void background(Rectangle rect, int color) {
|
||||
log.trace("Background {} {}", color, rect);
|
||||
Layer layer = getLayer(index);
|
||||
for (int i = rect.y(); i < rect.y() + rect.height(); i++) {
|
||||
for (int j = rect.x(); j < rect.x() + rect.width(); j++) {
|
||||
DefaultScreenItem item = layer.getScreenItem(j, i);
|
||||
item.background = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void text(String text, Rectangle rect, HorizontalAlign hAlign, VerticalAlign vAlign) {
|
||||
int x = rect.x();
|
||||
if (hAlign == HorizontalAlign.CENTER) {
|
||||
x = (x + rect.width()) / 2;
|
||||
x = x - text.length() / 2;
|
||||
}
|
||||
else if (hAlign == HorizontalAlign.RIGHT) {
|
||||
x = x + rect.width() - text.length();
|
||||
}
|
||||
int y = rect.y();
|
||||
if (vAlign == VerticalAlign.CENTER) {
|
||||
y = (y + rect.height()) / 2;
|
||||
}
|
||||
else if (vAlign == VerticalAlign.BOTTOM) {
|
||||
y = y + rect.height() - 1;
|
||||
}
|
||||
text(text, x, y);
|
||||
}
|
||||
|
||||
private void printBorderHorizontal(int x, int y, int width) {
|
||||
Layer layer = getLayer(index);
|
||||
for (int i = x; i < x + width; i++) {
|
||||
if (i < 0 || i >= columns) {
|
||||
continue;
|
||||
}
|
||||
if (y >= rows) {
|
||||
continue;
|
||||
}
|
||||
DefaultScreenItem item = layer.getScreenItem(i, y);
|
||||
if (i > x) {
|
||||
item.border |= ScreenItem.BORDER_RIGHT;
|
||||
}
|
||||
if (i < x + width - 1) {
|
||||
item.border |= ScreenItem.BORDER_LEFT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void printBorderVertical(int x, int y, int height) {
|
||||
Layer layer = getLayer(index);
|
||||
for (int i = y; i < y + height; i++) {
|
||||
if (i < 0 || i >= rows) {
|
||||
continue;
|
||||
}
|
||||
if (x >= columns) {
|
||||
continue;
|
||||
}
|
||||
DefaultScreenItem item = layer.getScreenItem(x, i);
|
||||
if (i > y) {
|
||||
item.border |= ScreenItem.BORDER_BOTTOM;
|
||||
}
|
||||
if (i < y + height - 1) {
|
||||
item.border |= ScreenItem.BORDER_TOP;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.screen;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.jline.utils.AttributedString;
|
||||
import org.jline.utils.Display;
|
||||
|
||||
/**
|
||||
* Interface for an implementation which is able to return list of
|
||||
* {@link AttributedString} usually used together with {@link Display}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface DisplayLines {
|
||||
|
||||
/**
|
||||
* Gets a list of screen lines.
|
||||
*
|
||||
* @return list of screen lines
|
||||
*/
|
||||
List<AttributedString> getScreenLines();
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* 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.screen;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.shell.component.view.geom.HorizontalAlign;
|
||||
import org.springframework.shell.component.view.geom.Position;
|
||||
import org.springframework.shell.component.view.geom.Rectangle;
|
||||
import org.springframework.shell.component.view.geom.VerticalAlign;
|
||||
|
||||
/**
|
||||
* {@code Screen} is representing a virtual area which is sitting between a user
|
||||
* and lower level {@code jline} terminal providing convenient methods working
|
||||
* with visible content.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface Screen {
|
||||
|
||||
/**
|
||||
* Sets if cursor should be visible.
|
||||
*
|
||||
* @param show true if cursor should be visible
|
||||
*/
|
||||
void setShowCursor(boolean show);
|
||||
|
||||
/**
|
||||
* Gets if cursor is visible.
|
||||
*
|
||||
* @return true if cursor is visible
|
||||
*/
|
||||
boolean isShowCursor();
|
||||
|
||||
/**
|
||||
* Sets a cursor position.
|
||||
*
|
||||
* @param position new cursor position
|
||||
*/
|
||||
void setCursorPosition(Position position);
|
||||
|
||||
/**
|
||||
* Gets a cursor position.
|
||||
*
|
||||
* @return cursor position
|
||||
*/
|
||||
Position getCursorPosition();
|
||||
|
||||
/**
|
||||
* Gets a new instance of a {@link WriterBuilder}.
|
||||
*
|
||||
* @return a new writer builder
|
||||
*/
|
||||
WriterBuilder writerBuilder();
|
||||
|
||||
/**
|
||||
* Resize a screen.
|
||||
*
|
||||
* @param rows the new row count
|
||||
* @param columns the new column count
|
||||
*/
|
||||
void resize(int rows, int columns);
|
||||
|
||||
/**
|
||||
* Gets a screen items.
|
||||
*
|
||||
* @return a screen items
|
||||
*/
|
||||
ScreenItem[][] getItems();
|
||||
|
||||
/**
|
||||
* Clip a screen with a given bounds.
|
||||
*
|
||||
* @param x the x coordinate
|
||||
* @param y the y coordinate
|
||||
* @param width the width
|
||||
* @param height the height
|
||||
* @return new clipped screen
|
||||
*/
|
||||
Screen clip(int x, int y, int width, int height);
|
||||
|
||||
/**
|
||||
* Interface to write into a {@link Screen}. Contains convenient methods user is
|
||||
* most likely to need to operate on a {@link Screen}.
|
||||
*/
|
||||
interface Writer {
|
||||
|
||||
/**
|
||||
* Write a text horizontally starting from a position defined by {@code x} and
|
||||
* {@code y} within a bounds of a {@link Screen}.
|
||||
*
|
||||
* @param text the text to write
|
||||
* @param x the x position
|
||||
* @param y the y position
|
||||
*/
|
||||
void text(String text, int x, int y);
|
||||
|
||||
/**
|
||||
* Write a border with a given rectangle coordinates.
|
||||
*
|
||||
* @param x the x position
|
||||
* @param y the y position
|
||||
* @param width the rectangle width
|
||||
* @param height the rectangle height
|
||||
*/
|
||||
void border(int x, int y, int width, int height);
|
||||
|
||||
/**
|
||||
* Fill background with a given color.
|
||||
*
|
||||
* @param rect the rectange to fill
|
||||
* @param color the color to use
|
||||
*/
|
||||
void background(Rectangle rect, int color);
|
||||
|
||||
/**
|
||||
* Write aligned text within a bounds.
|
||||
*
|
||||
* @param text the text to write
|
||||
* @param rect the rectangle bounds
|
||||
* @param hAlign the horizontal aligment
|
||||
* @param vAlign the vertical aligment
|
||||
*/
|
||||
void text(String text, Rectangle rect, @Nullable HorizontalAlign hAlign, @Nullable VerticalAlign vAlign);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder interface for a {@link Writer}. Allows to defined settings a builder
|
||||
* will operare on.
|
||||
*/
|
||||
interface WriterBuilder {
|
||||
|
||||
/**
|
||||
* Define a {@code z-index} this {@link Writer} operates on.
|
||||
* {@code WriterBuilder} defaults on a layer index {@code 0}.
|
||||
*
|
||||
* @param index the z-index
|
||||
* @return a writer builder for chaining
|
||||
*/
|
||||
WriterBuilder layer(int index);
|
||||
|
||||
WriterBuilder color(int color);
|
||||
WriterBuilder style(int style);
|
||||
|
||||
/**
|
||||
* Build a {@link Writer}.
|
||||
*
|
||||
* @return a build writer
|
||||
*/
|
||||
Writer build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.screen;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface ScreenItem {
|
||||
|
||||
static final int STYLE_BOLD = 1;
|
||||
static final int STYLE_FAINT = STYLE_BOLD << 1;
|
||||
static final int STYLE_ITALIC = STYLE_BOLD << 2;
|
||||
static final int STYLE_UNDERLINE = STYLE_BOLD << 3;
|
||||
static final int STYLE_BLINK = STYLE_BOLD << 4;
|
||||
static final int STYLE_INVERSE = STYLE_BOLD << 5;
|
||||
static final int STYLE_CONCEAL = STYLE_BOLD << 6;
|
||||
static final int STYLE_CROSSEDOUT = STYLE_BOLD << 7;
|
||||
|
||||
static final int BORDER_LEFT = 1;
|
||||
static final int BORDER_TOP = BORDER_LEFT << 1;
|
||||
static final int BORDER_RIGHT = BORDER_LEFT << 2;
|
||||
static final int BORDER_BOTTOM = BORDER_LEFT << 3;
|
||||
|
||||
CharSequence getContent();
|
||||
|
||||
int getBorder();
|
||||
|
||||
int getBackground();
|
||||
|
||||
int getForeground();
|
||||
|
||||
int getStyle();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.assertj.core.api.AbstractAssert;
|
||||
import org.jline.utils.AttributedString;
|
||||
|
||||
import org.springframework.shell.component.view.geom.Position;
|
||||
import org.springframework.shell.component.view.screen.DisplayLines;
|
||||
import org.springframework.shell.component.view.screen.Screen;
|
||||
import org.springframework.shell.component.view.screen.ScreenItem;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Asserts for {@link Screen}. This is a work-in-progress implementation and
|
||||
* relevant parts are copied into spring-shell-test.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class ScreenAssert extends AbstractAssert<ScreenAssert, Screen> {
|
||||
|
||||
public ScreenAssert(Screen actual) {
|
||||
super(actual, ScreenAssert.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the actual {@link Screen} has a cursor visible.
|
||||
*
|
||||
* @return this assertion object
|
||||
*/
|
||||
public ScreenAssert hasCursorVisible() {
|
||||
isNotNull();
|
||||
if (!actual.isShowCursor()) {
|
||||
failWithMessage("Expecting a Screen to have a visible cursor", actual);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the actual {@link Screen} has a cursor in a given position.
|
||||
*
|
||||
* @param x a x position in a screen
|
||||
* @param y a y position in a screen
|
||||
* @return this assertion object
|
||||
*/
|
||||
public ScreenAssert hasCursorInPosition(int x, int y) {
|
||||
isNotNull();
|
||||
Position cursorPosition = actual.getCursorPosition();
|
||||
if (cursorPosition.x() != x || cursorPosition.y() != y) {
|
||||
failWithMessage("Expecting a Screen to have position <%s,%s> but was <%s,%s>", x, y, cursorPosition.x(),
|
||||
cursorPosition.y());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the actual {@link Screen} has a foreground color in a position.
|
||||
*
|
||||
* @param x a x position in a screen
|
||||
* @param y a y position in a screen
|
||||
* @param color the color
|
||||
* @return this assertion object
|
||||
*/
|
||||
public ScreenAssert hasForegroundColor(int x, int y, int color) {
|
||||
isNotNull();
|
||||
ScreenItem[][] items = actual.getItems();
|
||||
ScreenItem i = items[y][x];
|
||||
int expectedColor = i.getForeground();
|
||||
if (expectedColor != color) {
|
||||
failWithMessage("Expecting a Screen to have foreground color <%s> position <%s,%s> but was <%s>", color, x, y,
|
||||
expectedColor);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the actual {@link Screen} has a foreground style in a position.
|
||||
*
|
||||
* @param x a x position in a screen
|
||||
* @param y a y position in a screen
|
||||
* @param color the style
|
||||
* @return this assertion object
|
||||
*/
|
||||
public ScreenAssert hasStyle(int x, int y, int style) {
|
||||
isNotNull();
|
||||
ScreenItem[][] items = actual.getItems();
|
||||
ScreenItem i = items[y][x];
|
||||
int expectedStyle = i.getStyle();
|
||||
if (expectedStyle != style) {
|
||||
failWithMessage("Expecting a Screen to have style <%s> position <%s,%s> but was <%s>", style, x, y,
|
||||
expectedStyle);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the actual {@link Screen} has a background color in a position.
|
||||
*
|
||||
* @param x a x position in a screen
|
||||
* @param y a y position in a screen
|
||||
* @param color the color
|
||||
* @return this assertion object
|
||||
*/
|
||||
public ScreenAssert hasBackgroundColor(int x, int y, int color) {
|
||||
isNotNull();
|
||||
ScreenItem[][] items = actual.getItems();
|
||||
ScreenItem i = items[y][x];
|
||||
int expectedColor = i.getBackground();
|
||||
if (expectedColor != color) {
|
||||
failWithMessage("Expecting a Screen to have background color <%s> position <%s,%s> but was <%s>", color, x, y,
|
||||
expectedColor);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a given bounded box is legal for a screen and that characters
|
||||
* along border look like border characters.
|
||||
*
|
||||
* @param x a x position in a screen
|
||||
* @param y a y position in a screen
|
||||
* @param width a width in a screen
|
||||
* @param height a height in a screen
|
||||
* @return this assertion object
|
||||
*/
|
||||
public ScreenAssert hasBorder(int x, int y, int width, int height) {
|
||||
return hasBorderType(x, y, width, height, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a given bounded box is legal for a screen and that characters
|
||||
* along border doesn't look like border characters.
|
||||
*
|
||||
* @param x a x position in a screen
|
||||
* @param y a y position in a screen
|
||||
* @param width a width in a screen
|
||||
* @param height a height in a screen
|
||||
* @return this assertion object
|
||||
*/
|
||||
public ScreenAssert hasNoBorder(int x, int y, int width, int height) {
|
||||
return hasBorderType(x, y, width, height, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a given text can be found from a screen coordinates following
|
||||
* horizontal width.
|
||||
*
|
||||
* @param text a text to verify
|
||||
* @param x a x position of a text
|
||||
* @param y a y position of a text
|
||||
* @param width a width of a text to check
|
||||
* @return this assertion object
|
||||
*/
|
||||
public ScreenAssert hasHorizontalText(String text, int x, int y, int width) {
|
||||
isNotNull();
|
||||
ScreenItem[][] content = actual.getItems();
|
||||
checkBounds(content, x, y, width, 1);
|
||||
ScreenItem[] items = getHorizontalBorder(content, y, x, width);
|
||||
StringBuilder buf = new StringBuilder();
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
if (items[i].getContent() != null) {
|
||||
buf.append(items[i].getContent());
|
||||
}
|
||||
}
|
||||
String actualText = buf.toString();
|
||||
assertThat(actualText).isEqualTo(text);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a given text can not be found from a screen coordinates following
|
||||
* horizontal width.
|
||||
*
|
||||
* @param text a text to verify
|
||||
* @param x a x position of a text
|
||||
* @param y a y position of a text
|
||||
* @param width a width of a text to check
|
||||
* @return this assertion object
|
||||
*/
|
||||
public ScreenAssert hasNoHorizontalText(String text, int x, int y, int width) {
|
||||
isNotNull();
|
||||
ScreenItem[][] content = actual.getItems();
|
||||
checkBounds(content, x, y, width, 1);
|
||||
ScreenItem[] items = getHorizontalBorder(content, x, y, width);
|
||||
StringBuilder buf = new StringBuilder();
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
if (items[i] != null) {
|
||||
if (items[i].getContent() != null) {
|
||||
buf.append(items[i].getContent());
|
||||
}
|
||||
}
|
||||
}
|
||||
String actualText = buf.toString();
|
||||
assertThat(actualText).isNotEqualTo(text);
|
||||
return this;
|
||||
}
|
||||
|
||||
private String screenError(int x, int y, int width, int height) {
|
||||
StringBuffer buf = new StringBuffer();
|
||||
if (actual instanceof DisplayLines dl1) {
|
||||
List<AttributedString> screenLines = dl1.getScreenLines();
|
||||
buf.append(String.format("%nExpecting screen:%n"));
|
||||
for (AttributedString line : screenLines) {
|
||||
buf.append(String.format("%n %s", AttributedString.stripAnsi(line.toString())));
|
||||
}
|
||||
Screen clip = actual.clip(x, y, width, height);
|
||||
if (clip instanceof DisplayLines dl2) {
|
||||
List<AttributedString> screenLines2 = dl2.getScreenLines();
|
||||
buf.append(String.format("%nhave border in bounded box x=%s y=%s width=%s height=%s, was:%n", x, y, width, height));
|
||||
for (AttributedString line : screenLines2) {
|
||||
buf.append(String.format("%n %s", AttributedString.stripAnsi(line.toString())));
|
||||
}
|
||||
}
|
||||
}
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
private ScreenAssert hasBorderType(int x, int y, int width, int height, boolean border) {
|
||||
isNotNull();
|
||||
ScreenItem[][] content = actual.getItems();
|
||||
checkBounds(content, x, y, width, height);
|
||||
ScreenItem[][] borders = getBorders(content, x, y, width, height);
|
||||
ScreenItem[] topBorder = borders[0];
|
||||
ScreenItem[] rightBorder = borders[1];
|
||||
ScreenItem[] bottomBorder = borders[2];
|
||||
ScreenItem[] leftBorder = borders[3];
|
||||
if (topBorder.length != width) {
|
||||
failWithMessage("Top Border size doesn't match");
|
||||
}
|
||||
String failMessage = screenError(x, y, width, height);
|
||||
assertThat(topBorder).withFailMessage(failMessage).allSatisfy(b -> {
|
||||
if (border) {
|
||||
assertThat(b).isNotNull();
|
||||
assertThat(b.getBorder()).isGreaterThan(0);
|
||||
}
|
||||
else {
|
||||
if (b != null) {
|
||||
assertThat(b.getBorder()).isEqualTo(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
assertThat(rightBorder).withFailMessage(failMessage).allSatisfy(b -> {
|
||||
if (border) {
|
||||
assertThat(b).isNotNull();
|
||||
assertThat(b.getBorder()).isGreaterThan(0);
|
||||
}
|
||||
else {
|
||||
if (b != null) {
|
||||
assertThat(b.getBorder()).isEqualTo(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
assertThat(bottomBorder).withFailMessage(failMessage).allSatisfy(b -> {
|
||||
if (border) {
|
||||
assertThat(b).isNotNull();
|
||||
assertThat(b.getBorder()).isGreaterThan(0);
|
||||
}
|
||||
else {
|
||||
if (b != null) {
|
||||
assertThat(b.getBorder()).isEqualTo(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
assertThat(leftBorder).withFailMessage(failMessage).allSatisfy(b -> {
|
||||
if (border) {
|
||||
assertThat(b).isNotNull();
|
||||
// assertThat(b.getType()).isEqualTo(Screen.Type.BORDER);
|
||||
assertThat(b.getBorder()).isGreaterThan(0);
|
||||
}
|
||||
else {
|
||||
if (b != null) {
|
||||
// assertThat(b.getType()).isNotEqualTo(Screen.Type.BORDER);
|
||||
assertThat(b.getBorder()).isEqualTo(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
private ScreenItem[][] getBorders(ScreenItem[][] content, int x, int y, int width, int height) {
|
||||
ScreenItem[] topBorder = getHorizontalBorder(content, y, x, width);
|
||||
ScreenItem[] rightBorder = getVerticalBorder(content, x + width - 1, y, height);
|
||||
ScreenItem[] bottomBorder = getHorizontalBorder(content, y + height - 1, x, width);
|
||||
ScreenItem[] leftBorder = getVerticalBorder(content, x, y, height);
|
||||
return new ScreenItem[][] { topBorder, rightBorder, bottomBorder, leftBorder };
|
||||
}
|
||||
|
||||
private ScreenItem[] getHorizontalBorder(ScreenItem[][] content, int row, int start, int width) {
|
||||
return Arrays.copyOfRange(content[row], start, start + width);
|
||||
}
|
||||
|
||||
private ScreenItem[] getVerticalBorder(ScreenItem[][] content, int column, int start, int height) {
|
||||
ScreenItem[] array = new ScreenItem[height];
|
||||
for (int i = 0; i < array.length; i++) {
|
||||
array[i] = content[start][column];
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
private void checkBounds(ScreenItem[][] content, int x, int y, int width, int height) {
|
||||
if (x < 0 || y < 0 || width < 1 || height < 1) {
|
||||
failWithMessage("Can't assert with negative bounded rectangle, was x=%s y=%s width=%s height=%s", x, y,
|
||||
width, height);
|
||||
}
|
||||
if (x >= content[0].length) {
|
||||
failWithMessage("Can't assert position x %s as width is %s", x, content[0].length);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import org.assertj.core.api.AssertProvider;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.shell.component.view.geom.Rectangle;
|
||||
import org.springframework.shell.component.view.screen.Color;
|
||||
import org.springframework.shell.component.view.screen.DefaultScreen;
|
||||
import org.springframework.shell.component.view.screen.Screen;
|
||||
import org.springframework.shell.component.view.screen.ScreenItem;
|
||||
import org.springframework.shell.component.view.screen.Screen.Writer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
class ScreenAssertTests {
|
||||
|
||||
@Test
|
||||
void testCursorPosition() {
|
||||
Screen screen = new DefaultScreen(5, 5);
|
||||
assertThatExceptionOfType(AssertionError.class)
|
||||
.isThrownBy(() -> assertThat(forScreen(screen)).hasCursorVisible())
|
||||
.withMessageContaining("Expecting a Screen to have a visible cursor");
|
||||
assertThatExceptionOfType(AssertionError.class)
|
||||
.isThrownBy(() -> assertThat(forScreen(screen)).hasCursorInPosition(1, 1))
|
||||
.withMessageContaining("Expecting a Screen to have position <1,1> but was <0,0>");
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasForegroundColorShouldPass() {
|
||||
Screen screen = new DefaultScreen(5, 5);
|
||||
screen.writerBuilder().color(Color.RED).build().text("test", 0, 0);
|
||||
assertThat(forScreen(screen)).hasForegroundColor(0, 0, Color.RED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasForegroundColorShouldFail() {
|
||||
Screen screen = new DefaultScreen(5, 5);
|
||||
screen.writerBuilder().color(Color.RED).build().text("test", 0, 0);
|
||||
assertThatExceptionOfType(AssertionError.class)
|
||||
.isThrownBy(() -> assertThat(forScreen(screen)).hasForegroundColor(0, 0, Color.BLUE))
|
||||
.withMessageContaining("Expecting a Screen to have foreground color <255> position <0,0> but was <16711680>");
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasStyleShouldPass() {
|
||||
Screen screen = new DefaultScreen(5, 5);
|
||||
screen.writerBuilder().style(ScreenItem.STYLE_BOLD).build().text("test", 0, 0);
|
||||
assertThat(forScreen(screen)).hasStyle(0, 0, ScreenItem.STYLE_BOLD);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasStyleShouldFail() {
|
||||
Screen screen = new DefaultScreen(5, 5);
|
||||
screen.writerBuilder().build().text("test", 0, 0);
|
||||
assertThatExceptionOfType(AssertionError.class)
|
||||
.isThrownBy(() -> assertThat(forScreen(screen)).hasStyle(0, 0, ScreenItem.STYLE_BOLD))
|
||||
.withMessageContaining("Expecting a Screen to have style <1> position <0,0> but was <-1>");
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasBackgroundColorShouldPass() {
|
||||
Screen screen = new DefaultScreen(5, 5);
|
||||
Writer writer = screen.writerBuilder().build();
|
||||
writer.background(new Rectangle(0, 0, 5, 5), Color.BLUE);
|
||||
writer.text("test", 0, 0);
|
||||
assertThat(forScreen(screen)).hasBackgroundColor(0, 0, Color.BLUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasBackgroundColorShouldFail() {
|
||||
Screen screen = new DefaultScreen(5, 5);
|
||||
Writer writer = screen.writerBuilder().build();
|
||||
writer.text("test", 0, 0);
|
||||
assertThatExceptionOfType(AssertionError.class)
|
||||
.isThrownBy(() -> assertThat(forScreen(screen)).hasBackgroundColor(0, 0, Color.BLUE))
|
||||
.withMessageContaining("Expecting a Screen to have background color <255> position <0,0> but was <-1>");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowWithInvalidBounds() {
|
||||
Screen screen = new DefaultScreen(5, 5);
|
||||
assertThatExceptionOfType(AssertionError.class)
|
||||
.isThrownBy(() -> assertThat(forScreen(screen)).hasBorder(0, 0, 0, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowWithInvalidBorder() {
|
||||
Screen screen = new DefaultScreen(5, 5);
|
||||
screen.writerBuilder().build().border(0, 0, 5, 5);
|
||||
assertThatExceptionOfType(AssertionError.class)
|
||||
.isThrownBy(() -> assertThat(forScreen(screen)).hasBorder(0, 0, 5, 4));
|
||||
assertThatExceptionOfType(AssertionError.class)
|
||||
.isThrownBy(() -> assertThat(forScreen(screen)).hasBorder(0, 0, 5, 4));
|
||||
assertThatExceptionOfType(AssertionError.class)
|
||||
.isThrownBy(() -> assertThat(forScreen(screen)).hasBorder(0, 0, 4, 4));
|
||||
assertThatExceptionOfType(AssertionError.class)
|
||||
.isThrownBy(() -> assertThat(forScreen(screen)).hasBorder(1, 1, 3, 3));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotThrowWithValidBorder() {
|
||||
Screen screen = new DefaultScreen(5, 10);
|
||||
screen.writerBuilder().build().border(0, 0, 10, 5);
|
||||
assertThat(forScreen(screen)).hasBorder(0, 0, 10, 5);
|
||||
|
||||
screen = new DefaultScreen(10, 5);
|
||||
screen.writerBuilder().build().border(0, 0, 5, 10);
|
||||
assertThat(forScreen(screen)).hasBorder(0, 0, 5, 10);
|
||||
|
||||
screen = new DefaultScreen(10, 5);
|
||||
screen.writerBuilder().build().border(1, 1, 3, 8);
|
||||
assertThat(forScreen(screen)).hasBorder(1, 1, 3, 8);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotThrowWithValidNonBorder() {
|
||||
Screen screen = new DefaultScreen(5, 10);
|
||||
screen.writerBuilder().build().border(0, 0, 10, 5);
|
||||
assertThat(forScreen(screen)).hasNoBorder(1, 1, 8, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasHorizontalText() {
|
||||
Screen screen = new DefaultScreen(5, 10);
|
||||
screen.writerBuilder().build().text("test", 0, 0);
|
||||
assertThat(forScreen(screen)).hasHorizontalText("test", 0, 0, 4);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasNoHorizontalText() {
|
||||
Screen screen = new DefaultScreen(5, 10);
|
||||
screen.writerBuilder().build().text("xxxx", 0, 0);
|
||||
assertThat(forScreen(screen)).hasNoHorizontalText("test", 0, 0, 4);
|
||||
}
|
||||
|
||||
// @Test
|
||||
// void xxx() {
|
||||
// Screen screen = new Screen(5, 5);
|
||||
// screen.printBorder(0, 0, 5, 5);
|
||||
// assertThat(forScreen(screen)).hasBorder(0, 0, 5, 4);
|
||||
// }
|
||||
|
||||
private AssertProvider<ScreenAssert> forScreen(Screen screen) {
|
||||
return () -> new ScreenAssert(screen);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 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.assertj.core.api.AssertProvider;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
||||
import org.springframework.shell.component.view.ScreenAssert;
|
||||
import org.springframework.shell.component.view.event.DefaultEventLoop;
|
||||
import org.springframework.shell.component.view.event.KeyEvent;
|
||||
import org.springframework.shell.component.view.event.KeyHandler;
|
||||
import org.springframework.shell.component.view.event.KeyHandler.KeyHandlerResult;
|
||||
import org.springframework.shell.component.view.event.MouseEvent;
|
||||
import org.springframework.shell.component.view.event.MouseHandler;
|
||||
import org.springframework.shell.component.view.event.MouseHandler.MouseHandlerResult;
|
||||
import org.springframework.shell.component.view.screen.DefaultScreen;
|
||||
import org.springframework.shell.component.view.screen.Screen;
|
||||
|
||||
public class AbstractViewTests {
|
||||
|
||||
protected Screen screen24x80;
|
||||
protected Screen screen7x10;
|
||||
protected Screen screen10x10;
|
||||
protected Screen screen0x0;
|
||||
protected DefaultEventLoop eventLoop;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
screen24x80 = new DefaultScreen(24, 80);
|
||||
screen7x10 = new DefaultScreen(7, 10);
|
||||
screen0x0 = new DefaultScreen();
|
||||
screen10x10 = new DefaultScreen(10, 10);
|
||||
eventLoop = new DefaultEventLoop();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
if (eventLoop != null) {
|
||||
eventLoop.destroy();
|
||||
}
|
||||
eventLoop = null;
|
||||
}
|
||||
|
||||
protected void configure(View view) {
|
||||
if (eventLoop != null) {
|
||||
if (view instanceof AbstractView v) {
|
||||
v.setEventLoop(eventLoop);
|
||||
}
|
||||
eventLoop.onDestroy(eventLoop.mouseEvents()
|
||||
.doOnNext(m -> {
|
||||
view.getMouseHandler().handle(MouseHandler.argsOf(m));
|
||||
})
|
||||
.subscribe());
|
||||
eventLoop.onDestroy(eventLoop.keyEvents()
|
||||
.doOnNext(m -> {
|
||||
view.getKeyHandler().handle(KeyHandler.argsOf(m));
|
||||
})
|
||||
.subscribe());
|
||||
}
|
||||
}
|
||||
|
||||
protected void dispatchEvent(View view, KeyEvent event) {
|
||||
KeyHandler keyHandler = view.getKeyHandler();
|
||||
if (keyHandler != null) {
|
||||
keyHandler.handle(new KeyHandler.KeyHandlerArgs(event));
|
||||
}
|
||||
}
|
||||
|
||||
protected AssertProvider<ScreenAssert> forScreen(Screen screen) {
|
||||
return () -> new ScreenAssert(screen);
|
||||
}
|
||||
|
||||
protected KeyHandlerResult handleKey(View view, Integer key) {
|
||||
return handleKeyEvent(view, KeyEvent.of(key));
|
||||
}
|
||||
|
||||
protected KeyHandlerResult handleKeyEvent(View view, KeyEvent key) {
|
||||
return view.getKeyHandler().handle(KeyHandler.argsOf(key));
|
||||
}
|
||||
|
||||
protected MouseEvent mouseClick(int x, int y) {
|
||||
return MouseEvent.of(x, y, MouseEvent.Type.Released | MouseEvent.Button.Button1);
|
||||
}
|
||||
|
||||
protected MouseHandlerResult handleMouseClick(View view, int x, int y) {
|
||||
MouseEvent click = mouseClick(0, 2);
|
||||
return handleMouseClick(view, click);
|
||||
}
|
||||
|
||||
protected MouseHandlerResult handleMouseClick(View view, MouseEvent click) {
|
||||
return view.getMouseHandler().handle(MouseHandler.argsOf(click));
|
||||
}
|
||||
|
||||
protected MouseHandlerResult handleMouseWheelDown(View view, int x, int y) {
|
||||
MouseEvent wheel = mouseWheelDown(x, y);
|
||||
return view.getMouseHandler().handle(MouseHandler.argsOf(wheel));
|
||||
}
|
||||
|
||||
protected MouseHandlerResult handleMouseWheelUp(View view, int x, int y) {
|
||||
MouseEvent wheel = mouseWheelUp(x, y);
|
||||
return view.getMouseHandler().handle(MouseHandler.argsOf(wheel));
|
||||
}
|
||||
|
||||
protected MouseEvent mouseWheelUp(int x, int y) {
|
||||
return MouseEvent.of(x, y, MouseEvent.Type.Wheel | MouseEvent.Button.WheelUp);
|
||||
}
|
||||
|
||||
protected MouseEvent mouseWheelDown(int x, int y) {
|
||||
return MouseEvent.of(x, y, MouseEvent.Type.Wheel | MouseEvent.Button.WheelDown);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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.Test;
|
||||
|
||||
import org.springframework.shell.component.view.event.MouseEvent;
|
||||
import org.springframework.shell.component.view.event.MouseHandler;
|
||||
import org.springframework.shell.component.view.event.MouseHandler.MouseHandlerResult;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class BoxViewTests extends AbstractViewTests {
|
||||
|
||||
@Test
|
||||
void hasBorder() {
|
||||
BoxView view = new BoxView();
|
||||
view.setShowBorder(true);
|
||||
view.setRect(0, 0, 80, 24);
|
||||
view.draw(screen24x80);
|
||||
assertThat(forScreen(screen24x80)).hasBorder(0, 0, 80, 24);
|
||||
|
||||
BoxView view2 = new BoxView();
|
||||
view2.setShowBorder(true);
|
||||
view2.setRect(0, 0, 10, 7);
|
||||
view2.draw(screen7x10);
|
||||
assertThat(forScreen(screen7x10)).hasBorder(0, 0, 10, 7);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasNoBorder() {
|
||||
BoxView view = new BoxView();
|
||||
view.setShowBorder(false);
|
||||
view.setRect(0, 0, 80, 24);
|
||||
view.draw(screen24x80);
|
||||
assertThat(forScreen(screen24x80)).hasNoBorder(0, 0, 80, 24);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasTitle() {
|
||||
BoxView view = new BoxView();
|
||||
view.setShowBorder(true);
|
||||
view.setTitle("title");
|
||||
view.setRect(0, 0, 80, 24);
|
||||
view.draw(screen24x80);
|
||||
assertThat(forScreen(screen24x80)).hasHorizontalText("title", 1, 0, 5);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasNoTitleWhenNoBorder() {
|
||||
BoxView view = new BoxView();
|
||||
view.setShowBorder(false);
|
||||
view.setTitle("title");
|
||||
view.setRect(0, 0, 80, 24);
|
||||
view.draw(screen24x80);
|
||||
assertThat(forScreen(screen24x80)).hasNoHorizontalText("title", 0, 1, 5);
|
||||
}
|
||||
|
||||
@Test
|
||||
void mouseClickInBounds() {
|
||||
BoxView view = new BoxView();
|
||||
configure(view);
|
||||
view.setRect(0, 0, 80, 24);
|
||||
MouseEvent event = mouseClick(0, 0);
|
||||
MouseHandlerResult result = view.getMouseHandler().handle(MouseHandler.argsOf(event));
|
||||
assertThat(result).isNotNull().satisfies(r -> {
|
||||
assertThat(r.event()).isEqualTo(event);
|
||||
// assertThat(r.consumed()).isTrue();
|
||||
// assertThat(r.focus()).isEqualTo(view);
|
||||
assertThat(r.capture()).isEqualTo(view);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void mouseClickOutOfBounds() {
|
||||
BoxView view = new BoxView();
|
||||
view.setRect(0, 0, 80, 24);
|
||||
MouseEvent event = mouseClick(100, 100);
|
||||
MouseHandlerResult result = view.getMouseHandler().handle(MouseHandler.argsOf(event));
|
||||
assertThat(result).isNotNull().satisfies(r -> {
|
||||
assertThat(r.event()).isEqualTo(event);
|
||||
assertThat(r.consumed()).isFalse();
|
||||
assertThat(r.focus()).isNull();
|
||||
assertThat(r.capture()).isEqualTo(view);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* 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.Test;
|
||||
|
||||
import org.springframework.shell.component.view.screen.DefaultScreen;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class GridViewTests extends AbstractViewTests {
|
||||
|
||||
@Test
|
||||
void hasBordersWith1x1() {
|
||||
BoxView box1 = new BoxView();
|
||||
|
||||
GridView grid = new GridView();
|
||||
grid.setShowBorders(true);
|
||||
grid.setRowSize(0);
|
||||
grid.setColumnSize(0);
|
||||
grid.setShowBorder(false);
|
||||
grid.addItem(box1, 0, 0, 1, 1, 0, 0);
|
||||
|
||||
grid.setRect(0, 0, 80, 24);
|
||||
grid.draw(screen24x80);
|
||||
|
||||
assertThat(forScreen(screen24x80)).hasBorder(0, 0, 80, 24);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasBordersWith1x2() {
|
||||
BoxView box1 = new BoxView();
|
||||
BoxView box2 = new BoxView();
|
||||
|
||||
GridView grid = new GridView();
|
||||
grid.setShowBorders(true);
|
||||
grid.setRowSize(0);
|
||||
grid.setColumnSize(0, 0);
|
||||
grid.setShowBorder(false);
|
||||
grid.addItem(box1, 0, 0, 1, 1, 0, 0);
|
||||
grid.addItem(box2, 0, 1, 1, 1, 0, 0);
|
||||
|
||||
grid.setRect(0, 0, 80, 24);
|
||||
grid.draw(screen24x80);
|
||||
|
||||
assertThat(forScreen(screen24x80)).hasBorder(0, 0, 80, 24);
|
||||
assertThat(forScreen(screen24x80)).hasBorder(0, 0, 39, 24);
|
||||
assertThat(forScreen(screen24x80)).hasBorder(39, 0, 41, 24);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasBordersWith2x1() {
|
||||
BoxView box1 = new BoxView();
|
||||
BoxView box2 = new BoxView();
|
||||
|
||||
GridView grid = new GridView();
|
||||
grid.setShowBorders(true);
|
||||
grid.setRowSize(0, 0);
|
||||
grid.setColumnSize(0);
|
||||
grid.setShowBorder(false);
|
||||
grid.addItem(box1, 0, 0, 1, 1, 0, 0);
|
||||
grid.addItem(box2, 1, 0, 1, 1, 0, 0);
|
||||
|
||||
grid.setRect(0, 0, 80, 24);
|
||||
grid.draw(screen24x80);
|
||||
|
||||
assertThat(forScreen(screen24x80)).hasBorder(0, 0, 80, 24);
|
||||
assertThat(forScreen(screen24x80)).hasBorder(0, 0, 80, 12);
|
||||
assertThat(forScreen(screen24x80)).hasBorder(0, 11, 80, 13);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasBordersWith2x2() {
|
||||
BoxView box1 = new BoxView();
|
||||
BoxView box2 = new BoxView();
|
||||
BoxView box3 = new BoxView();
|
||||
BoxView box4 = new BoxView();
|
||||
|
||||
GridView grid = new GridView();
|
||||
grid.setShowBorders(true);
|
||||
grid.setRowSize(0, 0);
|
||||
grid.setColumnSize(0, 0);
|
||||
grid.setShowBorder(false);
|
||||
grid.addItem(box1, 0, 0, 1, 1, 0, 0);
|
||||
grid.addItem(box2, 0, 1, 1, 1, 0, 0);
|
||||
grid.addItem(box3, 1, 0, 1, 1, 0, 0);
|
||||
grid.addItem(box4, 1, 1, 1, 1, 0, 0);
|
||||
|
||||
grid.setRect(0, 0, 80, 24);
|
||||
grid.draw(screen24x80);
|
||||
|
||||
assertThat(forScreen(screen24x80)).hasBorder(0, 0, 80, 24);
|
||||
assertThat(forScreen(screen24x80)).hasBorder(0, 0, 39, 12);
|
||||
assertThat(forScreen(screen24x80)).hasBorder(39, 0, 41, 12);
|
||||
assertThat(forScreen(screen24x80)).hasBorder(0, 11, 80, 13);
|
||||
assertThat(forScreen(screen24x80)).hasBorder(39, 11, 41, 13);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasBordersWithHidden() {
|
||||
screen24x80 = new DefaultScreen(20, 10);
|
||||
|
||||
BoxView menu = new BoxView();
|
||||
BoxView main = new BoxView();
|
||||
BoxView sideBar = new BoxView();
|
||||
BoxView header = new BoxView();
|
||||
BoxView footer = new BoxView();
|
||||
|
||||
GridView grid = new GridView();
|
||||
grid.setRowSize(3, 0, 3);
|
||||
grid.setColumnSize(30, 0, 30);
|
||||
grid.setShowBorders(true);
|
||||
|
||||
grid.addItem(header, 0, 0, 1, 3, 0, 0);
|
||||
grid.addItem(footer, 2, 0, 1, 3, 0, 0);
|
||||
|
||||
grid.addItem(menu, 0, 0, 0, 0, 0, 0);
|
||||
grid.addItem(main, 1, 0, 1, 3, 0, 0);
|
||||
grid.addItem(sideBar, 0, 0, 0, 0, 0, 0);
|
||||
|
||||
grid.addItem(menu, 1, 0, 1, 1, 0, 100);
|
||||
grid.addItem(main, 1, 1, 1, 1, 0, 100);
|
||||
grid.addItem(sideBar, 1, 2, 1, 1, 0, 100);
|
||||
|
||||
grid.setRect(0, 0, 10, 20);
|
||||
grid.draw(screen24x80);
|
||||
|
||||
// overflows, don't have full border anywhere
|
||||
// assertThat(forScreen(screen)).hasBorder(0, 0, 80, 24);
|
||||
}
|
||||
|
||||
@Test
|
||||
void gridBoxHasTitle() {
|
||||
BoxView box1 = new BoxView();
|
||||
|
||||
GridView grid = new GridView();
|
||||
grid.setShowBorder(true);
|
||||
grid.setTitle("title");
|
||||
grid.setShowBorders(true);
|
||||
grid.setRowSize(0);
|
||||
grid.setColumnSize(0);
|
||||
grid.addItem(box1, 0, 0, 1, 1, 0, 0);
|
||||
|
||||
grid.setRect(0, 0, 80, 24);
|
||||
grid.draw(screen24x80);
|
||||
|
||||
assertThat(forScreen(screen24x80)).hasBorder(0, 0, 80, 24);
|
||||
assertThat(forScreen(screen24x80)).hasBorder(1, 1, 78, 22);
|
||||
assertThat(forScreen(screen24x80)).hasHorizontalText("title", 1, 0, 5);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.Test;
|
||||
|
||||
import org.springframework.shell.component.view.event.KeyEvent;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class InputViewTests extends AbstractViewTests {
|
||||
|
||||
@Test
|
||||
void shouldShowInput() {
|
||||
InputView view = new InputView();
|
||||
view.setShowBorder(true);
|
||||
view.setRect(0, 0, 80, 24);
|
||||
|
||||
dispatchEvent(view, KeyEvent.of('1'));
|
||||
view.draw(screen24x80);
|
||||
|
||||
assertThat(forScreen(screen24x80)).hasHorizontalText("1", 1, 1, 1);
|
||||
assertThat(forScreen(screen24x80)).hasCursorInPosition(2, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldShowUnicode() {
|
||||
InputView view = new InputView();
|
||||
view.setShowBorder(true);
|
||||
view.setRect(0, 0, 80, 24);
|
||||
|
||||
dispatchEvent(view, KeyEvent.of('★'));
|
||||
view.draw(screen24x80);
|
||||
|
||||
assertThat(forScreen(screen24x80)).hasHorizontalText("★", 1, 1, 1);
|
||||
assertThat(forScreen(screen24x80)).hasCursorInPosition(2, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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 java.util.Arrays;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.shell.component.view.control.cell.ListCell;
|
||||
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;
|
||||
import org.springframework.shell.component.view.event.KeyHandler.KeyHandlerResult;
|
||||
import org.springframework.shell.component.view.event.MouseEvent;
|
||||
import org.springframework.shell.component.view.event.MouseHandler;
|
||||
import org.springframework.shell.component.view.event.MouseHandler.MouseHandlerResult;
|
||||
import org.springframework.shell.component.view.geom.Rectangle;
|
||||
import org.springframework.shell.component.view.screen.Screen;
|
||||
import org.springframework.shell.component.view.screen.Screen.Writer;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ListViewTests extends AbstractViewTests {
|
||||
|
||||
@Test
|
||||
void hasBorder() {
|
||||
ListView<?> view = new ListView<>();
|
||||
view.setShowBorder(true);
|
||||
view.setRect(0, 0, 80, 24);
|
||||
view.draw(screen24x80);
|
||||
assertThat(forScreen(screen24x80)).hasBorder(0, 0, 80, 24);
|
||||
}
|
||||
|
||||
@Test
|
||||
void arrowKeysMoveSelection() {
|
||||
ListView<String> view = new ListView<>();
|
||||
configure(view);
|
||||
view.setRect(0, 0, 80, 24);
|
||||
view.setItems(Arrays.asList("item1", "item2"));
|
||||
assertThat(ReflectionTestUtils.getField(view, "selected")).isEqualTo(-1);
|
||||
|
||||
KeyEvent eventDown = KeyEvent.of(Key.CursorDown);
|
||||
KeyEvent eventUp = KeyEvent.of(Key.CursorUp);
|
||||
KeyHandlerResult result = view.getKeyHandler().handle(KeyHandler.argsOf(eventDown));
|
||||
assertThat(result).isNotNull().satisfies(r -> {
|
||||
assertThat(r.event()).isEqualTo(eventDown);
|
||||
assertThat(r.consumed()).isTrue();
|
||||
// assertThat(r.focus()).isEqualTo(view);
|
||||
// assertThat(r.capture()).isEqualTo(view);
|
||||
});
|
||||
assertThat(selected(view)).isEqualTo(0);
|
||||
result = view.getKeyHandler().handle(KeyHandler.argsOf(eventDown));
|
||||
assertThat(selected(view)).isEqualTo(1);
|
||||
result = view.getKeyHandler().handle(KeyHandler.argsOf(eventUp));
|
||||
assertThat(selected(view)).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void mouseWheelMoveSelection() {
|
||||
ListView<String> view = new ListView<>();
|
||||
configure(view);
|
||||
view.setRect(0, 0, 80, 24);
|
||||
view.setItems(Arrays.asList("item1", "item2"));
|
||||
assertThat(selected(view)).isEqualTo(-1);
|
||||
|
||||
MouseEvent eventDown = mouseWheelDown(0, 0);
|
||||
MouseEvent eventUp = mouseWheelUp(0, 0);
|
||||
MouseHandlerResult result = view.getMouseHandler().handle(MouseHandler.argsOf(eventDown));
|
||||
assertThat(result).isNotNull().satisfies(r -> {
|
||||
assertThat(r.event()).isEqualTo(eventDown);
|
||||
// assertThat(r.consumed()).isFalse();
|
||||
// assertThat(r.focus()).isNull();
|
||||
assertThat(r.capture()).isEqualTo(view);
|
||||
});
|
||||
assertThat(selected(view)).isEqualTo(0);
|
||||
result = view.getMouseHandler().handle(MouseHandler.argsOf(eventDown));
|
||||
assertThat(selected(view)).isEqualTo(1);
|
||||
result = view.getMouseHandler().handle(MouseHandler.argsOf(eventUp));
|
||||
assertThat(selected(view)).isEqualTo(0);
|
||||
}
|
||||
|
||||
static int selected(ListView<?> view) {
|
||||
return (int) ReflectionTestUtils.getField(view, "selected");
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Visual {
|
||||
|
||||
@Test
|
||||
void customCellFactory() {
|
||||
ListView<String> view = new ListView<>();
|
||||
view.setShowBorder(true);
|
||||
view.setCellFactory(list -> new TestListCell());
|
||||
view.setRect(0, 0, 80, 24);
|
||||
view.setItems(Arrays.asList("item1"));
|
||||
view.draw(screen24x80);
|
||||
assertThat(forScreen(screen24x80)).hasHorizontalText("pre-item1-post", 0, 1, 16);
|
||||
}
|
||||
|
||||
static class TestListCell extends ListCell<String> {
|
||||
|
||||
@Override
|
||||
public void draw(Screen screen) {
|
||||
Rectangle rect = getRect();
|
||||
Writer writer = screen.writerBuilder().build();
|
||||
writer.text(String.format("pre-%s-post", getItem()), rect.x(), rect.y());
|
||||
writer.background(rect, getBackgroundColor());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
* 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.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.shell.component.view.control.MenuBarView.MenuBarItem;
|
||||
import org.springframework.shell.component.view.control.MenuView.MenuItem;
|
||||
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;
|
||||
import org.springframework.shell.component.view.event.MouseHandler.MouseHandlerResult;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class MenuBarViewTests extends AbstractViewTests {
|
||||
|
||||
private static final String SELECTED_FIELD = "activeItemIndex";
|
||||
private static final String MENUVIEW_FIELD = "currentMenuView";
|
||||
|
||||
@Nested
|
||||
class Construction {
|
||||
|
||||
@Test
|
||||
void constructView() {
|
||||
MenuBarView view;
|
||||
|
||||
view = MenuBarView.of(MenuBarItem.of("title"));
|
||||
assertThat(view.getItems()).hasSize(1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Styling {
|
||||
|
||||
@Test
|
||||
void hasBorder() {
|
||||
MenuBarView view = new MenuBarView(new MenuBarView.MenuBarItem[0]);
|
||||
view.setShowBorder(true);
|
||||
view.setRect(0, 0, 80, 24);
|
||||
view.draw(screen24x80);
|
||||
assertThat(forScreen(screen24x80)).hasBorder(0, 0, 80, 24);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// @Nested
|
||||
// class Selection {
|
||||
// MenuBarView view;
|
||||
|
||||
// }
|
||||
|
||||
@Nested
|
||||
class Menus {
|
||||
|
||||
@Test
|
||||
void mouseClicksSameOpensAndClosesMenu() {
|
||||
MenuItem menuItem = new MenuView.MenuItem("sub1");
|
||||
MenuBarItem menuBarItem = new MenuBarView.MenuBarItem("menu1", new MenuView.MenuItem[] { menuItem });
|
||||
MenuBarView view = new MenuBarView(new MenuBarView.MenuBarItem[] { menuBarItem });
|
||||
configure(view);
|
||||
view.setRect(0, 0, 10, 10);
|
||||
|
||||
MouseEvent click1 = mouseClick(0, 0);
|
||||
MouseHandlerResult result1 = view.getMouseHandler().handle(MouseHandler.argsOf(click1));
|
||||
assertThat(result1).isNotNull().satisfies(r -> {
|
||||
assertThat(r.event()).isEqualTo(click1);
|
||||
assertThat(r.consumed()).isTrue();
|
||||
assertThat(r.focus()).isEqualTo(view);
|
||||
assertThat(r.capture()).isEqualTo(view);
|
||||
});
|
||||
|
||||
Integer selected = (Integer) ReflectionTestUtils.getField(view, SELECTED_FIELD);
|
||||
assertThat(selected).isEqualTo(0);
|
||||
|
||||
MenuView menuView1 = (MenuView) ReflectionTestUtils.getField(view, MENUVIEW_FIELD);
|
||||
assertThat(menuView1).isNotNull();
|
||||
|
||||
MouseEvent click2 = mouseClick(0, 0);
|
||||
MouseHandlerResult result2 = view.getMouseHandler().handle(MouseHandler.argsOf(click2));
|
||||
assertThat(result2).isNotNull().satisfies(r -> {
|
||||
assertThat(r.event()).isEqualTo(click2);
|
||||
assertThat(r.consumed()).isTrue();
|
||||
assertThat(r.focus()).isEqualTo(view);
|
||||
assertThat(r.capture()).isEqualTo(view);
|
||||
});
|
||||
|
||||
MenuView menuView2 = (MenuView) ReflectionTestUtils.getField(view, MENUVIEW_FIELD);
|
||||
assertThat(menuView2).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void mouseClicksOpensDifferentMenus() {
|
||||
MenuItem menuItem1 = new MenuView.MenuItem("sub1");
|
||||
MenuItem menuItem2 = new MenuView.MenuItem("sub2");
|
||||
MenuBarItem menuBarItem1 = new MenuBarView.MenuBarItem("menu1", new MenuView.MenuItem[] { menuItem1 });
|
||||
MenuBarItem menuBarItem2 = new MenuBarView.MenuBarItem("menu2", new MenuView.MenuItem[] { menuItem2 });
|
||||
MenuBarView view = new MenuBarView(new MenuBarView.MenuBarItem[] { menuBarItem1, menuBarItem2 });
|
||||
configure(view);
|
||||
view.setRect(0, 0, 10, 10);
|
||||
|
||||
MouseEvent click1 = mouseClick(0, 0);
|
||||
MouseHandlerResult result1 = view.getMouseHandler().handle(MouseHandler.argsOf(click1));
|
||||
assertThat(result1).isNotNull().satisfies(r -> {
|
||||
assertThat(r.event()).isEqualTo(click1);
|
||||
assertThat(r.consumed()).isTrue();
|
||||
assertThat(r.focus()).isEqualTo(view);
|
||||
assertThat(r.capture()).isEqualTo(view);
|
||||
});
|
||||
|
||||
Integer selected = (Integer) ReflectionTestUtils.getField(view, SELECTED_FIELD);
|
||||
assertThat(selected).isEqualTo(0);
|
||||
|
||||
MenuView menuView1 = (MenuView) ReflectionTestUtils.getField(view, MENUVIEW_FIELD);
|
||||
assertThat(menuView1).isNotNull();
|
||||
assertThat(menuView1.getItems().get(0).getTitle()).isEqualTo("sub1");
|
||||
|
||||
MouseEvent click2 = mouseClick(7, 0);
|
||||
MouseHandlerResult result2 = view.getMouseHandler().handle(MouseHandler.argsOf(click2));
|
||||
assertThat(result2).isNotNull().satisfies(r -> {
|
||||
assertThat(r.event()).isEqualTo(click2);
|
||||
assertThat(r.consumed()).isTrue();
|
||||
assertThat(r.focus()).isEqualTo(view);
|
||||
assertThat(r.capture()).isEqualTo(view);
|
||||
});
|
||||
|
||||
MenuView menuView2 = (MenuView) ReflectionTestUtils.getField(view, MENUVIEW_FIELD);
|
||||
assertThat(menuView2).isNotNull();
|
||||
assertThat(menuView2.getItems().get(0).getTitle()).isEqualTo("sub2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void arrowKeysMoveBetweenDifferentMenus() {
|
||||
MenuItem menuItem1 = new MenuView.MenuItem("sub1");
|
||||
MenuItem menuItem2 = new MenuView.MenuItem("sub2");
|
||||
MenuBarItem menuBarItem1 = new MenuBarView.MenuBarItem("menu1", new MenuView.MenuItem[] { menuItem1 });
|
||||
MenuBarItem menuBarItem2 = new MenuBarView.MenuBarItem("menu2", new MenuView.MenuItem[] { menuItem2 });
|
||||
MenuBarView view = new MenuBarView(new MenuBarView.MenuBarItem[] { menuBarItem1, menuBarItem2 });
|
||||
configure(view);
|
||||
view.setRect(0, 0, 10, 10);
|
||||
|
||||
// Can't yet active menu with a key, so start with a click
|
||||
MouseEvent click1 = mouseClick(0, 0);
|
||||
MouseHandlerResult result1 = view.getMouseHandler().handle(MouseHandler.argsOf(click1));
|
||||
assertThat(result1).isNotNull().satisfies(r -> {
|
||||
assertThat(r.event()).isEqualTo(click1);
|
||||
assertThat(r.consumed()).isTrue();
|
||||
assertThat(r.focus()).isEqualTo(view);
|
||||
assertThat(r.capture()).isEqualTo(view);
|
||||
});
|
||||
|
||||
Integer selected = (Integer) ReflectionTestUtils.getField(view, SELECTED_FIELD);
|
||||
assertThat(selected).isEqualTo(0);
|
||||
|
||||
MenuView menuView1 = (MenuView) ReflectionTestUtils.getField(view, MENUVIEW_FIELD);
|
||||
assertThat(menuView1).isNotNull();
|
||||
assertThat(menuView1.getItems().get(0).getTitle()).isEqualTo("sub1");
|
||||
|
||||
handleKey(view, Key.CursorRight);
|
||||
|
||||
MenuView menuView2 = (MenuView) ReflectionTestUtils.getField(view, MENUVIEW_FIELD);
|
||||
assertThat(menuView2).isNotNull();
|
||||
assertThat(menuView2.getItems().get(0).getTitle()).isEqualTo("sub2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void menuHasPositionRelativeToHeader() {
|
||||
MenuBarView view = new MenuBarView(new MenuBarItem[] {
|
||||
new MenuBarItem("menu1", new MenuItem[] {
|
||||
new MenuItem("sub11")
|
||||
}),
|
||||
new MenuBarItem("menu2", new MenuItem[] {
|
||||
new MenuItem("sub21")
|
||||
})
|
||||
});
|
||||
configure(view);
|
||||
view.setRect(0, 0, 20, 1);
|
||||
|
||||
MouseEvent click = mouseClick(7, 0);
|
||||
MouseHandlerResult result = view.getMouseHandler().handle(MouseHandler.argsOf(click));
|
||||
assertThat(result).isNotNull().satisfies(r -> {
|
||||
assertThat(r.event()).isEqualTo(click);
|
||||
assertThat(r.consumed()).isTrue();
|
||||
assertThat(r.focus()).isEqualTo(view);
|
||||
assertThat(r.capture()).isEqualTo(view);
|
||||
});
|
||||
|
||||
MenuView menuView = (MenuView) ReflectionTestUtils.getField(view, MENUVIEW_FIELD);
|
||||
assertThat(menuView).isNotNull().satisfies(m -> {
|
||||
assertThat(m.getRect()).satisfies(r -> {
|
||||
assertThat(r.x()).isEqualTo(7);
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
/*
|
||||
* 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 java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
|
||||
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.control.MenuView.MenuItem;
|
||||
import org.springframework.shell.component.view.control.MenuView.MenuItemCheckStyle;
|
||||
import org.springframework.shell.component.view.control.MenuView.MenuViewOpenSelectedItemEvent;
|
||||
import org.springframework.shell.component.view.control.MenuView.MenuViewSelectedItemChangedEvent;
|
||||
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;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class MenuViewTests extends AbstractViewTests {
|
||||
|
||||
private static final String SELECTED_FIELD = "activeItemIndex";
|
||||
|
||||
@Nested
|
||||
class Construction {
|
||||
|
||||
@Test
|
||||
void constructView() {
|
||||
MenuView view;
|
||||
|
||||
view = new MenuView(new MenuItem[] {
|
||||
MenuItem.of("sub1"),
|
||||
MenuItem.of("sub2")
|
||||
});
|
||||
assertThat(view.getItems()).hasSize(2);
|
||||
|
||||
view = new MenuView(new MenuItem[] {
|
||||
new MenuItem("sub1"),
|
||||
new MenuItem("sub2")
|
||||
});
|
||||
assertThat(view.getItems()).hasSize(2);
|
||||
|
||||
view = new MenuView(new MenuItem[] {
|
||||
MenuItem.of("sub1", MenuItemCheckStyle.RADIO),
|
||||
MenuItem.of("sub2", MenuItemCheckStyle.RADIO)
|
||||
});
|
||||
assertThat(view.getItems()).hasSize(2);
|
||||
|
||||
view = new MenuView(new MenuItem[] {
|
||||
new MenuItem("sub1", MenuItemCheckStyle.RADIO),
|
||||
new MenuItem("sub2", MenuItemCheckStyle.RADIO)
|
||||
});
|
||||
assertThat(view.getItems()).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructUsingRunnable() {
|
||||
MenuView view;
|
||||
|
||||
view = new MenuView(new MenuItem[] {
|
||||
MenuItem.of("sub1", MenuItemCheckStyle.RADIO, () -> {}),
|
||||
MenuItem.of("sub2", MenuItemCheckStyle.RADIO, () -> {})
|
||||
});
|
||||
assertThat(view.getItems()).hasSize(2);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Styling {
|
||||
|
||||
@Test
|
||||
void hasBorder() {
|
||||
MenuItem menuItem = new MenuView.MenuItem("sub1");
|
||||
MenuView view = new MenuView(Arrays.asList(menuItem));
|
||||
view.setShowBorder(true);
|
||||
view.setRect(0, 0, 80, 24);
|
||||
view.draw(screen24x80);
|
||||
assertThat(forScreen(screen24x80)).hasBorder(0, 0, 80, 24);
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultItemCheckStyleIsNoCheck() {
|
||||
MenuItem menuItem = new MenuView.MenuItem("sub1");
|
||||
MenuView view = new MenuView(Arrays.asList(menuItem));
|
||||
assertThat(view.getItems()).allSatisfy(item -> {
|
||||
assertThat(item.getCheckStyle()).isEqualTo(MenuItemCheckStyle.NOCHECK);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Selection {
|
||||
|
||||
MenuView view;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
view = new MenuView(new MenuItem[] {
|
||||
MenuItem.of("sub1"),
|
||||
MenuItem.of("sub2")
|
||||
});
|
||||
configure(view);
|
||||
view.setRect(0, 0, 10, 10);
|
||||
}
|
||||
|
||||
@Test
|
||||
void firstItemShouldAlwaysBeSelected() {
|
||||
MenuItem menuItem = new MenuView.MenuItem("sub1");
|
||||
MenuView view = new MenuView(Arrays.asList(menuItem));
|
||||
Integer selected = (Integer) ReflectionTestUtils.getField(view, SELECTED_FIELD);
|
||||
assertThat(selected).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void clickInItemSelects() {
|
||||
handleMouseClick(view, 0, 2);
|
||||
Integer selected = (Integer) ReflectionTestUtils.getField(view, SELECTED_FIELD);
|
||||
assertThat(selected).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void downArrowMovesSelection() {
|
||||
Integer selected;
|
||||
|
||||
handleKey(view, Key.CursorDown);
|
||||
selected = (Integer) ReflectionTestUtils.getField(view, SELECTED_FIELD);
|
||||
assertThat(selected).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void upArrowMovesSelection() {
|
||||
Integer selected;
|
||||
|
||||
handleKey(view, Key.CursorUp);
|
||||
selected = (Integer) ReflectionTestUtils.getField(view, SELECTED_FIELD);
|
||||
assertThat(selected).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void wheelMovesSelection() {
|
||||
Integer selected;
|
||||
|
||||
handleMouseWheelDown(view, 0, 1);
|
||||
selected = (Integer) ReflectionTestUtils.getField(view, SELECTED_FIELD);
|
||||
assertThat(selected).isEqualTo(1);
|
||||
|
||||
handleMouseWheelUp(view, 0, 1);
|
||||
selected = (Integer) ReflectionTestUtils.getField(view, SELECTED_FIELD);
|
||||
assertThat(selected).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectionShouldNotMoveOutOfBounds() {
|
||||
Integer selected;
|
||||
|
||||
handleKey(view, Key.CursorDown);
|
||||
selected = (Integer) ReflectionTestUtils.getField(view, SELECTED_FIELD);
|
||||
assertThat(selected).isEqualTo(1);
|
||||
|
||||
handleKey(view, Key.CursorDown);
|
||||
selected = (Integer) ReflectionTestUtils.getField(view, SELECTED_FIELD);
|
||||
assertThat(selected).isEqualTo(0);
|
||||
}
|
||||
|
||||
void canSelectManually() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Checked {
|
||||
|
||||
MenuItem sub1;
|
||||
MenuItem sub2;
|
||||
MenuItem sub3;
|
||||
MenuItem sub4;
|
||||
MenuItem sub5;
|
||||
MenuItem sub6;
|
||||
MenuView view;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
sub1 = MenuItem.of("sub1", MenuItemCheckStyle.NOCHECK);
|
||||
sub2 = MenuItem.of("sub2", MenuItemCheckStyle.NOCHECK);
|
||||
sub3 = MenuItem.of("sub3", MenuItemCheckStyle.CHECKED);
|
||||
sub4 = MenuItem.of("sub4", MenuItemCheckStyle.CHECKED);
|
||||
sub5 = MenuItem.of("sub5", MenuItemCheckStyle.RADIO);
|
||||
sub6 = MenuItem.of("sub6", MenuItemCheckStyle.RADIO);
|
||||
}
|
||||
|
||||
@Test
|
||||
void onlyNocheckDontAddPrefix() {
|
||||
view = new MenuView(new MenuItem[] { sub1, sub2 });
|
||||
view.setShowBorder(true);
|
||||
configure(view);
|
||||
view.setRect(0, 0, 10, 10);
|
||||
view.draw(screen10x10);
|
||||
assertThat(forScreen(screen10x10)).hasHorizontalText("sub1", 0, 1, 5);
|
||||
assertThat(forScreen(screen10x10)).hasHorizontalText("sub2", 0, 2, 5);
|
||||
}
|
||||
|
||||
@Test
|
||||
void showsCheckedInRadio() {
|
||||
sub5.setChecked(true);
|
||||
view = new MenuView(new MenuItem[] { sub5, sub6 });
|
||||
view.setShowBorder(true);
|
||||
configure(view);
|
||||
view.setRect(0, 0, 10, 10);
|
||||
view.draw(screen10x10);
|
||||
assertThat(forScreen(screen10x10)).hasHorizontalText("[x] sub5", 0, 1, 9);
|
||||
assertThat(forScreen(screen10x10)).hasHorizontalText("[ ] sub6", 0, 2, 9);
|
||||
}
|
||||
|
||||
@Test
|
||||
void showsCheckedInNonRadio() {
|
||||
sub4.setChecked(true);
|
||||
view = new MenuView(new MenuItem[] { sub3, sub4 });
|
||||
view.setShowBorder(true);
|
||||
configure(view);
|
||||
view.setRect(0, 0, 10, 10);
|
||||
view.draw(screen10x10);
|
||||
assertThat(forScreen(screen10x10)).hasHorizontalText("[ ] sub3", 0, 1, 9);
|
||||
assertThat(forScreen(screen10x10)).hasHorizontalText("[x] sub4", 0, 2, 9);
|
||||
}
|
||||
|
||||
@Test
|
||||
void mixedAddPrefixforNocheck() {
|
||||
view = new MenuView(new MenuItem[] { sub1, sub3, sub4 });
|
||||
view.setShowBorder(true);
|
||||
configure(view);
|
||||
view.setRect(0, 0, 10, 10);
|
||||
view.draw(screen10x10);
|
||||
assertThat(forScreen(screen10x10)).hasHorizontalText(" sub1", 0, 1, 9);
|
||||
assertThat(forScreen(screen10x10)).hasHorizontalText("[ ] sub3", 0, 2, 9);
|
||||
assertThat(forScreen(screen10x10)).hasHorizontalText("[ ] sub4", 0, 3, 9);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Events {
|
||||
|
||||
MenuView view;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
view = new MenuView(new MenuItem[] {
|
||||
MenuItem.of("sub1"),
|
||||
MenuItem.of("sub2")
|
||||
});
|
||||
configure(view);
|
||||
view.setRect(0, 0, 10, 10);
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlesMouseClickInItem() {
|
||||
MouseEvent click = mouseClick(0, 2);
|
||||
|
||||
Flux<MenuViewSelectedItemChangedEvent> actions = eventLoop
|
||||
.viewEvents(MenuViewSelectedItemChangedEvent.class);
|
||||
StepVerifier verifier = StepVerifier.create(actions)
|
||||
.expectNextCount(1)
|
||||
.thenCancel()
|
||||
.verifyLater();
|
||||
|
||||
MouseHandlerResult result = handleMouseClick(view, click);
|
||||
|
||||
assertThat(result).isNotNull().satisfies(r -> {
|
||||
assertThat(r.event()).isEqualTo(click);
|
||||
assertThat(r.consumed()).isTrue();
|
||||
assertThat(r.focus()).isEqualTo(view);
|
||||
assertThat(r.capture()).isEqualTo(view);
|
||||
});
|
||||
verifier.verify(Duration.ofSeconds(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void keySelectSendsEvent() {
|
||||
Flux<MenuViewOpenSelectedItemEvent> actions = eventLoop
|
||||
.viewEvents(MenuViewOpenSelectedItemEvent.class);
|
||||
StepVerifier verifier = StepVerifier.create(actions)
|
||||
.expectNextCount(1)
|
||||
.thenCancel()
|
||||
.verifyLater();
|
||||
|
||||
handleKey(view, Key.Enter);
|
||||
verifier.verify(Duration.ofSeconds(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectionChangedSendsEvent() {
|
||||
Flux<MenuViewSelectedItemChangedEvent> actions = eventLoop
|
||||
.viewEvents(MenuViewSelectedItemChangedEvent.class);
|
||||
StepVerifier verifier = StepVerifier.create(actions)
|
||||
.expectNextCount(1)
|
||||
.thenCancel()
|
||||
.verifyLater();
|
||||
|
||||
handleKey(view, Key.CursorDown);
|
||||
verifier.verify(Duration.ofSeconds(1));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Visual {
|
||||
MenuView view;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
view = new MenuView(new MenuItem[] {
|
||||
MenuItem.of("sub1"),
|
||||
MenuItem.of("sub2")
|
||||
});
|
||||
configure(view);
|
||||
view.setRect(0, 0, 10, 10);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasDefaultItemSelected() {
|
||||
MenuItem menuItem = new MenuView.MenuItem("sub1");
|
||||
MenuView view = new MenuView(Arrays.asList(menuItem));
|
||||
view.setShowBorder(true);
|
||||
view.setRect(0, 0, 80, 24);
|
||||
view.draw(screen24x80);
|
||||
assertThat(forScreen(screen24x80)).hasHorizontalText("sub1", 0, 1, 5);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* 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 java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
|
||||
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.control.StatusBarView.StatusBarViewOpenSelectedItemEvent;
|
||||
import org.springframework.shell.component.view.control.StatusBarView.StatusItem;
|
||||
import org.springframework.shell.component.view.event.MouseEvent;
|
||||
import org.springframework.shell.component.view.event.MouseHandler.MouseHandlerResult;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class StatusBarViewTests extends AbstractViewTests {
|
||||
|
||||
@Nested
|
||||
class Construction {
|
||||
|
||||
@Test
|
||||
void constructView() {
|
||||
StatusBarView view;
|
||||
|
||||
view = new StatusBarView();
|
||||
assertThat(view.getItems()).hasSize(0);
|
||||
|
||||
view = new StatusBarView(new StatusItem[] {
|
||||
new StatusItem("item1")
|
||||
});
|
||||
assertThat(view.getItems()).hasSize(1);
|
||||
|
||||
view = new StatusBarView(Arrays.asList(new StatusItem("item1")));
|
||||
assertThat(view.getItems()).hasSize(1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Internal {
|
||||
|
||||
StatusBarView view;
|
||||
StatusItem item;
|
||||
|
||||
@Test
|
||||
void itemPosition() {
|
||||
view = new StatusBarView(new StatusItem[] {
|
||||
new StatusItem("item1"),
|
||||
new StatusItem("item2")
|
||||
});
|
||||
view.setRect(0, 0, 10, 1);
|
||||
|
||||
item = (StatusItem) ReflectionTestUtils.invokeMethod(view, "itemAt", 0, 0);
|
||||
assertThat(item).isNotNull();
|
||||
assertThat(item.getTitle()).isEqualTo("item1");
|
||||
|
||||
item = (StatusItem) ReflectionTestUtils.invokeMethod(view, "itemAt", 7, 0);
|
||||
assertThat(item).isNotNull();
|
||||
assertThat(item.getTitle()).isEqualTo("item2");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Styling {
|
||||
|
||||
@Test
|
||||
void hasBorder() {
|
||||
StatusBarView view = new StatusBarView();
|
||||
view.setShowBorder(true);
|
||||
view.setRect(0, 0, 80, 24);
|
||||
view.draw(screen24x80);
|
||||
assertThat(forScreen(screen24x80)).hasBorder(0, 0, 80, 24);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Events {
|
||||
|
||||
StatusBarView view;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
view = new StatusBarView(new StatusItem[] {
|
||||
new StatusItem("item1")
|
||||
});
|
||||
configure(view);
|
||||
view.setRect(0, 0, 20, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlesMouseClickInItem() {
|
||||
MouseEvent click = mouseClick(1, 0);
|
||||
|
||||
Flux<StatusBarViewOpenSelectedItemEvent> actions = eventLoop
|
||||
.viewEvents(StatusBarViewOpenSelectedItemEvent.class);
|
||||
StepVerifier verifier = StepVerifier.create(actions)
|
||||
.expectNextCount(1)
|
||||
.thenCancel()
|
||||
.verifyLater();
|
||||
|
||||
MouseHandlerResult result = handleMouseClick(view, click);
|
||||
|
||||
assertThat(result).isNotNull().satisfies(r -> {
|
||||
assertThat(r.event()).isEqualTo(click);
|
||||
assertThat(r.consumed()).isTrue();
|
||||
assertThat(r.focus()).isNull();
|
||||
assertThat(r.capture()).isNull();
|
||||
});
|
||||
verifier.verify(Duration.ofSeconds(1));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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.cell;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.shell.component.view.control.AbstractViewTests;
|
||||
import org.springframework.shell.component.view.screen.Color;
|
||||
import org.springframework.shell.component.view.screen.ScreenItem;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ListCellTests extends AbstractViewTests {
|
||||
|
||||
@Test
|
||||
void simpleTextWrites() {
|
||||
ListCell<String> cell = new ListCell<>();
|
||||
cell.setRect(0, 0, 10, 1);
|
||||
cell.updateItem("item");
|
||||
cell.draw(screen10x10);
|
||||
assertThat(forScreen(screen10x10)).hasHorizontalText("item", 0, 0, 4);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasBackgroundColor() {
|
||||
ListCell<String> cell = new ListCell<>();
|
||||
cell.setBackgroundColor(Color.BLUE);
|
||||
cell.setRect(0, 0, 10, 1);
|
||||
cell.updateItem("item");
|
||||
cell.draw(screen10x10);
|
||||
assertThat(forScreen(screen10x10)).hasHorizontalText("item", 0, 0, 4).hasBackgroundColor(0, 0, Color.BLUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasForegroundColor() {
|
||||
ListCell<String> cell = new ListCell<>();
|
||||
cell.setForegroundColor(Color.BLUE);
|
||||
cell.setRect(0, 0, 10, 1);
|
||||
cell.updateItem("item");
|
||||
cell.draw(screen10x10);
|
||||
assertThat(forScreen(screen10x10)).hasHorizontalText("item", 0, 0, 4).hasForegroundColor(0, 0, Color.BLUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasStyle() {
|
||||
ListCell<String> cell = new ListCell<>();
|
||||
cell.setStyle(ScreenItem.STYLE_BOLD);
|
||||
cell.setRect(0, 0, 10, 1);
|
||||
cell.updateItem("item");
|
||||
cell.draw(screen10x10);
|
||||
assertThat(forScreen(screen10x10)).hasHorizontalText("item", 0, 0, 4).hasStyle(0, 0, ScreenItem.STYLE_BOLD);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* 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.event;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.shell.component.view.event.EventLoop.EventLoopProcessor;
|
||||
import org.springframework.shell.component.view.message.ShellMessageBuilder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class DefaultEventLoopTests {
|
||||
|
||||
private DefaultEventLoop loop;
|
||||
|
||||
@AfterEach
|
||||
void clean() {
|
||||
if (loop != null) {
|
||||
loop.destroy();
|
||||
}
|
||||
loop = null;
|
||||
}
|
||||
|
||||
private void initDefault() {
|
||||
loop = new DefaultEventLoop();
|
||||
}
|
||||
|
||||
@Test
|
||||
void eventsGetIntoSingleSubscriber() {
|
||||
initDefault();
|
||||
Message<String> message = MessageBuilder.withPayload("TEST").build();
|
||||
|
||||
StepVerifier verifier1 = StepVerifier.create(loop.events())
|
||||
.expectNextCount(1)
|
||||
.thenCancel()
|
||||
.verifyLater();
|
||||
|
||||
loop.dispatch(message);
|
||||
verifier1.verify(Duration.ofSeconds(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void eventsGetIntoMultipleSubscriber() {
|
||||
initDefault();
|
||||
Message<String> message = MessageBuilder.withPayload("TEST").build();
|
||||
|
||||
StepVerifier verifier1 = StepVerifier.create(loop.events())
|
||||
.expectNextCount(1)
|
||||
.thenCancel()
|
||||
.verifyLater();
|
||||
|
||||
StepVerifier verifier2 = StepVerifier.create(loop.events())
|
||||
.expectNextCount(1)
|
||||
.thenCancel()
|
||||
.verifyLater();
|
||||
|
||||
loop.dispatch(message);
|
||||
verifier1.verify(Duration.ofSeconds(1));
|
||||
verifier2.verify(Duration.ofSeconds(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void canDispatchFlux() {
|
||||
initDefault();
|
||||
Message<String> message = MessageBuilder.withPayload("TEST").build();
|
||||
Flux<Message<String>> flux = Flux.just(message);
|
||||
|
||||
StepVerifier verifier1 = StepVerifier.create(loop.events())
|
||||
.expectNextCount(1)
|
||||
.thenCancel()
|
||||
.verifyLater();
|
||||
|
||||
loop.dispatch(flux);
|
||||
verifier1.verify(Duration.ofSeconds(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void canDispatchMono() {
|
||||
initDefault();
|
||||
Message<String> message = MessageBuilder.withPayload("TEST").build();
|
||||
Mono<Message<String>> mono = Mono.just(message);
|
||||
|
||||
StepVerifier verifier1 = StepVerifier.create(loop.events())
|
||||
.expectNextCount(1)
|
||||
.thenCancel()
|
||||
.verifyLater();
|
||||
|
||||
loop.dispatch(mono);
|
||||
verifier1.verify(Duration.ofSeconds(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void subsribtionCompletesWhenLoopDestroyed() {
|
||||
initDefault();
|
||||
StepVerifier verifier1 = StepVerifier.create(loop.events())
|
||||
.expectComplete()
|
||||
.verifyLater();
|
||||
|
||||
loop.destroy();
|
||||
verifier1.verify(Duration.ofSeconds(1));
|
||||
}
|
||||
|
||||
static class TestEventLoopProcessor implements EventLoopProcessor {
|
||||
|
||||
int count;
|
||||
|
||||
@Override
|
||||
public boolean canProcess(Message<?> message) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<? extends Message<?>> process(Message<?> message) {
|
||||
Message<?> m = MessageBuilder.fromMessage(message)
|
||||
.setHeader("count", count++)
|
||||
.build();
|
||||
return Flux.just(m);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void processorCreatesSameMessagesForAll() {
|
||||
TestEventLoopProcessor processor = new TestEventLoopProcessor();
|
||||
loop = new DefaultEventLoop(Arrays.asList(processor));
|
||||
|
||||
StepVerifier verifier1 = StepVerifier.create(loop.events())
|
||||
.assertNext(m -> {
|
||||
Integer count = m.getHeaders().get("count", Integer.class);
|
||||
assertThat(count).isEqualTo(0);
|
||||
})
|
||||
.thenCancel()
|
||||
.verifyLater();
|
||||
|
||||
StepVerifier verifier2 = StepVerifier.create(loop.events())
|
||||
.assertNext(m -> {
|
||||
Integer count = m.getHeaders().get("count", Integer.class);
|
||||
assertThat(count).isEqualTo(0);
|
||||
})
|
||||
.thenCancel()
|
||||
.verifyLater();
|
||||
|
||||
Message<String> message = MessageBuilder.withPayload("TEST").build();
|
||||
loop.dispatch(message);
|
||||
verifier1.verify(Duration.ofSeconds(1));
|
||||
verifier2.verify(Duration.ofSeconds(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void taskRunnableShouldExecute() {
|
||||
initDefault();
|
||||
TestRunnable task = new TestRunnable();
|
||||
Message<TestRunnable> message = ShellMessageBuilder.withPayload(task).setEventType(EventLoop.Type.TASK).build();
|
||||
StepVerifier verifier1 = StepVerifier.create(loop.events())
|
||||
.expectNextCount(1)
|
||||
.thenCancel()
|
||||
.verifyLater();
|
||||
loop.dispatch(message);
|
||||
verifier1.verify(Duration.ofSeconds(1));
|
||||
assertThat(task.count).isEqualTo(1);
|
||||
}
|
||||
|
||||
static class TestRunnable implements Runnable {
|
||||
int count = 0;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.event;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.shell.component.view.event.KeyEvent.Key;
|
||||
import org.springframework.shell.component.view.event.KeyEvent.KeyMask;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class KeyEventTests {
|
||||
|
||||
@Test
|
||||
void hasCtrl() {
|
||||
assertThat(KeyEvent.of(Key.A).hasCtrl()).isFalse();
|
||||
assertThat(KeyEvent.of(Key.A | KeyMask.CtrlMask).hasCtrl()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void plainKey() {
|
||||
assertThat(KeyEvent.of(Key.A).getPlainKey()).isEqualTo(Key.A);
|
||||
assertThat(KeyEvent.of(Key.A | KeyMask.CtrlMask).getPlainKey()).isEqualTo(Key.A);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isKey() {
|
||||
assertThat(KeyEvent.of(Key.A).isKey(Key.A)).isTrue();
|
||||
assertThat(KeyEvent.of(Key.A | KeyMask.CtrlMask).isKey(Key.A)).isTrue();
|
||||
assertThat(KeyEvent.of(Key.CursorDown).isKey(Key.CursorDown)).isTrue();
|
||||
assertThat(KeyEvent.of(Key.CursorLeft).isKey(Key.CursorRight)).isFalse();
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void isKey2() {
|
||||
assertThat(KeyEvent.of(Key.A).isKey()).isTrue();
|
||||
assertThat(KeyEvent.of(Key.Backspace).isKey()).isFalse();
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.view.event;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.shell.component.view.event.KeyEvent.Key;
|
||||
import org.springframework.shell.component.view.event.KeyHandler.KeyHandlerArgs;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class KeyHandlerTests {
|
||||
|
||||
private static final KeyEvent EVENT = KeyEvent.of(Key.x);
|
||||
private static final KeyHandlerArgs ARGS = KeyHandler.argsOf(EVENT);
|
||||
|
||||
@Test
|
||||
void handlesOtherIfThisConsumes() {
|
||||
TestKeyHandler h1 = new TestKeyHandler(true);
|
||||
TestKeyHandler h2 = new TestKeyHandler(false);
|
||||
KeyHandler composed = h1.thenIfConsumed(h2);
|
||||
composed.handle(ARGS);
|
||||
assertThat(h1.calls).isEqualTo(1);
|
||||
assertThat(h2.calls).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotHandlesOtherIfThisDoesNotConsume() {
|
||||
TestKeyHandler h1 = new TestKeyHandler(true);
|
||||
TestKeyHandler h2 = new TestKeyHandler(false);
|
||||
KeyHandler composed = h2.thenIfConsumed(h1);
|
||||
composed.handle(ARGS);
|
||||
assertThat(h1.calls).isEqualTo(0);
|
||||
assertThat(h2.calls).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlesOtherIfThisDoesNotConsume() {
|
||||
TestKeyHandler h1 = new TestKeyHandler(true);
|
||||
TestKeyHandler h2 = new TestKeyHandler(false);
|
||||
KeyHandler composed = h2.thenIfNotConsumed(h1);
|
||||
composed.handle(ARGS);
|
||||
assertThat(h1.calls).isEqualTo(1);
|
||||
assertThat(h2.calls).isEqualTo(1);
|
||||
}
|
||||
|
||||
private static class TestKeyHandler implements KeyHandler {
|
||||
|
||||
boolean willConsume;
|
||||
int calls;
|
||||
|
||||
TestKeyHandler(boolean willConsume) {
|
||||
this.willConsume = willConsume;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyHandlerResult handle(KeyHandlerArgs args) {
|
||||
calls++;
|
||||
return KeyHandler.resultOf(args.event(), willConsume, null);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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.event;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.shell.component.view.event.MouseEvent.Button;
|
||||
import org.springframework.shell.component.view.event.MouseEvent.Modifier;
|
||||
import org.springframework.shell.component.view.event.MouseEvent.Type;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class MouseEventTests {
|
||||
|
||||
@Test
|
||||
void hasType() {
|
||||
assertThat(MouseEvent.of(0, 0, 0).hasType()).isFalse();
|
||||
|
||||
assertThat(MouseEvent.of(0, 0, Modifier.Shift).hasType()).isFalse();
|
||||
assertThat(MouseEvent.of(0, 0, Modifier.Alt).hasType()).isFalse();
|
||||
assertThat(MouseEvent.of(0, 0, Modifier.Control).hasType()).isFalse();
|
||||
|
||||
assertThat(MouseEvent.of(0, 0, Type.Released).hasType()).isTrue();
|
||||
assertThat(MouseEvent.of(0, 0, Type.Pressed).hasType()).isTrue();
|
||||
assertThat(MouseEvent.of(0, 0, Type.Wheel).hasType()).isTrue();
|
||||
assertThat(MouseEvent.of(0, 0, Type.Moved).hasType()).isTrue();
|
||||
assertThat(MouseEvent.of(0, 0, Type.Dragged).hasType()).isTrue();
|
||||
|
||||
assertThat(MouseEvent.of(0, 0, Button.Button1).hasType()).isFalse();
|
||||
assertThat(MouseEvent.of(0, 0, Button.Button2).hasType()).isFalse();
|
||||
assertThat(MouseEvent.of(0, 0, Button.Button3).hasType()).isFalse();
|
||||
|
||||
assertThat(MouseEvent.of(0, 0, Button.Button3 | Modifier.Control).hasType()).isFalse();
|
||||
assertThat(MouseEvent.of(0, 0, Modifier.Shift | Type.Dragged).hasType()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasButton() {
|
||||
assertThat(MouseEvent.of(0, 0, 0).hasButton()).isFalse();
|
||||
|
||||
assertThat(MouseEvent.of(0, 0, Modifier.Shift).hasButton()).isFalse();
|
||||
assertThat(MouseEvent.of(0, 0, Modifier.Alt).hasButton()).isFalse();
|
||||
assertThat(MouseEvent.of(0, 0, Modifier.Control).hasButton()).isFalse();
|
||||
|
||||
assertThat(MouseEvent.of(0, 0, Type.Released).hasButton()).isFalse();
|
||||
assertThat(MouseEvent.of(0, 0, Type.Pressed).hasButton()).isFalse();
|
||||
assertThat(MouseEvent.of(0, 0, Type.Wheel).hasButton()).isFalse();
|
||||
assertThat(MouseEvent.of(0, 0, Type.Moved).hasButton()).isFalse();
|
||||
assertThat(MouseEvent.of(0, 0, Type.Dragged).hasButton()).isFalse();
|
||||
|
||||
assertThat(MouseEvent.of(0, 0, Button.Button1).hasButton()).isTrue();
|
||||
assertThat(MouseEvent.of(0, 0, Button.Button2).hasButton()).isTrue();
|
||||
assertThat(MouseEvent.of(0, 0, Button.Button3).hasButton()).isTrue();
|
||||
|
||||
assertThat(MouseEvent.of(0, 0, Button.Button3 | Modifier.Control).hasButton()).isTrue();
|
||||
assertThat(MouseEvent.of(0, 0, Modifier.Shift | Type.Dragged).hasButton()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasModifier() {
|
||||
assertThat(MouseEvent.of(0, 0, 0).hasModifier()).isFalse();
|
||||
|
||||
assertThat(MouseEvent.of(0, 0, Modifier.Shift).hasModifier()).isTrue();
|
||||
assertThat(MouseEvent.of(0, 0, Modifier.Alt).hasModifier()).isTrue();
|
||||
assertThat(MouseEvent.of(0, 0, Modifier.Control).hasModifier()).isTrue();
|
||||
|
||||
assertThat(MouseEvent.of(0, 0, Type.Released).hasModifier()).isFalse();
|
||||
assertThat(MouseEvent.of(0, 0, Type.Pressed).hasModifier()).isFalse();
|
||||
assertThat(MouseEvent.of(0, 0, Type.Wheel).hasModifier()).isFalse();
|
||||
assertThat(MouseEvent.of(0, 0, Type.Moved).hasModifier()).isFalse();
|
||||
assertThat(MouseEvent.of(0, 0, Type.Dragged).hasModifier()).isFalse();
|
||||
|
||||
assertThat(MouseEvent.of(0, 0, Button.Button1).hasModifier()).isFalse();
|
||||
assertThat(MouseEvent.of(0, 0, Button.Button2).hasModifier()).isFalse();
|
||||
assertThat(MouseEvent.of(0, 0, Button.Button3).hasModifier()).isFalse();
|
||||
|
||||
assertThat(MouseEvent.of(0, 0, Button.Button3 | Type.Dragged).hasModifier()).isFalse();
|
||||
assertThat(MouseEvent.of(0, 0, Modifier.Shift | Type.Dragged).hasModifier()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBaseIdeas() {
|
||||
assertThat(MouseEvent.of(0, 0, Modifier.Shift).has(Modifier.Shift)).isTrue();
|
||||
assertThat(MouseEvent.of(0, 0, Modifier.Shift).has(Modifier.Alt)).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.view.event;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.shell.component.view.event.MouseHandler.MouseHandlerArgs;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class MouseHandlerTests {
|
||||
|
||||
private static final MouseEvent EVENT = MouseEvent.of(0, 0, 0);
|
||||
private static final MouseHandlerArgs ARGS = MouseHandler.argsOf(EVENT);
|
||||
|
||||
@Test
|
||||
void handlesOtherIfThisConsumes() {
|
||||
TestMouseHandler h1 = new TestMouseHandler(true);
|
||||
TestMouseHandler h2 = new TestMouseHandler(false);
|
||||
MouseHandler composed = h1.thenIfConsumed(h2);
|
||||
composed.handle(ARGS);
|
||||
assertThat(h1.calls).isEqualTo(1);
|
||||
assertThat(h2.calls).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotHandlesOtherIfThisDoesNotConsume() {
|
||||
TestMouseHandler h1 = new TestMouseHandler(true);
|
||||
TestMouseHandler h2 = new TestMouseHandler(false);
|
||||
MouseHandler composed = h2.thenIfConsumed(h1);
|
||||
composed.handle(ARGS);
|
||||
assertThat(h1.calls).isEqualTo(0);
|
||||
assertThat(h2.calls).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlesOtherIfThisDoesNotConsume() {
|
||||
TestMouseHandler h1 = new TestMouseHandler(true);
|
||||
TestMouseHandler h2 = new TestMouseHandler(false);
|
||||
MouseHandler composed = h2.thenIfNotConsumed(h1);
|
||||
composed.handle(ARGS);
|
||||
assertThat(h1.calls).isEqualTo(1);
|
||||
assertThat(h2.calls).isEqualTo(1);
|
||||
}
|
||||
|
||||
private static class TestMouseHandler implements MouseHandler {
|
||||
|
||||
boolean willConsume;
|
||||
int calls;
|
||||
|
||||
TestMouseHandler(boolean willConsume) {
|
||||
this.willConsume = willConsume;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MouseHandlerResult handle(MouseHandlerArgs args) {
|
||||
calls++;
|
||||
return MouseHandler.resultOf(args.event(), willConsume, null, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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.screen;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.shell.component.view.control.AbstractViewTests;
|
||||
import org.springframework.shell.component.view.geom.HorizontalAlign;
|
||||
import org.springframework.shell.component.view.geom.Rectangle;
|
||||
import org.springframework.shell.component.view.geom.VerticalAlign;
|
||||
import org.springframework.shell.component.view.screen.Screen.Writer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class ScreenTests extends AbstractViewTests {
|
||||
|
||||
@Test
|
||||
void zeroDataSizeDoesntBreak() {
|
||||
assertThat(screen0x0.getItems()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void cantResizeNegative() {
|
||||
assertThatThrownBy(() -> {
|
||||
screen0x0.resize(-1, 0);
|
||||
}).hasMessageContaining("negative rows");
|
||||
assertThatThrownBy(() -> {
|
||||
screen0x0.resize(0, -1);
|
||||
}).hasMessageContaining("negative columns");
|
||||
}
|
||||
|
||||
@Test
|
||||
void printInBoxShows() {
|
||||
screen10x10.writerBuilder().build().border(0, 0, 10, 10);
|
||||
assertThat(forScreen(screen10x10)).hasBorder(0, 0, 10, 10);
|
||||
}
|
||||
|
||||
@Test
|
||||
void printsText() {
|
||||
screen24x80.writerBuilder().build().text("text", 0, 0);
|
||||
assertThat(forScreen(screen24x80)).hasHorizontalText("text", 0, 0, 4);
|
||||
}
|
||||
|
||||
@Test
|
||||
void printsTextWithForegroundColor() {
|
||||
Writer writer = screen24x80.writerBuilder().color(Color.RED).build();
|
||||
writer.text("text", 0, 0);
|
||||
assertThat(forScreen(screen24x80)).hasForegroundColor(0, 0, Color.RED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void printsTextWithForegroundColorOnLayerTopOverrides() {
|
||||
Writer writer0 = screen24x80.writerBuilder().layer(0).color(Color.BLACK).build();
|
||||
Writer writer1 = screen24x80.writerBuilder().layer(1).color(Color.RED).build();
|
||||
writer0.text("text", 0, 0);
|
||||
assertThat(forScreen(screen24x80)).hasForegroundColor(0, 0, Color.BLACK);
|
||||
writer1.text("text", 0, 0);
|
||||
assertThat(forScreen(screen24x80)).hasForegroundColor(0, 0, Color.RED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void printsTextAlign() {
|
||||
Rectangle rect = new Rectangle(1, 1, 10, 10);
|
||||
screen24x80.writerBuilder().build().text("text", rect, HorizontalAlign.CENTER, VerticalAlign.CENTER);
|
||||
assertThat(forScreen(screen24x80)).hasHorizontalText("text", 3, 5, 4);
|
||||
}
|
||||
|
||||
@Test
|
||||
void printsTextAlignInOneRowRect() {
|
||||
Rectangle rect = new Rectangle(1, 1, 10, 1);
|
||||
screen24x80.writerBuilder().build().text("text", rect, HorizontalAlign.CENTER, VerticalAlign.CENTER);
|
||||
assertThat(forScreen(screen24x80)).hasHorizontalText("text", 3, 1, 4);
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeTextFromWriterLayerOverrides() {
|
||||
DefaultScreen screen = new DefaultScreen(24, 80);
|
||||
screen.writerBuilder().layer(0).build().text("text", 0, 0);
|
||||
screen.writerBuilder().layer(1).build().text("xxx", 0, 0);
|
||||
ScreenItem[][] items = screen.getScreenItems();
|
||||
assertThat(items[0][0].getContent()).isEqualTo("x");
|
||||
assertThat(items[0][1].getContent()).isEqualTo("x");
|
||||
assertThat(items[0][2].getContent()).isEqualTo("x");
|
||||
assertThat(items[0][3].getContent()).isEqualTo("t");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
[#appendix-tui-catalog]
|
||||
=== Catalog App
|
||||
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
|
||||
|
||||
Catalog application is showing various ways how Terminal UI Framework can be used.
|
||||
In this section we discuss how this application works. It can be considered to be
|
||||
a reference application as it's using most of the features available and tries
|
||||
to follow best practices.
|
||||
|
||||
==== Create Scenario
|
||||
Every `Scenario` essentially is a sample code of a `View` as that's what catalog
|
||||
app demonstrates.
|
||||
@@ -0,0 +1,5 @@
|
||||
[#appendix-tui-control]
|
||||
=== Control
|
||||
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
|
||||
|
||||
_Control_ draws something into a screen with a given bounds.
|
||||
@@ -0,0 +1,5 @@
|
||||
[#appendix-tui-eventloop]
|
||||
=== EventLoop
|
||||
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
|
||||
|
||||
_EventLoop_ is a central system handling eventing in a framework.
|
||||
@@ -0,0 +1,5 @@
|
||||
[#appendix-tui-keyhandling]
|
||||
=== Key Handling
|
||||
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
|
||||
|
||||
Handles incoming key events.
|
||||
@@ -0,0 +1,5 @@
|
||||
[#appendix-tui-mousehandling]
|
||||
=== Mouse Handling
|
||||
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
|
||||
|
||||
Handles incoming mouse events.
|
||||
@@ -0,0 +1,6 @@
|
||||
[#appendix-tui-screen]
|
||||
=== Screen
|
||||
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
|
||||
|
||||
_Screen_ is an abstraction providing higher level concept to draw something
|
||||
into a terminal.
|
||||
@@ -0,0 +1,5 @@
|
||||
[#appendix-tui-view]
|
||||
=== View
|
||||
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
|
||||
|
||||
_View_ extends _Control_ providing integration into event loop.
|
||||
22
spring-shell-docs/src/main/asciidoc/appendices-tui.adoc
Normal file
22
spring-shell-docs/src/main/asciidoc/appendices-tui.adoc
Normal file
@@ -0,0 +1,22 @@
|
||||
[appendix]
|
||||
[#appendix-tech-intro-tui]
|
||||
== Terminal UI
|
||||
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
|
||||
|
||||
This is a technical introduction to _UI Framework_.
|
||||
|
||||
_UI Framework_ is a toolkit to build rich console apps.
|
||||
|
||||
include::appendices-tui-control.adoc[]
|
||||
|
||||
include::appendices-tui-view.adoc[]
|
||||
|
||||
include::appendices-tui-eventloop.adoc[]
|
||||
|
||||
include::appendices-tui-screen.adoc[]
|
||||
|
||||
include::appendices-tui-keyhandling.adoc[]
|
||||
|
||||
include::appendices-tui-mousehandling.adoc[]
|
||||
|
||||
include::appendices-tui-catalog.adoc[]
|
||||
@@ -1,3 +1,5 @@
|
||||
include::appendices-techical-intro.adoc[]
|
||||
|
||||
include::appendices-debugging.adoc[]
|
||||
|
||||
include::appendices-tui.adoc[]
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
[[using-shell-tui-intro]]
|
||||
=== Introduction
|
||||
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
|
||||
|
||||
Lets start with a simple app which prints "hello world" in a view.
|
||||
====
|
||||
[source, java, indent=0]
|
||||
----
|
||||
include::{snippets}/TerminalUiSnippets.java[tag=snippet1]
|
||||
----
|
||||
====
|
||||
|
||||
There is not much to see here other than `TerminalUI` is a class handling
|
||||
all logic aroung views and uses `View` as it's root view.
|
||||
@@ -0,0 +1,6 @@
|
||||
[[using-shell-tui-views-box]]
|
||||
==== BoxView
|
||||
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
|
||||
|
||||
_BoxView_ is a base implementation providing functionality to draw into a
|
||||
bounded _Rectancle_.
|
||||
@@ -0,0 +1,5 @@
|
||||
[[using-shell-tui-views-list]]
|
||||
==== ListView
|
||||
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
|
||||
|
||||
_ListView_ is a base implementation providing functionality to draw list of items.
|
||||
@@ -0,0 +1,11 @@
|
||||
[[using-shell-tui-views]]
|
||||
=== Views
|
||||
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
|
||||
|
||||
Framework provides a build-in views which are documented below.
|
||||
|
||||
TIP: To learn more about views, see <<appendix-tui-view>>.
|
||||
|
||||
include::using-shell-tui-views-box.adoc[]
|
||||
|
||||
include::using-shell-tui-views-list.adoc[]
|
||||
16
spring-shell-docs/src/main/asciidoc/using-shell-tui.adoc
Normal file
16
spring-shell-docs/src/main/asciidoc/using-shell-tui.adoc
Normal file
@@ -0,0 +1,16 @@
|
||||
[[using-shell-tui]]
|
||||
== Terminal UI
|
||||
|
||||
NOTE: Feature is experimental and subject to breaking changes until foundation
|
||||
and related concepts around framework are getting more stable.
|
||||
|
||||
_Terminal UI Framework_ is a toolkit to build rich console apps. This section is
|
||||
for those using existing features as is. If you're planning to go deeper possibly
|
||||
creating your own components <<appendix-tech-intro-tui>> provides more detailed
|
||||
documentation.
|
||||
|
||||
TIP: Catalog sample is a good place to study a real application <<appendix-tui-catalog>>.
|
||||
|
||||
include::using-shell-tui-intro.adoc[]
|
||||
|
||||
include::using-shell-tui-views.adoc[]
|
||||
@@ -10,6 +10,8 @@ include::using-shell-building.adoc[]
|
||||
|
||||
include::using-shell-components.adoc[]
|
||||
|
||||
include::using-shell-tui.adoc[]
|
||||
|
||||
include::using-shell-customization.adoc[]
|
||||
|
||||
include::using-shell-execution.adoc[]
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.jline.terminal.Terminal;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.shell.component.view.TerminalUI;
|
||||
import org.springframework.shell.component.view.control.BoxView;
|
||||
import org.springframework.shell.component.view.geom.HorizontalAlign;
|
||||
import org.springframework.shell.component.view.geom.VerticalAlign;
|
||||
|
||||
class TerminalUiSnippets {
|
||||
|
||||
class Sample {
|
||||
|
||||
// tag::snippet1[]
|
||||
@Autowired
|
||||
Terminal terminal;
|
||||
|
||||
void build() {
|
||||
TerminalUI ui = new TerminalUI(terminal);
|
||||
BoxView view = new BoxView();
|
||||
view.setDrawFunction((screen, rect) -> {
|
||||
screen.writerBuilder()
|
||||
.build()
|
||||
.text("Hello World", rect, HorizontalAlign.CENTER, VerticalAlign.CENTER);
|
||||
return rect;
|
||||
});
|
||||
ui.setRoot(view, true);
|
||||
ui.run();
|
||||
}
|
||||
// end::snippet1[]
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.shell.component.view.TerminalUI;
|
||||
import org.springframework.shell.component.view.control.AppView;
|
||||
import org.springframework.shell.component.view.control.AppView.AppViewEvent;
|
||||
import org.springframework.shell.component.view.control.GridView;
|
||||
import org.springframework.shell.component.view.control.ListView;
|
||||
import org.springframework.shell.component.view.control.ListView.ListViewOpenSelectedItemEvent;
|
||||
import org.springframework.shell.component.view.control.ListView.ListViewSelectedItemChangedEvent;
|
||||
import org.springframework.shell.component.view.control.MenuBarView;
|
||||
import org.springframework.shell.component.view.control.MenuBarView.MenuBarItem;
|
||||
import org.springframework.shell.component.view.control.MenuView.MenuItem;
|
||||
import org.springframework.shell.component.view.control.MenuView.MenuItemCheckStyle;
|
||||
import org.springframework.shell.component.view.control.StatusBarView;
|
||||
import org.springframework.shell.component.view.control.StatusBarView.StatusItem;
|
||||
import org.springframework.shell.component.view.control.View;
|
||||
import org.springframework.shell.component.view.control.cell.ListCell;
|
||||
import org.springframework.shell.component.view.event.EventLoop;
|
||||
import org.springframework.shell.component.view.event.KeyEvent.Key;
|
||||
import org.springframework.shell.component.view.geom.Rectangle;
|
||||
import org.springframework.shell.component.view.message.ShellMessageBuilder;
|
||||
import org.springframework.shell.component.view.screen.Screen;
|
||||
import org.springframework.shell.component.view.screen.Screen.Writer;
|
||||
import org.springframework.shell.samples.catalog.scenario.Scenario;
|
||||
import org.springframework.shell.samples.catalog.scenario.ScenarioComponent;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Catalog app logic. Builds a simple application ui where scenarios can be
|
||||
* selected and run.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class Catalog {
|
||||
|
||||
// ref types helping with deep nested generics from events
|
||||
private final static ParameterizedTypeReference<ListViewOpenSelectedItemEvent<ScenarioData>> LISTVIEW_SCENARIO_TYPEREF
|
||||
= new ParameterizedTypeReference<ListViewOpenSelectedItemEvent<ScenarioData>>() {};
|
||||
private final static ParameterizedTypeReference<ListViewSelectedItemChangedEvent<String>> LISTVIEW_STRING_TYPEREF
|
||||
= new ParameterizedTypeReference<ListViewSelectedItemChangedEvent<String>>() {};
|
||||
|
||||
// mapping from category name to scenarios(can belong to multiple categories)
|
||||
private final Map<String, List<ScenarioData>> categoryMap = new TreeMap<>();
|
||||
private final Terminal terminal;
|
||||
private View currentScenarioView = null;
|
||||
private TerminalUI ui;
|
||||
private ListView<String> categories;
|
||||
private EventLoop eventLoop;
|
||||
|
||||
public Catalog(Terminal terminal, List<Scenario> scenarios) {
|
||||
this.terminal = terminal;
|
||||
mapScenarios(scenarios);
|
||||
}
|
||||
|
||||
private void mapScenarios(List<Scenario> scenarios) {
|
||||
// we blindly expect scenario to have ScenarioComponent annotation with all fields
|
||||
scenarios.forEach(sce -> {
|
||||
ScenarioComponent ann = AnnotationUtils.findAnnotation(sce.getClass(), ScenarioComponent.class);
|
||||
if (ann != null) {
|
||||
String name = ann.name();
|
||||
String description = ann.description();
|
||||
String[] category = ann.category();
|
||||
if (StringUtils.hasText(name) && StringUtils.hasText(description) && !ObjectUtils.isEmpty(category)) {
|
||||
for (String cat : category) {
|
||||
ScenarioData scenarioData = new ScenarioData(sce, name, description, category);
|
||||
categoryMap.computeIfAbsent(Scenario.CATEGORY_ALL, key -> new ArrayList<>()).add(scenarioData);
|
||||
categoryMap.computeIfAbsent(cat, key -> new ArrayList<>()).add(scenarioData);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void requestQuit() {
|
||||
Message<String> msg = ShellMessageBuilder.withPayload("int")
|
||||
.setEventType(EventLoop.Type.SYSTEM)
|
||||
.setPriority(0)
|
||||
.build();
|
||||
eventLoop.dispatch(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main run loop. Builds the ui and exits when user requests exit.
|
||||
*/
|
||||
public void run() {
|
||||
ui = new TerminalUI(terminal);
|
||||
eventLoop = ui.getEventLoop();
|
||||
AppView app = buildScenarioBrowser(eventLoop, ui);
|
||||
|
||||
// handle logic to switch between main scenario browser
|
||||
// and currently active scenario
|
||||
eventLoop.onDestroy(eventLoop.keyEvents()
|
||||
.doOnNext(m -> {
|
||||
if (m.getPlainKey() == Key.q && m.hasCtrl()) {
|
||||
if (currentScenarioView != null) {
|
||||
currentScenarioView = null;
|
||||
ui.setRoot(app, true);
|
||||
}
|
||||
else {
|
||||
requestQuit();
|
||||
}
|
||||
}
|
||||
})
|
||||
.subscribe());
|
||||
|
||||
// start main scenario browser
|
||||
ui.setRoot(app, true);
|
||||
ui.setFocus(categories);
|
||||
categories.setSelected(0);
|
||||
ui.run();
|
||||
}
|
||||
|
||||
private AppView buildScenarioBrowser(EventLoop eventLoop, TerminalUI component) {
|
||||
// we use main app view to represent scenario browser
|
||||
AppView app = new AppView();
|
||||
app.setEventLoop(eventLoop);
|
||||
|
||||
// category selector on left, scenario selector on right
|
||||
GridView grid = new GridView();
|
||||
grid.setRowSize(1, 0, 1);
|
||||
grid.setColumnSize(30, 0);
|
||||
|
||||
categories = buildCategorySelector(eventLoop);
|
||||
ListView<ScenarioData> scenarios = buildScenarioSelector(eventLoop);
|
||||
|
||||
// handle event when scenario is chosen
|
||||
eventLoop.onDestroy(eventLoop.viewEvents(LISTVIEW_SCENARIO_TYPEREF, scenarios)
|
||||
.subscribe(event -> {
|
||||
View view = event.args().item().scenario().configure(eventLoop).build();
|
||||
component.setRoot(view, true);
|
||||
currentScenarioView = view;
|
||||
}));
|
||||
|
||||
|
||||
// handle event when category selection is changed
|
||||
eventLoop.onDestroy(eventLoop.viewEvents(LISTVIEW_STRING_TYPEREF, categories)
|
||||
.subscribe(event -> {
|
||||
if (event.args().item() != null) {
|
||||
String selected = event.args().item();
|
||||
List<ScenarioData> list = categoryMap.get(selected);
|
||||
scenarios.setItems(list);
|
||||
}
|
||||
}));
|
||||
|
||||
// handle focus change between lists
|
||||
eventLoop.onDestroy(eventLoop.viewEvents(AppViewEvent.class, app)
|
||||
.subscribe(event -> {
|
||||
switch (event.args().direction()) {
|
||||
case NEXT -> ui.setFocus(scenarios);
|
||||
case PREVIOUS -> ui.setFocus(categories);
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
// We place statusbar below categories and scenarios
|
||||
MenuBarView menuBar = buildMenuBar(eventLoop);
|
||||
StatusBarView statusBar = buildStatusBar(eventLoop);
|
||||
grid.addItem(menuBar, 0, 0, 1, 2, 0, 0);
|
||||
grid.addItem(categories, 1, 0, 1, 1, 0, 0);
|
||||
grid.addItem(scenarios, 1, 1, 1, 1, 0, 0);
|
||||
grid.addItem(statusBar, 2, 0, 1, 2, 0, 0);
|
||||
app.setMain(grid);
|
||||
return app;
|
||||
}
|
||||
|
||||
private ListView<String> buildCategorySelector(EventLoop eventLoop) {
|
||||
ListView<String> categories = new ListView<>();
|
||||
categories.setEventLoop(eventLoop);
|
||||
List<String> items = List.copyOf(categoryMap.keySet());
|
||||
categories.setItems(items);
|
||||
categories.setTitle("Categories");
|
||||
categories.setShowBorder(true);
|
||||
return categories;
|
||||
}
|
||||
|
||||
private static class ScenarioListCell extends ListCell<ScenarioData> {
|
||||
|
||||
@Override
|
||||
public void draw(Screen screen) {
|
||||
Rectangle rect = getRect();
|
||||
Writer writer = screen.writerBuilder().style(getStyle()).build();
|
||||
writer.text(String.format("%-20s %s", getItem().name(), getItem().description()), rect.x(), rect.y());
|
||||
writer.background(rect, getBackgroundColor());
|
||||
}
|
||||
}
|
||||
|
||||
private ListView<ScenarioData> buildScenarioSelector(EventLoop eventLoop) {
|
||||
ListView<ScenarioData> scenarios = new ListView<>();
|
||||
scenarios.setEventLoop(eventLoop);
|
||||
scenarios.setTitle("Scenarios");
|
||||
scenarios.setShowBorder(true);
|
||||
scenarios.setCellFactory(list -> new ScenarioListCell());
|
||||
return scenarios;
|
||||
}
|
||||
|
||||
private MenuBarView buildMenuBar(EventLoop eventLoop) {
|
||||
Runnable quitAction = () -> requestQuit();
|
||||
MenuBarView menuBar = MenuBarView.of(
|
||||
MenuBarItem.of("File",
|
||||
MenuItem.of("Quit", MenuItemCheckStyle.NOCHECK, quitAction)),
|
||||
MenuBarItem.of("Theme",
|
||||
MenuItem.of("Dump", MenuItemCheckStyle.RADIO),
|
||||
MenuItem.of("Funky", MenuItemCheckStyle.RADIO)
|
||||
),
|
||||
MenuBarItem.of("Help",
|
||||
MenuItem.of("About"))
|
||||
);
|
||||
|
||||
menuBar.setEventLoop(eventLoop);
|
||||
return menuBar;
|
||||
}
|
||||
|
||||
private StatusBarView buildStatusBar(EventLoop eventLoop) {
|
||||
Runnable quitAction = () -> requestQuit();
|
||||
StatusBarView statusBar = new StatusBarView();
|
||||
statusBar.setEventLoop(eventLoop);
|
||||
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;
|
||||
}
|
||||
|
||||
private record ScenarioData(Scenario scenario, String name, String description, String[] category){};
|
||||
|
||||
}
|
||||
@@ -15,16 +15,29 @@
|
||||
*/
|
||||
package org.springframework.shell.samples.catalog;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.shell.command.annotation.Command;
|
||||
import org.springframework.shell.command.annotation.Option;
|
||||
import org.springframework.shell.samples.catalog.scenario.Scenario;
|
||||
import org.springframework.shell.standard.AbstractShellComponent;
|
||||
|
||||
/**
|
||||
* Main command access point to view showcase catalog.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@Command
|
||||
public class CatalogCommand {
|
||||
public class CatalogCommand extends AbstractShellComponent {
|
||||
|
||||
@Command
|
||||
String catalog(
|
||||
@Option() String arg
|
||||
) {
|
||||
return String.format("Hi arg=%s", arg);
|
||||
private final List<Scenario> scenarios;
|
||||
|
||||
public CatalogCommand(List<Scenario> scenarios) {
|
||||
this.scenarios = scenarios;
|
||||
}
|
||||
|
||||
@Command(command = "catalog")
|
||||
public void catalog() {
|
||||
Catalog catalog = new Catalog(getTerminal(), scenarios);
|
||||
catalog.run();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,10 @@ package org.springframework.shell.samples.catalog;
|
||||
import org.springframework.boot.Banner.Mode;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.shell.command.annotation.CommandScan;
|
||||
|
||||
@SpringBootApplication
|
||||
@CommandScan
|
||||
public class SpringShellApplication {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import org.springframework.shell.component.view.event.EventLoop;
|
||||
|
||||
/**
|
||||
* Base implementation of a {@link Scenario} helping to avoid some bloatware.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public abstract class AbstractScenario implements Scenario {
|
||||
|
||||
private EventLoop eventloop;
|
||||
|
||||
@Override
|
||||
public Scenario configure(EventLoop eventloop) {
|
||||
this.eventloop = eventloop;
|
||||
return this;
|
||||
}
|
||||
|
||||
protected EventLoop getEventloop() {
|
||||
return eventloop;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import org.springframework.shell.component.view.control.View;
|
||||
import org.springframework.shell.component.view.event.EventLoop;
|
||||
|
||||
/**
|
||||
* {@link Scenario} participates in a catalog showcase.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface Scenario {
|
||||
|
||||
// Common category names
|
||||
public static final String CATEGORY_ALL = "All Scenarios";
|
||||
public static final String CATEGORY_LISTVIEW = "ListView";
|
||||
public static final String CATEGORY_BOXVIEW = "BoxView";
|
||||
public static final String CATEGORY_LAYOUT = "Layout";
|
||||
public static final String CATEGORY_OTHER = "Other";
|
||||
|
||||
/**
|
||||
* Build a {@link View} to be shown with a scenario.
|
||||
*
|
||||
* @return view of a scenario
|
||||
*/
|
||||
View build();
|
||||
|
||||
/**
|
||||
* Configure scenario.
|
||||
*
|
||||
* @param eventloop eventloop for scenario
|
||||
* @return scenario for chaining
|
||||
*/
|
||||
Scenario configure(EventLoop eventloop);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.stereotype.Indexed;
|
||||
|
||||
/**
|
||||
* Annotation needed for a scenarios to get hooked up into a catalog app.
|
||||
* Typically all fields in this annotation needs to have content to get attached
|
||||
* into a catalog app.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Indexed
|
||||
@Component
|
||||
public @interface ScenarioComponent {
|
||||
|
||||
/**
|
||||
* Define a name of a scenario.
|
||||
*
|
||||
* @return name of a scenario
|
||||
*/
|
||||
String name() default "";
|
||||
|
||||
/**
|
||||
* Define a short description of a scenario.
|
||||
*
|
||||
* @return short description of a scenario
|
||||
*/
|
||||
String description() default "";
|
||||
|
||||
/**
|
||||
* Define a categories of a scenario.
|
||||
*
|
||||
* @return categories of a scenario
|
||||
*/
|
||||
String[] category() default {};
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.box;
|
||||
|
||||
import org.springframework.shell.component.view.control.BoxView;
|
||||
import org.springframework.shell.component.view.control.View;
|
||||
import org.springframework.shell.component.view.geom.HorizontalAlign;
|
||||
import org.springframework.shell.component.view.geom.VerticalAlign;
|
||||
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 = "Draw Function", description = "BoxView with DrawFunction", category = {
|
||||
Scenario.CATEGORY_BOXVIEW })
|
||||
public class DrawFunctionScenario extends AbstractScenario {
|
||||
|
||||
@Override
|
||||
public View build() {
|
||||
BoxView view = new BoxView();
|
||||
view.setDrawFunction((screen, rect) -> {
|
||||
screen.writerBuilder().build().text("Hello World", rect, HorizontalAlign.CENTER, VerticalAlign.CENTER);
|
||||
return rect;
|
||||
});
|
||||
return view;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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.box;
|
||||
|
||||
import org.springframework.shell.component.view.control.BoxView;
|
||||
import org.springframework.shell.component.view.control.View;
|
||||
import org.springframework.shell.component.view.geom.HorizontalAlign;
|
||||
import org.springframework.shell.component.view.screen.Color;
|
||||
import org.springframework.shell.component.view.screen.ScreenItem;
|
||||
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 = "Simple boxview", description = "BoxView with color and style", category = {
|
||||
Scenario.CATEGORY_BOXVIEW })
|
||||
public class SimpleBoxViewScenario extends AbstractScenario {
|
||||
|
||||
@Override
|
||||
public View build() {
|
||||
BoxView box = new BoxView();
|
||||
box.setTitle("Title");
|
||||
box.setShowBorder(true);
|
||||
box.setBackgroundColor(Color.KHAKI4);
|
||||
box.setTitleColor(Color.RED);
|
||||
box.setTitleStyle(ScreenItem.STYLE_BOLD | ScreenItem.STYLE_ITALIC);
|
||||
box.setTitleAlign(HorizontalAlign.CENTER);
|
||||
return box;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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.grid;
|
||||
|
||||
import org.springframework.shell.component.view.control.BoxView;
|
||||
import org.springframework.shell.component.view.control.GridView;
|
||||
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 = "Simple gridview", description = "GridView sample", category = { Scenario.CATEGORY_LAYOUT })
|
||||
public class SimpleGridViewScenario extends AbstractScenario {
|
||||
|
||||
@Override
|
||||
public View build() {
|
||||
BoxView menu = new BoxView();
|
||||
menu.setBackgroundColor(Color.KHAKI4);
|
||||
menu.setTitle("Menu");
|
||||
menu.setShowBorder(true);
|
||||
|
||||
BoxView main = new BoxView();
|
||||
main.setBackgroundColor(Color.KHAKI4);
|
||||
main.setTitle("Main");
|
||||
main.setShowBorder(true);
|
||||
|
||||
BoxView sideBar = new BoxView();
|
||||
sideBar.setBackgroundColor(Color.KHAKI4);
|
||||
sideBar.setTitle("Sidebar");
|
||||
sideBar.setShowBorder(true);
|
||||
|
||||
BoxView header = new BoxView();
|
||||
header.setBackgroundColor(Color.KHAKI4);
|
||||
header.setTitle("Header");
|
||||
header.setShowBorder(true);
|
||||
|
||||
BoxView footer = new BoxView();
|
||||
footer.setBackgroundColor(Color.KHAKI4);
|
||||
footer.setTitle("Footer");
|
||||
footer.setShowBorder(true);
|
||||
|
||||
GridView grid = new GridView();
|
||||
grid.setBackgroundColor(Color.KHAKI3);
|
||||
grid.setTitle("Grid");
|
||||
grid.setShowBorder(true);
|
||||
|
||||
grid.setRowSize(3, 0, 3);
|
||||
grid.setColumnSize(30, 0, 30);
|
||||
// grid.setShowBorder(true);
|
||||
grid.setShowBorders(true);
|
||||
grid.addItem(header, 0, 0, 1, 3, 0, 0);
|
||||
grid.addItem(footer, 2, 0, 1, 3, 0, 0);
|
||||
|
||||
grid.addItem(menu, 0, 0, 0, 0, 0, 0);
|
||||
grid.addItem(main, 1, 0, 1, 3, 0, 0);
|
||||
grid.addItem(sideBar, 0, 0, 0, 0, 0, 0);
|
||||
|
||||
grid.addItem(menu, 1, 0, 1, 1, 0, 100);
|
||||
grid.addItem(main, 1, 1, 1, 1, 0, 100);
|
||||
grid.addItem(sideBar, 1, 2, 1, 1, 0, 100);
|
||||
return grid;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.listview;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.springframework.shell.component.view.control.ListView;
|
||||
import org.springframework.shell.component.view.control.View;
|
||||
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 = "Basic", description = "Basic list", category = { Scenario.CATEGORY_LISTVIEW })
|
||||
public class SimpleListViewScenario extends AbstractScenario {
|
||||
|
||||
@Override
|
||||
public View build() {
|
||||
ListView<String> view = new ListView<>();
|
||||
view.setEventLoop(getEventloop());
|
||||
view.setItems(Arrays.asList("item1", "item2"));
|
||||
return view;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* 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 java.time.Duration;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.shell.component.view.control.BoxView;
|
||||
import org.springframework.shell.component.view.control.View;
|
||||
import org.springframework.shell.component.view.event.EventLoop;
|
||||
import org.springframework.shell.component.view.event.KeyEvent.Key;
|
||||
import org.springframework.shell.component.view.geom.HorizontalAlign;
|
||||
import org.springframework.shell.component.view.geom.Rectangle;
|
||||
import org.springframework.shell.component.view.geom.VerticalAlign;
|
||||
import org.springframework.shell.component.view.message.ShellMessageBuilder;
|
||||
import org.springframework.shell.component.view.message.ShellMessageHeaderAccessor;
|
||||
import org.springframework.shell.component.view.message.StaticShellMessageHeaderAccessor;
|
||||
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 = "Clock", description = "Showing time ticks", category = { Scenario.CATEGORY_OTHER })
|
||||
public class ClockScenario extends AbstractScenario {
|
||||
|
||||
@Override
|
||||
public View build() {
|
||||
// simply use a plain box view to draw a date using a custom
|
||||
// draw function
|
||||
BoxView root = new BoxView();
|
||||
root.setTitle("What's o'clock");
|
||||
root.setShowBorder(true);
|
||||
|
||||
// store text to print
|
||||
AtomicReference<String> ref = new AtomicReference<>();
|
||||
|
||||
// dispatch dates as messages
|
||||
Flux<Message<?>> dates = Flux.interval(Duration.ofSeconds(1)).map(l -> {
|
||||
String date = new Date().toString();
|
||||
Message<String> message = MessageBuilder
|
||||
.withPayload(date)
|
||||
.setHeader(ShellMessageHeaderAccessor.EVENT_TYPE, EventLoop.Type.USER)
|
||||
.build();
|
||||
return message;
|
||||
});
|
||||
getEventloop().dispatch(dates);
|
||||
|
||||
// process dates
|
||||
getEventloop().onDestroy(getEventloop().events()
|
||||
.filter(m -> EventLoop.Type.USER.equals(StaticShellMessageHeaderAccessor.getEventType(m)))
|
||||
.subscribe(m -> {
|
||||
if (m.getPayload() instanceof String s) {
|
||||
ref.set(s);
|
||||
getEventloop().dispatch(ShellMessageBuilder.ofRedraw());
|
||||
}
|
||||
}));
|
||||
|
||||
// testing for animations for now
|
||||
AtomicInteger animX = new AtomicInteger();
|
||||
getEventloop().onDestroy(getEventloop().events()
|
||||
.filter(m -> EventLoop.Type.SYSTEM.equals(StaticShellMessageHeaderAccessor.getEventType(m)))
|
||||
.filter(m -> m.getHeaders().containsKey("animationtick"))
|
||||
.subscribe(m -> {
|
||||
Object payload = m.getPayload();
|
||||
if (payload instanceof Integer i) {
|
||||
animX.set(i);
|
||||
getEventloop().dispatch(ShellMessageBuilder.ofRedraw());
|
||||
}
|
||||
}));
|
||||
|
||||
AtomicReference<HorizontalAlign> hAlign = new AtomicReference<>(HorizontalAlign.CENTER);
|
||||
AtomicReference<VerticalAlign> vAlign = new AtomicReference<>(VerticalAlign.CENTER);
|
||||
|
||||
getEventloop().onDestroy(getEventloop().keyEvents()
|
||||
.subscribe(event -> {
|
||||
switch (event.key()) {
|
||||
case Key.CursorDown -> {
|
||||
if (vAlign.get() == VerticalAlign.TOP) {
|
||||
vAlign.set(VerticalAlign.CENTER);
|
||||
}
|
||||
else if (vAlign.get() == VerticalAlign.CENTER) {
|
||||
vAlign.set(VerticalAlign.BOTTOM);
|
||||
}
|
||||
}
|
||||
case Key.CursorUp -> {
|
||||
if (vAlign.get() == VerticalAlign.BOTTOM) {
|
||||
vAlign.set(VerticalAlign.CENTER);
|
||||
}
|
||||
else if (vAlign.get() == VerticalAlign.CENTER) {
|
||||
vAlign.set(VerticalAlign.TOP);
|
||||
}
|
||||
}
|
||||
case Key.CursorLeft -> {
|
||||
if (hAlign.get() == HorizontalAlign.RIGHT) {
|
||||
hAlign.set(HorizontalAlign.CENTER);
|
||||
}
|
||||
else if (hAlign.get() == HorizontalAlign.CENTER) {
|
||||
hAlign.set(HorizontalAlign.LEFT);
|
||||
}
|
||||
}
|
||||
case Key.CursorRight -> {
|
||||
Message<String> animStart = MessageBuilder
|
||||
.withPayload("")
|
||||
.setHeader(ShellMessageHeaderAccessor.EVENT_TYPE, EventLoop.Type.SYSTEM)
|
||||
.setHeader("animationstart", true)
|
||||
.build();
|
||||
getEventloop().dispatch(animStart);
|
||||
}
|
||||
};
|
||||
|
||||
}));
|
||||
|
||||
// draw current date
|
||||
root.setDrawFunction((screen, rect) -> {
|
||||
int a = animX.get();
|
||||
Rectangle r = new Rectangle(rect.x() + 1 + a, rect.y() + 1, rect.width() - 2, rect.height() - 2);
|
||||
// Rectangle r = new View.Rectangle(rect.x() + 1, rect.y() + 1, rect.width() - 2, rect.height() - 2);
|
||||
String s = ref.get();
|
||||
if (s != null) {
|
||||
screen.writerBuilder().build().text(s, r, hAlign.get(), vAlign.get());
|
||||
}
|
||||
return rect;
|
||||
});
|
||||
return root;
|
||||
}
|
||||
}
|
||||
@@ -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.catalog.scenario.other;
|
||||
|
||||
import org.springframework.shell.component.view.control.InputView;
|
||||
import org.springframework.shell.component.view.control.View;
|
||||
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 = "Simple inputview", description = "InputView sample", category = { Scenario.CATEGORY_OTHER })
|
||||
public class SimpleInputViewScenario extends AbstractScenario {
|
||||
|
||||
@Override
|
||||
public View build() {
|
||||
InputView view = new InputView();
|
||||
view.setEventLoop(getEventloop());
|
||||
view.setTitle("Input");
|
||||
view.setShowBorder(true);
|
||||
return view;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
/*
|
||||
* 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 java.time.Duration;
|
||||
import java.util.LinkedList;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.shell.component.view.control.BoxView;
|
||||
import org.springframework.shell.component.view.control.View;
|
||||
import org.springframework.shell.component.view.event.KeyEvent.Key;
|
||||
import org.springframework.shell.component.view.message.ShellMessageBuilder;
|
||||
import org.springframework.shell.component.view.screen.Screen;
|
||||
import org.springframework.shell.component.view.screen.Screen.Writer;
|
||||
import org.springframework.shell.samples.catalog.scenario.AbstractScenario;
|
||||
import org.springframework.shell.samples.catalog.scenario.Scenario;
|
||||
import org.springframework.shell.samples.catalog.scenario.ScenarioComponent;
|
||||
|
||||
/**
|
||||
* Scenario implementing a classic snake game.
|
||||
*
|
||||
* Demonstrates how we can just use box view to draw something
|
||||
* manually with its draw function.
|
||||
*
|
||||
* Game logic.
|
||||
* 1. Snake starts in a center, initial direction needs arrow key
|
||||
* 2. Arrows control snake direction
|
||||
* 3. Eating a food crows a snake, new food is generated
|
||||
* 4. Game ends if snake eats itself or goes out of bounds
|
||||
* 5. Game ends if perfect score is established
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@ScenarioComponent(name = "Snake", description = "Classic snake game", category = { Scenario.CATEGORY_OTHER })
|
||||
public class SnakeGameScenario extends AbstractScenario {
|
||||
|
||||
@Override
|
||||
public View build() {
|
||||
SnakeGame snakeGame = new SnakeGame(10, 15);
|
||||
BoxView view = new BoxView();
|
||||
view.setTitle("Snake");
|
||||
view.setShowBorder(true);
|
||||
|
||||
// we're outside of a view so no bindings,
|
||||
// just subscribe to events and handle what is needed.
|
||||
getEventloop().onDestroy(getEventloop().keyEvents()
|
||||
.subscribe(event -> {
|
||||
Integer direction = switch (event.key()) {
|
||||
case Key.CursorDown -> 1;
|
||||
case Key.CursorUp -> -1;
|
||||
case Key.CursorLeft -> -2;
|
||||
case Key.CursorRight -> 2;
|
||||
default -> 0;
|
||||
};
|
||||
if (direction != null) {
|
||||
snakeGame.update(direction);
|
||||
}
|
||||
}));
|
||||
|
||||
// schedule game updates
|
||||
getEventloop().onDestroy(Flux.interval(Duration.ofMillis(500))
|
||||
.subscribe(l -> {
|
||||
snakeGame.update(0);
|
||||
getEventloop().dispatch(ShellMessageBuilder.ofRedraw());
|
||||
}
|
||||
));
|
||||
|
||||
// draw game area
|
||||
view.setDrawFunction((screen, rect) -> {
|
||||
snakeGame.draw(screen);
|
||||
return rect;
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
private static class SnakeGame {
|
||||
Board board;
|
||||
Game game;
|
||||
|
||||
SnakeGame(int rows, int cols) {
|
||||
// snake starts from a center
|
||||
Cell initial = new Cell(rows / 2, cols / 2, 1);
|
||||
|
||||
Snake snake = new Snake(initial);
|
||||
board = new Board(rows, cols, initial);
|
||||
game = new Game(snake, board);
|
||||
}
|
||||
|
||||
void update(int direction) {
|
||||
if (direction != 0) {
|
||||
game.direction = direction;
|
||||
}
|
||||
game.update();
|
||||
}
|
||||
|
||||
void draw(Screen screen) {
|
||||
Cell[][] cells = board.cells;
|
||||
|
||||
Writer writer = screen.writerBuilder().build();
|
||||
// draw game area border
|
||||
writer.border(2, 2, board.cols + 2, board.rows + 2);
|
||||
|
||||
// draw info
|
||||
String info = game.gameOver ? "Game Over" : String.format("Points %s", game.points);
|
||||
writer.text(info, 2, 1);
|
||||
|
||||
// draw snake and food
|
||||
for (int i = 0; i < cells.length; i++) {
|
||||
for (int j = 0; j < cells[i].length; j++) {
|
||||
Cell cell = cells[i][j];
|
||||
String c = "";
|
||||
if (cell.type == 1) {
|
||||
c = "x";
|
||||
}
|
||||
else if (cell.type == -1) {
|
||||
c = "o";
|
||||
}
|
||||
writer.text(c, j + 3, i + 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class Cell {
|
||||
final int row, col;
|
||||
// 0 - empty, > 0 - snake, < 0 - food
|
||||
int type;
|
||||
|
||||
Cell(int row, int col, int type) {
|
||||
this.row = row;
|
||||
this.col = col;
|
||||
this.type = type;
|
||||
}
|
||||
}
|
||||
|
||||
private static class Board {
|
||||
final int rows, cols;
|
||||
Cell[][] cells;
|
||||
|
||||
Board(int rows, int cols, Cell initial) {
|
||||
this.rows = rows;
|
||||
this.cols = cols;
|
||||
cells = new Cell[rows][cols];
|
||||
for (int row = 0; row < rows; row++) {
|
||||
for (int col = 0; col < cols; col++) {
|
||||
cells[row][col] = new Cell(row, col, 0);
|
||||
}
|
||||
}
|
||||
cells[initial.row][initial.col] = initial;
|
||||
food();
|
||||
}
|
||||
|
||||
void food() {
|
||||
int row = 0, column = 0;
|
||||
while (true) {
|
||||
row = (int) (Math.random() * rows);
|
||||
column = (int) (Math.random() * cols);
|
||||
if (cells[row][column].type != 1)
|
||||
break;
|
||||
}
|
||||
cells[row][column].type = -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static class Snake {
|
||||
LinkedList<Cell> cells = new LinkedList<>();
|
||||
Cell head;
|
||||
|
||||
Snake(Cell cell) {
|
||||
head = cell;
|
||||
cells.add(head);
|
||||
head.type = 1;
|
||||
}
|
||||
|
||||
void move(Cell cell, boolean grow) {
|
||||
if (!grow) {
|
||||
Cell tail = cells.removeLast();
|
||||
tail.type = 0;
|
||||
}
|
||||
head = cell;
|
||||
head.type = 1;
|
||||
cells.addFirst(head);
|
||||
}
|
||||
|
||||
boolean checkCrash(Cell next) {
|
||||
for (Cell cell : cells) {
|
||||
if (cell == next) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static class Game {
|
||||
Snake snake;
|
||||
Board board;
|
||||
int direction;
|
||||
int points;
|
||||
boolean gameOver;
|
||||
|
||||
Game(Snake snake, Board board) {
|
||||
this.snake = snake;
|
||||
this.board = board;
|
||||
this.direction = 0;
|
||||
}
|
||||
|
||||
void update() {
|
||||
if (direction == 0) {
|
||||
return;
|
||||
}
|
||||
Cell next = next(snake.head);
|
||||
if (next == null || snake.checkCrash(next)) {
|
||||
direction = 0;
|
||||
gameOver = true;
|
||||
}
|
||||
else {
|
||||
boolean foundFood = next.type == -1;
|
||||
snake.move(next, foundFood);
|
||||
if (foundFood) {
|
||||
board.food();
|
||||
points++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cell next(Cell cell) {
|
||||
int row = cell.row;
|
||||
int col = cell.col;
|
||||
// return null if we're about to go out of bounds
|
||||
if (direction == 2) {
|
||||
col++;
|
||||
if (col >= board.cols) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else if (direction == -2) {
|
||||
col--;
|
||||
if (col < 0) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else if (direction == 1) {
|
||||
row++;
|
||||
if (row >= board.rows) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else if (direction == -1) {
|
||||
row--;
|
||||
if (row < 0) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return board.cells[row][col];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user