Hide statusbar in a catalog app

- Can clear items from a GridView
- Full refactor for AppView now using GridView internally
  and can hide/show menu/status.
- Add function key bindings f1-f10
- Modify catalog app to use new features in an AppView, listen
  F10 key to toggle statusbar visibility.
- Relates #805
- Relates #807
- Relates #811
This commit is contained in:
Janne Valkealahti
2023-07-22 14:21:10 +01:00
parent cd77272091
commit 4b927be807
7 changed files with 333 additions and 44 deletions

View File

@@ -15,9 +15,6 @@
*/
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;
@@ -25,6 +22,7 @@ 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.util.Assert;
/**
* {@link AppView} provides an opinionated terminal UI application view
@@ -34,40 +32,69 @@ import org.springframework.shell.component.view.screen.Screen;
*/
public class AppView extends BoxView {
private final static Logger log = LoggerFactory.getLogger(AppView.class);
private GridView grid;
private View main;
private View modal;
private View menu;
private View status;
private boolean menuVisible = true;
private boolean statusVisible = true;
public AppView(View main, View menuBar, View statusBar) {
Assert.notNull(main, "Main view must be set");
Assert.notNull(menuBar, "Menubar view must be set");
Assert.notNull(statusBar, "Statusbar view must be set");
this.main = main;
this.menu = menuBar;
this.status = statusBar;
initLayout();
}
private void initLayout() {
grid = new GridView();
grid.setRowSize(1, 0, 1);
grid.setColumnSize(0);
grid.clearItems();
if (menuVisible && statusVisible) {
grid.addItem(menu, 0, 0, 1, 1, 0, 0);
grid.addItem(main, 1, 0, 1, 1, 0, 0);
grid.addItem(status, 2, 0, 1, 1, 0, 0);
}
else if (!menuVisible && !statusVisible) {
grid.addItem(menu, 0, 0, 0, 0, 0, 0);
grid.addItem(main, 0, 0, 3, 1, 0, 0);
grid.addItem(status, 2, 0, 0, 0, 0, 0);
}
else if (menuVisible && !statusVisible) {
grid.addItem(menu, 0, 0, 1, 1, 0, 0);
grid.addItem(main, 1, 0, 2, 1, 0, 0);
grid.addItem(status, 2, 0, 0, 1, 0, 0);
}
else if (!menuVisible && statusVisible) {
grid.addItem(menu, 0, 0, 0, 1, 0, 0);
grid.addItem(main, 0, 0, 2, 1, 0, 0);
grid.addItem(status, 2, 0, 1, 1, 0, 0);
}
}
@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);
if (grid != null) {
grid.setRect(rect.x(), rect.y(), rect.width(), rect.height());
grid.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();
MouseHandler handler = grid.getMouseHandler();
return handler.thenIfNotConsumed(super.getMouseHandler());
}
@Override
public KeyHandler getKeyHandler() {
KeyHandler handler1 = args -> {
KeyHandler handler = args -> {
KeyEvent event = args.event();
boolean consumed = false;
if (event.isKey(Key.CursorLeft)) {
@@ -80,25 +107,50 @@ public class AppView extends BoxView {
}
return KeyHandler.resultOf(event, consumed, null);
};
KeyHandler handler2 = main != null ? main.getKeyHandler() : super.getKeyHandler();
return handler2.thenIfNotConsumed(handler1);
KeyHandler otherHandler = main != null ? main.getKeyHandler() : super.getKeyHandler();
return otherHandler.thenIfNotConsumed(handler);
}
@Override
public boolean hasFocus() {
if (main != null) {
return main.hasFocus();
if (grid != null) {
return grid.hasFocus();
}
return super.hasFocus();
}
public void setMain(View main) {
this.main = main;
/**
* Sets visibility for a {@code menubar}.
*
* @param visible the menubar visibility
*/
public void setMenuBarVisible(boolean visible) {
menuVisible = visible;
initLayout();
}
public void setModal(View modal) {
this.modal = modal;
/**
* Sets visibility for a {@code statusbar}.
*
* @param visible the statusbar visibility
*/
public void setStatusBarVisible(boolean visible) {
statusVisible = visible;
initLayout();
}
/**
* Toggles a {@code menubar} visibility.
*/
public void toggleMenuBarVisibility() {
setMenuBarVisible(!menuVisible);
}
/**
* Toggles a {@code statusbar} visibility.
*/
public void toggleStatusBarVisibility() {
setStatusBarVisible(!statusVisible);
}
/**

View File

@@ -173,6 +173,13 @@ public class GridView extends BoxView {
return this;
}
/**
* Remove all items.
*/
public void clearItems() {
this.gridItems.clear();
}
/**
* Defines if borders is shown.
*

View File

@@ -61,6 +61,17 @@ public class KeyBinder {
keyMap.bind(KeyEvent.Key.CursorUp, key(terminal, Capability.key_up));
keyMap.bind(KeyEvent.Key.CursorDown, key(terminal, Capability.key_down));
keyMap.bind(KeyEvent.Key.f1, key(terminal, Capability.key_f1));
keyMap.bind(KeyEvent.Key.f2, key(terminal, Capability.key_f2));
keyMap.bind(KeyEvent.Key.f3, key(terminal, Capability.key_f3));
keyMap.bind(KeyEvent.Key.f4, key(terminal, Capability.key_f4));
keyMap.bind(KeyEvent.Key.f5, key(terminal, Capability.key_f5));
keyMap.bind(KeyEvent.Key.f6, key(terminal, Capability.key_f6));
keyMap.bind(KeyEvent.Key.f7, key(terminal, Capability.key_f7));
keyMap.bind(KeyEvent.Key.f8, key(terminal, Capability.key_f8));
keyMap.bind(KeyEvent.Key.f9, key(terminal, Capability.key_f9));
keyMap.bind(KeyEvent.Key.f10, key(terminal, Capability.key_f10));
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)));

View File

@@ -112,6 +112,19 @@ public record KeyEvent(int key) {
public static final int Tab = 0x100007;
public static final int Backtab = 0x100008;
public static final int f1 = 0x100009;
public static final int f2 = 0x10000a;
public static final int f3 = 0x10000b;
public static final int f4 = 0x10000c;
public static final int f5 = 0x10000d;
public static final int f6 = 0x10000e;
public static final int f7 = 0x10000f;
public static final int f8 = 0x100010;
public static final int f9 = 0x100011;
public static final int f10 = 0x100012;
public static final int f11 = 0x100013;
public static final int f12 = 0x100014;
public static final int Char = 0x1000000;
public static final int Mouse = 0x1000001;
public static final int Unicode = 0x1000002;

View File

@@ -0,0 +1,138 @@
/*
* 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 static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
class AppViewTests extends AbstractViewTests {
@Nested
class ItemPositions {
@Test
void simpleSetup() {
BoxView menu = spy(new BoxView());
BoxView main = spy(new BoxView());
BoxView status = spy(new BoxView());
AppView view = new AppView(main, menu, status);
view.setRect(0, 0, 80, 24);
view.draw(screen24x80);
verify(menu).setRect(0, 0, 80, 1);
verify(main).setRect(0, 1, 80, 22);
verify(status).setRect(0, 23, 80, 1);
}
@Test
void simpleSetupWithInnerBorder() {
BoxView menu = spy(new BoxView());
BoxView main = spy(new BoxView());
main.setShowBorder(true);
BoxView status = spy(new BoxView());
AppView view = new AppView(main, menu, status);
view.setRect(0, 0, 80, 24);
view.draw(screen24x80);
verify(menu).setRect(0, 0, 80, 1);
verify(main).setRect(0, 1, 80, 22);
verify(status).setRect(0, 23, 80, 1);
assertThat(forScreen(screen24x80)).hasBorder(0, 1, 80, 22);
}
}
@Nested
class Visibility {
@Test
void menuAndStatusVisible() {
BoxView menu = spy(new BoxView());
BoxView main = spy(new BoxView());
BoxView status = spy(new BoxView());
AppView view = new AppView(main, menu, status);
view.setMenuBarVisible(true);
view.setStatusBarVisible(true);
view.setRect(0, 0, 80, 24);
view.draw(screen24x80);
verify(menu).setRect(0, 0, 80, 1);
verify(main).setRect(0, 1, 80, 22);
verify(status).setRect(0, 23, 80, 1);
}
@Test
void menuAndStatusInvisible() {
BoxView menu = spy(new BoxView());
BoxView main = spy(new BoxView());
BoxView status = spy(new BoxView());
AppView view = new AppView(main, menu, status);
view.setMenuBarVisible(false);
view.setStatusBarVisible(false);
view.setRect(0, 0, 80, 24);
view.draw(screen24x80);
verify(menu, never()).setRect(anyInt(), anyInt(), anyInt(), anyInt());
verify(main).setRect(0, 0, 80, 24);
verify(status, never()).setRect(anyInt(), anyInt(), anyInt(), anyInt());
}
@Test
void onlyMenuVisible() {
BoxView menu = spy(new BoxView());
BoxView main = spy(new BoxView());
BoxView status = spy(new BoxView());
AppView view = new AppView(main, menu, status);
view.setMenuBarVisible(true);
view.setStatusBarVisible(false);
view.setRect(0, 0, 80, 24);
view.draw(screen24x80);
verify(menu).setRect(0, 0, 80, 1);
verify(main).setRect(0, 1, 80, 23);
verify(status, never()).setRect(anyInt(), anyInt(), anyInt(), anyInt());
}
@Test
void onlyStatusVisible() {
BoxView menu = spy(new BoxView());
BoxView main = spy(new BoxView());
BoxView status = spy(new BoxView());
AppView view = new AppView(main, menu, status);
view.setMenuBarVisible(false);
view.setStatusBarVisible(true);
view.setRect(0, 0, 80, 24);
view.draw(screen24x80);
verify(menu, never()).setRect(anyInt(), anyInt(), anyInt(), anyInt());
verify(main).setRect(0, 0, 80, 23);
verify(status).setRect(0, 23, 80, 1);
}
}
}

View File

@@ -512,5 +512,55 @@ class GridViewTests extends AbstractViewTests {
}
@Nested
class NestedGrids {
@Test
void simpleNestedGrid() {
BoxView box = spy(new BoxView());
GridView grid2 = spy(new GridView());
grid2.setColumnSize(0);
grid2.setRowSize(0);
grid2.addItem(box, 0, 0, 1, 1, 0, 0);
GridView grid1 = spy(new GridView());
grid1.setColumnSize(0);
grid1.setRowSize(0);
grid1.addItem(grid2, 0, 0, 1, 1, 0, 0);
grid1.setRect(0, 0, 80, 24);
grid1.draw(screen24x80);
verify(grid1).setRect(0, 0, 80, 24);
verify(grid2).setRect(0, 0, 80, 24);
verify(box).setRect(0, 0, 80, 24);
}
@Test
void simpleNestedGridWithBorders() {
BoxView box = spy(new BoxView());
box.setShowBorder(true);
GridView grid2 = spy(new GridView());
grid2.setColumnSize(0);
grid2.setRowSize(0);
grid2.setShowBorder(true);
grid2.addItem(box, 0, 0, 1, 1, 0, 0);
GridView grid1 = spy(new GridView());
grid1.setColumnSize(0);
grid1.setRowSize(0);
grid1.setShowBorder(true);
grid1.addItem(grid2, 0, 0, 1, 1, 0, 0);
grid1.setRect(0, 0, 80, 24);
grid1.draw(screen24x80);
verify(grid1).setRect(0, 0, 80, 24);
verify(grid2).setRect(1, 1, 78, 22);
verify(box).setRect(2, 2, 76, 20);
}
}
}

View File

@@ -22,6 +22,8 @@ import java.util.Map;
import java.util.TreeMap;
import org.jline.terminal.Terminal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.annotation.AnnotationUtils;
@@ -42,6 +44,7 @@ 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;
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;
@@ -66,6 +69,7 @@ public class Catalog {
= new ParameterizedTypeReference<ListViewOpenSelectedItemEvent<ScenarioData>>() {};
private final static ParameterizedTypeReference<ListViewSelectedItemChangedEvent<String>> LISTVIEW_STRING_TYPEREF
= new ParameterizedTypeReference<ListViewSelectedItemChangedEvent<String>>() {};
private final static Logger log = LoggerFactory.getLogger(Catalog.class);
// mapping from category name to scenarios(can belong to multiple categories)
private final Map<String, List<ScenarioData>> categoryMap = new TreeMap<>();
@@ -139,18 +143,25 @@ public class Catalog {
}
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.setEventLoop(eventLoop);
grid.setRowSize(0);
grid.setColumnSize(30, 0);
categories = buildCategorySelector(eventLoop);
ListView<ScenarioData> scenarios = buildScenarioSelector(eventLoop);
grid.addItem(categories, 0, 0, 1, 1, 0, 0);
grid.addItem(scenarios, 0, 1, 1, 1, 0, 0);
MenuBarView menuBar = buildMenuBar(eventLoop);
StatusBarView statusBar = buildStatusBar(eventLoop);
// we use main app view to represent scenario browser
AppView app = new AppView(grid, menuBar, statusBar);
app.setEventLoop(eventLoop);
// handle event when scenario is chosen
eventLoop.onDestroy(eventLoop.viewEvents(LISTVIEW_SCENARIO_TYPEREF, scenarios)
.subscribe(event -> {
@@ -180,14 +191,21 @@ public class Catalog {
}
));
// 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);
// we could potentially do keybinding somewhere else
// but at least this shows how to do it in low level.
// essentially we now just handle F10 to toggle
// menubar visibility
// TODO: when we get support for hotkeys we should do
// binding there
eventLoop.onDestroy(eventLoop.keyEvents()
.subscribe(event -> {
log.debug("Raw keyevent {}", event);
if (event.isKey(KeyEvent.Key.f10)) {
app.toggleStatusBarVisibility();
}
}
));
return app;
}