Autoconfigure TerminalUI

- Add TerminalUIBuilder which can be used to build TerminalUI
- Add TerminalUICustomizer which can customize TerminalUI
- What is autoconfigured is TerminalUIBuilder.
- In TerminalUI add configure for views which now allows
  easier way to set needed stuff in views.
- Various changes in a catalog app
- Fixes #900
This commit is contained in:
Janne Valkealahti
2023-10-25 09:10:42 +01:00
parent f3b8bf3de1
commit 82df453cc0
18 changed files with 468 additions and 70 deletions

View File

@@ -21,7 +21,6 @@ import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.jline.terminal.Terminal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -29,6 +28,7 @@ import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.shell.component.message.ShellMessageBuilder;
import org.springframework.shell.component.view.TerminalUI;
import org.springframework.shell.component.view.TerminalUIBuilder;
import org.springframework.shell.component.view.control.AppView;
import org.springframework.shell.component.view.control.AppView.AppViewEvent;
import org.springframework.shell.component.view.control.BoxView;
@@ -52,10 +52,10 @@ import org.springframework.shell.component.view.event.KeyEvent.Key;
import org.springframework.shell.component.view.event.KeyEvent.KeyMask;
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.shell.geom.HorizontalAlign;
import org.springframework.shell.geom.Rectangle;
import org.springframework.shell.geom.VerticalAlign;
import org.springframework.shell.component.view.screen.ScreenItem;
import org.springframework.shell.samples.catalog.scenario.Scenario;
import org.springframework.shell.samples.catalog.scenario.ScenarioComponent;
import org.springframework.shell.style.ThemeResolver;
@@ -79,7 +79,6 @@ public class Catalog {
// 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;
@@ -88,9 +87,10 @@ public class Catalog {
private EventLoop eventLoop;
private ThemeResolver themeResolver;
private String activeThemeName = "default";
private TerminalUIBuilder terminalUIBuilder;
public Catalog(Terminal terminal, List<Scenario> scenarios, ThemeResolver themeResolver) {
this.terminal = terminal;
public Catalog(TerminalUIBuilder terminalUIBuilder, ThemeResolver themeResolver, List<Scenario> scenarios) {
this.terminalUIBuilder = terminalUIBuilder;
this.themeResolver = themeResolver;
mapScenarios(scenarios);
}
@@ -122,11 +122,9 @@ public class Catalog {
* Main run loop. Builds the ui and exits when user requests exit.
*/
public void run() {
ui = new TerminalUI(terminal);
ui = terminalUIBuilder.build();
eventLoop = ui.getEventLoop();
app = buildScenarioBrowser(eventLoop, ui);
app.setThemeResolver(themeResolver);
app.setThemeName(activeThemeName);
// handle logic to switch between main scenario browser
// and currently active scenario
@@ -155,14 +153,12 @@ public class Catalog {
private AppView buildScenarioBrowser(EventLoop eventLoop, TerminalUI component) {
// category selector on left, scenario selector on right
GridView grid = new GridView();
grid.setThemeResolver(themeResolver);
grid.setThemeName(activeThemeName);
grid.setEventLoop(eventLoop);
component.configure(grid);
grid.setRowSize(0);
grid.setColumnSize(30, 0);
categories = buildCategorySelector(eventLoop);
scenarios = buildScenarioSelector(eventLoop);
categories = buildCategorySelector();
scenarios = buildScenarioSelector();
grid.addItem(categories, 0, 0, 1, 1, 0, 0);
grid.addItem(scenarios, 0, 1, 1, 1, 0, 0);
@@ -172,16 +168,16 @@ public class Catalog {
// we use main app view to represent scenario browser
AppView app = new AppView(grid, menuBar, statusBar);
app.setThemeResolver(themeResolver);
app.setThemeName(activeThemeName);
app.setEventLoop(eventLoop);
component.configure(app);
// handle event when scenario is chosen
eventLoop.onDestroy(eventLoop.viewEvents(LISTVIEW_SCENARIO_TYPEREF, scenarios)
.subscribe(event -> {
View view = event.args().item().scenario()
.configure(ui, eventLoop, themeResolver, activeThemeName)
.build();
View view = event.args().item().scenario().configure(ui).build();
ui.configure(view);
// View view = event.args().item().scenario()
// .configure(ui, eventLoop, themeResolver, activeThemeName)
// .build();
component.setRoot(view, true);
currentScenarioView = view;
}));
@@ -225,11 +221,10 @@ public class Catalog {
return app;
}
private ListView<String> buildCategorySelector(EventLoop eventLoop) {
private ListView<String> buildCategorySelector() {
ListView<String> categories = new ListView<>();
categories.setThemeResolver(themeResolver);
categories.setThemeName(activeThemeName);
categories.setEventLoop(eventLoop);
ui.configure(categories);
List<String> items = List.copyOf(categoryMap.keySet());
categories.setItems(items);
categories.setTitle("Categories");
@@ -261,11 +256,9 @@ public class Catalog {
}
}
private ListView<ScenarioData> buildScenarioSelector(EventLoop eventLoop) {
private ListView<ScenarioData> buildScenarioSelector() {
ListView<ScenarioData> scenarios = new ListView<>();
scenarios.setThemeResolver(themeResolver);
scenarios.setThemeName(activeThemeName);
scenarios.setEventLoop(eventLoop);
ui.configure(scenarios);
scenarios.setTitle("Scenarios");
scenarios.setFocusedTitleStyle(ScreenItem.STYLE_BOLD);
scenarios.setShowBorder(true);
@@ -287,9 +280,7 @@ public class Catalog {
private DialogView buildAboutDialog() {
ButtonView button = new ButtonView("OK");
button.setThemeResolver(themeResolver);
button.setThemeName(activeThemeName);
button.setEventLoop(eventLoop);
ui.configure(button);
BoxView content = new BoxView();
content.setDrawFunction((screen, rect) -> {
@@ -297,11 +288,7 @@ public class Catalog {
return rect;
});
DialogView dialog = new DialogView(content, button);
dialog.setThemeResolver(themeResolver);
dialog.setThemeName(activeThemeName);
dialog.setEventLoop(eventLoop);
dialog.setViewService(ui);
ui.configure(dialog);
return dialog;
}
@@ -329,18 +316,14 @@ public class Catalog {
MenuItem.of("About", MenuItemCheckStyle.NOCHECK, aboutAction))
);
menuBar.setThemeResolver(themeResolver);
menuBar.setThemeName(activeThemeName);
menuBar.setEventLoop(eventLoop);
ui.configure(menuBar);
return menuBar;
}
private StatusBarView buildStatusBar(EventLoop eventLoop) {
Runnable quitAction = () -> requestQuit();
StatusBarView statusBar = new StatusBarView();
statusBar.setThemeResolver(themeResolver);
statusBar.setThemeName(activeThemeName);
statusBar.setEventLoop(eventLoop);
ui.configure(statusBar);
StatusItem item1 = new StatusBarView.StatusItem("CTRL-Q Quit", quitAction);
StatusItem item2 = new StatusBarView.StatusItem("F10 Status Bar");
statusBar.setItems(Arrays.asList(item1, item2));

View File

@@ -18,6 +18,7 @@ package org.springframework.shell.samples.catalog;
import java.util.List;
import org.springframework.shell.command.annotation.Command;
import org.springframework.shell.component.view.TerminalUIBuilder;
import org.springframework.shell.samples.catalog.scenario.Scenario;
import org.springframework.shell.standard.AbstractShellComponent;
import org.springframework.shell.style.ThemeResolver;
@@ -31,16 +32,18 @@ import org.springframework.shell.style.ThemeResolver;
public class CatalogCommand extends AbstractShellComponent {
private final List<Scenario> scenarios;
private final TerminalUIBuilder terminalUIBuilder;
private final ThemeResolver themeResolver;
public CatalogCommand(List<Scenario> scenarios, ThemeResolver themeResolver) {
public CatalogCommand(List<Scenario> scenarios, TerminalUIBuilder terminalUIBuilder, ThemeResolver themeResolver) {
this.scenarios = scenarios;
this.terminalUIBuilder = terminalUIBuilder;
this.themeResolver = themeResolver;
}
@Command(command = "catalog")
public void catalog() {
Catalog catalog = new Catalog(getTerminal(), scenarios, themeResolver);
Catalog catalog = new Catalog(terminalUIBuilder, themeResolver, scenarios);
catalog.run();
}
}

View File

@@ -15,7 +15,8 @@
*/
package org.springframework.shell.samples.catalog.scenario;
import org.springframework.shell.component.view.control.AbstractView;
import org.springframework.shell.component.view.TerminalUI;
import org.springframework.shell.component.view.control.View;
import org.springframework.shell.component.view.control.ViewService;
import org.springframework.shell.component.view.event.EventLoop;
import org.springframework.shell.style.ThemeResolver;
@@ -27,20 +28,12 @@ import org.springframework.shell.style.ThemeResolver;
*/
public abstract class AbstractScenario implements Scenario {
private TerminalUI ui;
private ViewService viewService;
private EventLoop eventloop;
private ThemeResolver themeResolver;
private String themeName;
@Override
public Scenario configure(ViewService viewService, EventLoop eventloop, ThemeResolver themeResolver, String themeName) {
this.viewService = viewService;
this.eventloop = eventloop;
this.themeResolver = themeResolver;
this.themeName = themeName;
return this;
}
protected ViewService getViewService() {
return viewService;
}
@@ -57,9 +50,23 @@ public abstract class AbstractScenario implements Scenario {
return themeName;
}
protected void configure(AbstractView view) {
view.setEventLoop(getEventloop());
view.setThemeResolver(getThemeResolver());
view.setThemeName(getThemeName());
protected TerminalUI getTerminalUI() {
return ui;
}
@Override
public Scenario configure(TerminalUI ui) {
this.ui = ui;
this.themeName = ui.getThemeName();
this.themeResolver = ui.getThemeResolver();
this.eventloop = ui.getEventLoop();
this.viewService = ui.getViewService();
return this;
}
protected void configure(View view) {
if (ui != null) {
ui.configure(view);
}
}
}

View File

@@ -15,10 +15,8 @@
*/
package org.springframework.shell.samples.catalog.scenario;
import org.springframework.shell.component.view.TerminalUI;
import org.springframework.shell.component.view.control.View;
import org.springframework.shell.component.view.control.ViewService;
import org.springframework.shell.component.view.event.EventLoop;
import org.springframework.shell.style.ThemeResolver;
/**
* {@link Scenario} participates in a catalog showcase.
@@ -44,11 +42,9 @@ public interface Scenario {
/**
* Configure scenario.
*
* @param eventloop eventloop for scenario
* @param themeResolver theme resolver for scenario
* @param themeName theme name for scenario
* @param ui the terminal ui
* @return scenario for chaining
*/
Scenario configure(ViewService viewService, EventLoop eventloop, ThemeResolver themeResolver, String themeName);
Scenario configure(TerminalUI ui);
}

View File

@@ -30,7 +30,7 @@ public class CheckedListViewScenario extends AbstractScenario {
@Override
public View build() {
ListView<String> view = new ListView<>(ItemStyle.RADIO);
view.setEventLoop(getEventloop());
configure(view);
view.setItems(Arrays.asList("item1", "item2", "item3"));
return view;
}

View File

@@ -31,7 +31,7 @@ public class LongListViewScenario extends AbstractScenario {
@Override
public View build() {
ListView<String> view = new ListView<>();
view.setEventLoop(getEventloop());
configure(view);
List<String> items = IntStream.of(20).mapToObj(i -> "item" + i).collect(Collectors.toList());
view.setItems(items);
return view;

View File

@@ -30,7 +30,7 @@ public class RadioListViewScenario extends AbstractScenario {
@Override
public View build() {
ListView<String> view = new ListView<>(ItemStyle.CHECKED);
view.setEventLoop(getEventloop());
configure(view);
view.setItems(Arrays.asList("item1", "item2", "item3"));
return view;
}

View File

@@ -29,7 +29,7 @@ public class SimpleListViewScenario extends AbstractScenario {
@Override
public View build() {
ListView<String> view = new ListView<>();
view.setEventLoop(getEventloop());
configure(view);
view.setItems(Arrays.asList("item1", "item2"));
return view;
}

View File

@@ -51,7 +51,6 @@ public class DialogScenario extends AbstractScenario {
});
DialogView dialog = new DialogView(content, button);
configure(dialog);
dialog.setViewService(getViewService());
return dialog;
}

View File

@@ -27,7 +27,7 @@ public class SimpleInputViewScenario extends AbstractScenario {
@Override
public View build() {
InputView view = new InputView();
view.setEventLoop(getEventloop());
configure(view);
view.setTitle("Input");
view.setShowBorder(true);
return view;