Base theming for views

- Control is now aware of active theme name and resolver.
- Theme some settings in BoxView, ListView and MenuView.
- Overhaul scenario system to make it work with themes.
- Add new "background" StyleSetting.
- Relates #824
This commit is contained in:
Janne Valkealahti
2023-08-04 14:19:33 +01:00
parent 55692735e6
commit ef713638b2
21 changed files with 480 additions and 55 deletions

View File

@@ -27,13 +27,14 @@ import reactor.core.Disposables;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.shell.component.view.control.cell.AbstractControl;
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.MouseBindingConsumerArgs;
import org.springframework.shell.component.view.event.MouseEvent;
import org.springframework.shell.component.view.event.MouseHandler;
import org.springframework.shell.component.view.geom.Rectangle;
@@ -46,14 +47,10 @@ import org.springframework.shell.component.view.screen.Screen;
*
* @author Janne Valkealahti
*/
public abstract class AbstractView implements View {
public abstract class AbstractView extends AbstractControl 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;
@@ -99,19 +96,6 @@ public abstract class AbstractView implements View {
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;

View File

@@ -22,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.shell.style.ThemeResolver;
import org.springframework.util.Assert;
/**
@@ -49,6 +50,22 @@ public class AppView extends BoxView {
initLayout();
}
@Override
public void setThemeName(String themeName) {
super.setThemeName(themeName);
main.setThemeName(themeName);
menu.setThemeName(themeName);
status.setThemeName(themeName);
}
@Override
public void setThemeResolver(ThemeResolver themeResolver) {
super.setThemeResolver(themeResolver);
main.setThemeResolver(themeResolver);
menu.setThemeResolver(themeResolver);
status.setThemeResolver(themeResolver);
}
private void initLayout() {
grid = new GridView();
grid.setRowSize(1, 0, 1);

View File

@@ -18,11 +18,11 @@ 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.shell.style.StyleSettings;
import org.springframework.util.StringUtils;
/**
@@ -45,7 +45,8 @@ public class BoxView extends AbstractView {
private int paddingBottom;
private int paddingLeft;
private int paddingRight;
private Integer backgroundColor = -1;
private boolean transparent = true;
private int backgroundColor = -1;
private int titleColor = -1;
private int titleStyle = -1;
private int focusedTitleColor = -1;
@@ -110,7 +111,7 @@ public class BoxView extends AbstractView {
*
* @param backgroundColor the background color
*/
public void setBackgroundColor(@Nullable Integer backgroundColor) {
public void setBackgroundColor(int backgroundColor) {
this.backgroundColor = backgroundColor;
}
@@ -161,6 +162,24 @@ public class BoxView extends AbstractView {
this.titleAlign = titleAlign;
}
/**
* Sets if box should be transparent, {@code true} by default.
*
* @param transparent a transparency flag
*/
public void setTransparent(boolean transparent) {
this.transparent = transparent;
}
/**
* Is box transparent.
*
* @return box transparency
*/
protected boolean isTransparent() {
return transparent;
}
/**
* Possibly draws a box around this view and title in a box top boundary. Also
* calls a {@code draw function} if defined.
@@ -173,12 +192,14 @@ public class BoxView extends AbstractView {
if (rect.width() <= 0 || rect.height() <= 0) {
return;
}
if (backgroundColor == null) {
screen.writerBuilder().layer(getLayer()).build().background(rect, -1);
int bgColor;
if (isTransparent()) {
bgColor = backgroundColor > -1 ? backgroundColor : -1;
}
else if (backgroundColor > -1) {
screen.writerBuilder().layer(getLayer()).build().background(rect, backgroundColor);
else {
bgColor = resolveThemeBackground(StyleSettings.TAG_BACKGROUND, backgroundColor, -1);
}
screen.writerBuilder().layer(getLayer()).build().background(rect, bgColor);
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)) {

View File

@@ -15,8 +15,10 @@
*/
package org.springframework.shell.component.view.control;
import org.springframework.lang.Nullable;
import org.springframework.shell.component.view.geom.Rectangle;
import org.springframework.shell.component.view.screen.Screen;
import org.springframework.shell.style.ThemeResolver;
/**
* Base interface for all controls. {@link Control} is able to define a
@@ -51,4 +53,19 @@ public interface Control {
* @param height a height of a bounded box
*/
void setRect(int x, int y, int width, int height);
/**
* Sets a {@link ThemeResolver}.
*
* @param themeResolver the theme resolver
*/
void setThemeResolver(@Nullable ThemeResolver themeResolver);
/**
* Sets a theme name to use.
*
* @param themeName the theme name
*/
void setThemeName(@Nullable String themeName);
}

View File

@@ -26,6 +26,7 @@ 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;
import org.springframework.shell.style.StyleSettings;
/**
* {@link ListView} shows {@code list items} vertically.
@@ -51,12 +52,13 @@ public class ListView<T> extends BoxView {
Rectangle rect = getInnerRect();
int y = rect.y();
int selectedStyle = resolveThemeStyle(StyleSettings.TAG_HIGHLIGHT, ScreenItem.STYLE_BOLD);
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);
c.setStyle(selectedStyle);
}
else {
c.updateSelected(false);

View File

@@ -35,6 +35,7 @@ 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.style.ThemeResolver;
import org.springframework.shell.component.view.screen.ScreenItem;
/**
@@ -201,6 +202,18 @@ public class MenuBarView extends BoxView {
checkMenuView();
}
@Override
public void setThemeName(String themeName) {
super.setThemeName(themeName);
menuViews.values().forEach(view -> view.setThemeName(themeName));
}
@Override
public void setThemeResolver(ThemeResolver themeResolver) {
super.setThemeResolver(themeResolver);
menuViews.values().forEach(view -> view.setThemeResolver(themeResolver));
}
private void checkMenuView() {
if (activeItemIndex < 0) {
closeCurrentMenuView();
@@ -218,8 +231,10 @@ public class MenuBarView extends BoxView {
private MenuView buildMenuView(MenuBarItem item) {
MenuView menuView = new MenuView(item.getItems());
menuView.setEventLoop(getEventLoop());
menuView.setThemeResolver(getThemeResolver());
menuView.setThemeName(getThemeName());
menuView.setShowBorder(true);
menuView.setBackgroundColor(null);
menuView.setTransparent(false);
menuView.setLayer(1);
Rectangle rect = getInnerRect();
int x = positionAtIndex(activeItemIndex);

View File

@@ -33,6 +33,7 @@ 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.style.StyleSettings;
import org.springframework.shell.component.view.screen.ScreenItem;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -143,7 +144,8 @@ public class MenuView extends BoxView {
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 themeStyle = resolveThemeStyle(StyleSettings.TAG_HIGHLIGHT, ScreenItem.STYLE_BOLD);
Writer writer2 = screen.writerBuilder().layer(getLayer()).style(themeStyle).build();
int i = 0;
boolean hasCheck = false;
for (MenuItem item : items) {

View File

@@ -15,8 +15,15 @@
*/
package org.springframework.shell.component.view.control.cell;
import java.util.Optional;
import org.jline.utils.AttributedStyle;
import org.springframework.lang.Nullable;
import org.springframework.shell.component.view.control.Control;
import org.springframework.shell.component.view.geom.Rectangle;
import org.springframework.shell.style.ThemeResolver;
import org.springframework.shell.style.ThemeResolver.ResolvedValues;
/**
* Base implementation of a {@link Control}.
@@ -29,6 +36,8 @@ public abstract class AbstractControl implements Control {
private int y = 0;
private int width = 0;
private int height = 0;
private ThemeResolver themeResolver;
private String themeName;
@Override
public void setRect(int x, int y, int width, int height) {
@@ -43,4 +52,98 @@ public abstract class AbstractControl implements Control {
return new Rectangle(x, y, width, height);
}
/**
* Sets a {@link ThemeResolver}.
*
* @param themeResolver the theme resolver
*/
public void setThemeResolver(@Nullable ThemeResolver themeResolver) {
this.themeResolver = themeResolver;
}
/**
* Gets a {@link ThemeResolver}.
*
* @return a theme resolver
*/
@Nullable
protected ThemeResolver getThemeResolver() {
return themeResolver;
}
/**
* Sets a theme name to use.
*
* @param themeName the theme name
*/
public void setThemeName(@Nullable String themeName) {
this.themeName = themeName;
}
/**
* Gets a theme name.
*
* @return a theme name
*/
@Nullable
protected String getThemeName() {
return themeName;
}
private Optional<ResolvedValues> getThemeResolvedValues(String tag) {
ThemeResolver themeResolver = getThemeResolver();
if (themeResolver != null) {
String styleTag = themeResolver.resolveStyleTag(tag, getThemeName());
AttributedStyle attributedStyle = themeResolver.resolveStyle(styleTag);
return Optional.of(themeResolver.resolveValues(attributedStyle));
}
return Optional.empty();
}
/**
* Resolve style using existing {@link ThemeResolver} and {@code theme name}.
* Use {@code defaultStyle} if resolving cannot happen.
*
* @param tag the style tag to use
* @param defaultStyle the default style to use
* @return resolved style
*/
protected int resolveThemeStyle(String tag, int defaultStyle) {
return getThemeResolvedValues(tag).map(ResolvedValues::style).orElse(defaultStyle);
}
/**
* Resolve foreground color using existing {@link ThemeResolver} and {@code theme name}.
* {@code defaultColor} is used if it's value is not negative. {@code fallbackColor} is
* used if theme resolver cannot be used.
*
* @param tag the style tag to use
* @param defaultColor the default foreground color to use
* @param fallbackColor the fallback foreground color to use
* @return resolved foreground color
*/
protected int resolveThemeForeground(String tag, int defaultColor, int fallbackColor) {
if (defaultColor > -1) {
return defaultColor;
}
return getThemeResolvedValues(tag).map(ResolvedValues::foreground).orElse(fallbackColor);
}
/**
* Resolve background color using existing {@link ThemeResolver} and {@code theme name}.
* {@code defaultColor} is used if it's value is not negative. {@code fallbackColor} is
* used if theme resolver cannot be used.
*
* @param tag the style tag to use
* @param defaultColor the default background color to use
* @param fallbackColor the fallback background color to use
* @return resolved background color
*/
protected int resolveThemeBackground(String tag, int defaultColor, int fallbackColor) {
if (defaultColor > -1) {
return defaultColor;
}
return getThemeResolvedValues(tag).map(ResolvedValues::background).orElse(fallbackColor);
}
}

View File

@@ -257,10 +257,15 @@ public class DefaultScreen implements Screen, DisplayLines {
// if (items[y] == null) {
// items[y] = new DefaultScreenItem[columns];
// }
if (items[y][x] == null) {
items[y][x] = new DefaultScreenItem();
if (y < rows && x < columns) {
if (items[y][x] == null) {
items[y][x] = new DefaultScreenItem();
}
return items[y][x];
}
else {
return null;
}
return items[y][x];
}
}
@@ -343,7 +348,9 @@ public class DefaultScreen implements Screen, DisplayLines {
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;
if (item != null) {
item.background = color;
}
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022 the original author or authors.
* Copyright 2022-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.
@@ -91,6 +91,11 @@ public abstract class StyleSettings {
*/
public final static String TAG_HIGHLIGHT = "style-highlight";
/**
* Styling for something which has a background.
*/
public final static String TAG_BACKGROUND = "style-background";
public String title() {
return "bold";
}
@@ -143,6 +148,10 @@ public abstract class StyleSettings {
return "bold";
}
public String background() {
return "default";
}
/**
* Resolve a theme setting from a given tag.
*
@@ -177,6 +186,8 @@ public abstract class StyleSettings {
return itemSelector();
case TAG_HIGHLIGHT:
return highlight();
case TAG_BACKGROUND:
return background();
}
throw new IllegalArgumentException(String.format("Unknown tag '%s'", tag));
}
@@ -213,7 +224,8 @@ public abstract class StyleSettings {
TAG_ITEM_SELECTED,
TAG_ITEM_UNSELECTED,
TAG_ITEM_SELECTOR,
TAG_HIGHLIGHT
TAG_HIGHLIGHT,
TAG_BACKGROUND
};
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022 the original author or authors.
* Copyright 2022-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.
@@ -17,6 +17,7 @@ package org.springframework.shell.style;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.springframework.util.Assert;
@@ -48,4 +49,13 @@ public class ThemeRegistry {
Assert.notNull(theme, "theme cannot be null");
themes.put(theme.getName(), theme);
}
/**
* Gets all theme names registered.
*
* @return theme names
*/
public Set<String> getThemeNames() {
return themes.keySet();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022 the original author or authors.
* Copyright 2022-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.
@@ -15,12 +15,17 @@
*/
package org.springframework.shell.style;
import java.util.Set;
import org.jline.style.MemoryStyleSource;
import org.jline.style.StyleExpression;
import org.jline.style.StyleResolver;
import org.jline.style.StyleSource;
import org.jline.utils.AttributedString;
import org.jline.utils.AttributedStyle;
import org.jline.utils.Colors;
import org.springframework.util.StringUtils;
/**
* Service which helps to do various things with styles.
@@ -32,12 +37,67 @@ public class ThemeResolver {
private StyleSource styleSource = new MemoryStyleSource();
private StyleResolver styleResolver = new StyleResolver(styleSource, "default");
private StyleExpression styleExpression = new StyleExpression(styleResolver);
private ThemeRegistry themeRegistry;
private final Theme theme;
public ThemeResolver(ThemeRegistry themeRegistry, String themeName) {
this.themeRegistry = themeRegistry;
this.theme = themeRegistry.get(themeName);
}
// copied from jline as no public access
private static final long F_FOREGROUND_IND = 0x00000100;
private static final long F_FOREGROUND_RGB = 0x00000200;
private static final long F_FOREGROUND = F_FOREGROUND_IND | F_FOREGROUND_RGB;
private static final long F_BACKGROUND_IND = 0x00000400;
private static final long F_BACKGROUND_RGB = 0x00000800;
private static final long F_BACKGROUND = F_BACKGROUND_IND | F_BACKGROUND_RGB;
private static final int FG_COLOR_EXP = 15;
private static final int BG_COLOR_EXP = 39;
private static final long FG_COLOR = 0xFFFFFFL << FG_COLOR_EXP;
private static final long BG_COLOR = 0xFFFFFFL << BG_COLOR_EXP;
/**
* Stores resolved values for {@link ThemeResolver#resolveValues(AttributedStyle)}.
*/
public record ResolvedValues(int style, int foreground, int background){}
/**
* Resolve {@code style}, {@code foreground color} and {@code background color}
* from an {@link AttributedStyle}.
*
* @param attributedStyle the attibuted style
* @return resolved values
*/
public ResolvedValues resolveValues(AttributedStyle attributedStyle) {
long style = attributedStyle.getStyle();
long s = style & ~(F_FOREGROUND | F_BACKGROUND);
s = (s & 0x00007FFF);
int fg = (int) ((style & FG_COLOR) >> FG_COLOR_EXP);
int bg = (int) ((style & BG_COLOR) >> BG_COLOR_EXP);
// if jline "ind" bit is set, resort to using
// same logic as jline.
boolean hasFb = (style & F_FOREGROUND_IND) != 0 || (style & F_FOREGROUND_RGB) != 0;
if ((style & F_FOREGROUND_IND) != 0) {
fg = Colors.DEFAULT_COLORS_256[fg & 0xFF];
}
boolean hasBg = (style & F_BACKGROUND_IND) != 0 || (style & F_BACKGROUND_RGB) != 0;
if ((style & F_BACKGROUND_IND) != 0) {
bg = Colors.DEFAULT_COLORS_256[bg & 0xFF];
}
return new ResolvedValues((int)s, hasFb ? fg : -1, hasBg ? bg : -1);
}
/**
* Return all known theme names.
*
* @return all theme names
*/
public Set<String> themeNames() {
return themeRegistry.getThemeNames();
}
/**
* Evaluate expression.
*
@@ -48,6 +108,7 @@ public class ThemeResolver {
return styleExpression.evaluate(expression);
}
/**
* Resolve style from a tag with activated theme.
*
@@ -58,6 +119,18 @@ public class ThemeResolver {
return theme.getSettings().styles().resolveTag(tag);
}
/**
* Resolve style from a tag with given theme.
*
* @param tag the tag
* @param themeName the theme name
* @return a style
*/
public String resolveStyleTag(String tag, String themeName) {
Theme t = StringUtils.hasText(themeName) ? themeRegistry.get(themeName) : theme;
return t.getSettings().styles().resolveTag(tag);
}
/**
* Resolve figure from a tag with activated theme.
*

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022 the original author or authors.
* Copyright 2022-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.
@@ -17,14 +17,21 @@ package org.springframework.shell.style;
import org.jline.utils.AttributedString;
import org.jline.utils.AttributedStyle;
import org.jline.utils.Colors;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.shell.component.view.screen.Color;
import org.springframework.shell.style.ThemeResolver.ResolvedValues;
import static org.assertj.core.api.Assertions.assertThat;
public class ThemeResolverTests {
@Test
public void test() {
private ThemeResolver themeResolver;
@BeforeEach
public void setup() {
ThemeRegistry themeRegistry = new ThemeRegistry();
themeRegistry.register(new Theme() {
@Override
@@ -37,10 +44,55 @@ public class ThemeResolverTests {
return ThemeSettings.defaults();
}
});
ThemeResolver themeResolver = new ThemeResolver(themeRegistry, "default");
themeResolver = new ThemeResolver(themeRegistry, "default");
}
@Test
public void styleExpression() {
assertThat(themeResolver.resolveStyleTag(StyleSettings.TAG_TITLE)).isEqualTo("bold");
assertThat(themeResolver.resolveStyle("bold")).isEqualTo(AttributedStyle.BOLD);
assertThat(themeResolver.evaluateExpression("@{bold foo}"))
.isEqualTo(new AttributedString("foo", AttributedStyle.BOLD));
}
@Test
public void resolveValuesIndStyle() {
AttributedStyle s = AttributedStyle.DEFAULT.background(AttributedStyle.BLUE).foreground(AttributedStyle.RED);
ResolvedValues resolvedValues = themeResolver.resolveValues(s);
assertThat(resolvedValues.background()).isEqualTo(Colors.DEFAULT_COLORS_256[AttributedStyle.BLUE]);
assertThat(resolvedValues.foreground()).isEqualTo(Colors.DEFAULT_COLORS_256[AttributedStyle.RED]);
}
@Test
public void resolveValuesRgbStyle() {
AttributedStyle s = AttributedStyle.DEFAULT.backgroundRgb(Color.BLUE).foregroundRgb(Color.RED);
ResolvedValues resolvedValues = themeResolver.resolveValues(s);
assertThat(resolvedValues.background()).isEqualTo(Color.BLUE);
assertThat(resolvedValues.foreground()).isEqualTo(Color.RED);
}
@Test
public void resolveValuesRgbExp() {
AttributedStyle s = themeResolver.resolveStyle("bg-rgb:#0000FF,fg-rgb:#FF0000");
ResolvedValues resolvedValues = themeResolver.resolveValues(s);
assertThat(resolvedValues.background()).isEqualTo(Color.BLUE);
assertThat(resolvedValues.foreground()).isEqualTo(Color.RED);
}
@Test
public void resolveValuesIndExp() {
AttributedStyle s = themeResolver.resolveStyle("bg:blue,fg:red");
ResolvedValues resolvedValues = themeResolver.resolveValues(s);
assertThat(resolvedValues.background()).isEqualTo(Colors.DEFAULT_COLORS_256[AttributedStyle.BLUE]);
assertThat(resolvedValues.foreground()).isEqualTo(Colors.DEFAULT_COLORS_256[AttributedStyle.RED]);
}
@Test
public void resolveValuesNotDefinedExp() {
AttributedStyle s = themeResolver.resolveStyle("");
ResolvedValues resolvedValues = themeResolver.resolveValues(s);
assertThat(resolvedValues.background()).isEqualTo(-1);
assertThat(resolvedValues.foreground()).isEqualTo(-1);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022 the original author or authors.
* Copyright 2022-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.
@@ -53,10 +53,11 @@ public class ThemeSettingsTests {
StyleSettings.TAG_ITEM_SELECTED,
StyleSettings.TAG_ITEM_UNSELECTED,
StyleSettings.TAG_ITEM_SELECTOR,
StyleSettings.TAG_HIGHLIGHT })
StyleSettings.TAG_HIGHLIGHT,
StyleSettings.TAG_BACKGROUND })
public void testTags(String tag) {
ThemeSettings themeSettings = ThemeSettings.defaults();
String resolveTag = themeSettings.styles().resolveTag(StyleSettings.TAG_TITLE);
String resolveTag = themeSettings.styles().resolveTag(tag);
assertThat(resolveTag).isNotNull();
}
}

View File

@@ -53,6 +53,7 @@ import org.springframework.shell.component.view.screen.Screen.Writer;
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;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@@ -77,10 +78,15 @@ public class Catalog {
private View currentScenarioView = null;
private TerminalUI ui;
private ListView<String> categories;
private ListView<ScenarioData> scenarios;
private AppView app;
private EventLoop eventLoop;
private ThemeResolver themeResolver;
private String activeThemeName = "default";
public Catalog(Terminal terminal, List<Scenario> scenarios) {
public Catalog(Terminal terminal, List<Scenario> scenarios, ThemeResolver themeResolver) {
this.terminal = terminal;
this.themeResolver = themeResolver;
mapScenarios(scenarios);
}
@@ -117,7 +123,9 @@ public class Catalog {
public void run() {
ui = new TerminalUI(terminal);
eventLoop = ui.getEventLoop();
AppView app = buildScenarioBrowser(eventLoop, ui);
app = buildScenarioBrowser(eventLoop, ui);
app.setThemeResolver(themeResolver);
app.setThemeName(activeThemeName);
// handle logic to switch between main scenario browser
// and currently active scenario
@@ -146,12 +154,14 @@ 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);
grid.setRowSize(0);
grid.setColumnSize(30, 0);
categories = buildCategorySelector(eventLoop);
ListView<ScenarioData> scenarios = buildScenarioSelector(eventLoop);
scenarios = buildScenarioSelector(eventLoop);
grid.addItem(categories, 0, 0, 1, 1, 0, 0);
grid.addItem(scenarios, 0, 1, 1, 1, 0, 0);
@@ -161,12 +171,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);
// 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();
View view = event.args().item().scenario()
.configure(eventLoop, themeResolver, activeThemeName)
.build();
component.setRoot(view, true);
currentScenarioView = view;
}));
@@ -212,6 +226,8 @@ public class Catalog {
private ListView<String> buildCategorySelector(EventLoop eventLoop) {
ListView<String> categories = new ListView<>();
categories.setThemeResolver(themeResolver);
categories.setThemeName(activeThemeName);
categories.setEventLoop(eventLoop);
List<String> items = List.copyOf(categoryMap.keySet());
categories.setItems(items);
@@ -234,6 +250,8 @@ public class Catalog {
private ListView<ScenarioData> buildScenarioSelector(EventLoop eventLoop) {
ListView<ScenarioData> scenarios = new ListView<>();
scenarios.setThemeResolver(themeResolver);
scenarios.setThemeName(activeThemeName);
scenarios.setEventLoop(eventLoop);
scenarios.setTitle("Scenarios");
scenarios.setFocusedTitleStyle(ScreenItem.STYLE_BOLD);
@@ -242,19 +260,36 @@ public class Catalog {
return scenarios;
}
private void setStyle(String name) {
log.debug("Setting active theme name {}", name);
activeThemeName = name;
scenarios.setThemeName(activeThemeName);
categories.setThemeName(activeThemeName);
app.setThemeName(activeThemeName);
}
private Runnable styleAction(String style) {
return () -> setStyle(style);
}
private MenuBarView buildMenuBar(EventLoop eventLoop) {
Runnable quitAction = () -> requestQuit();
MenuItem[] themeItems = themeResolver.themeNames().stream()
.map(tn -> {
return MenuItem.of(tn, MenuItemCheckStyle.RADIO, styleAction(tn), "default".equals(tn));
})
.toArray(MenuItem[]::new);
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)
),
themeItems),
MenuBarItem.of("Help",
MenuItem.of("About"))
);
menuBar.setThemeResolver(themeResolver);
menuBar.setThemeName(activeThemeName);
menuBar.setEventLoop(eventLoop);
return menuBar;
}
@@ -262,6 +297,8 @@ public class Catalog {
private StatusBarView buildStatusBar(EventLoop eventLoop) {
Runnable quitAction = () -> requestQuit();
StatusBarView statusBar = new StatusBarView();
statusBar.setThemeResolver(themeResolver);
statusBar.setThemeName(activeThemeName);
statusBar.setEventLoop(eventLoop);
StatusItem item1 = new StatusBarView.StatusItem("CTRL-Q Quit", quitAction);
StatusItem item2 = new StatusBarView.StatusItem("F10 Status Bar");

View File

@@ -20,6 +20,7 @@ import java.util.List;
import org.springframework.shell.command.annotation.Command;
import org.springframework.shell.samples.catalog.scenario.Scenario;
import org.springframework.shell.standard.AbstractShellComponent;
import org.springframework.shell.style.ThemeResolver;
/**
* Main command access point to view showcase catalog.
@@ -30,14 +31,16 @@ import org.springframework.shell.standard.AbstractShellComponent;
public class CatalogCommand extends AbstractShellComponent {
private final List<Scenario> scenarios;
private final ThemeResolver themeResolver;
public CatalogCommand(List<Scenario> scenarios) {
public CatalogCommand(List<Scenario> scenarios, ThemeResolver themeResolver) {
this.scenarios = scenarios;
this.themeResolver = themeResolver;
}
@Command(command = "catalog")
public void catalog() {
Catalog catalog = new Catalog(getTerminal(), scenarios);
Catalog catalog = new Catalog(getTerminal(), scenarios, themeResolver);
catalog.run();
}
}

View File

@@ -15,10 +15,50 @@
*/
package org.springframework.shell.samples.catalog;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.shell.command.annotation.CommandScan;
import org.springframework.shell.style.FigureSettings;
import org.springframework.shell.style.StyleSettings;
import org.springframework.shell.style.Theme;
import org.springframework.shell.style.ThemeSettings;
@Configuration
@CommandScan
class CatalogConfiguration {
@Bean
public Theme customTheme() {
return new Theme() {
@Override
public String getName() {
return "custom";
}
@Override
public ThemeSettings getSettings() {
return new CustomThemeSettings();
}
};
}
static class CustomThemeSettings extends ThemeSettings {
CustomThemeSettings() {
super(new CustomStyleSettings(), FigureSettings.defaults());
}
}
static class CustomStyleSettings extends StyleSettings {
@Override
public String highlight() {
return "bold,italic,fg:bright-cyan";
}
@Override
public String background() {
return "bg:blue";
}
}
}

View File

@@ -15,7 +15,9 @@
*/
package org.springframework.shell.samples.catalog.scenario;
import org.springframework.shell.component.view.control.AbstractView;
import org.springframework.shell.component.view.event.EventLoop;
import org.springframework.shell.style.ThemeResolver;
/**
* Base implementation of a {@link Scenario} helping to avoid some bloatware.
@@ -25,10 +27,14 @@ import org.springframework.shell.component.view.event.EventLoop;
public abstract class AbstractScenario implements Scenario {
private EventLoop eventloop;
private ThemeResolver themeResolver;
private String themeName;
@Override
public Scenario configure(EventLoop eventloop) {
public Scenario configure(EventLoop eventloop, ThemeResolver themeResolver, String themeName) {
this.eventloop = eventloop;
this.themeResolver = themeResolver;
this.themeName = themeName;
return this;
}
@@ -36,4 +42,17 @@ public abstract class AbstractScenario implements Scenario {
return eventloop;
}
protected ThemeResolver getThemeResolver() {
return themeResolver;
}
protected String getThemeName() {
return themeName;
}
protected void configure(AbstractView view) {
view.setEventLoop(getEventloop());
view.setThemeResolver(getThemeResolver());
view.setThemeName(getThemeName());
}
}

View File

@@ -17,6 +17,7 @@ package org.springframework.shell.samples.catalog.scenario;
import org.springframework.shell.component.view.control.View;
import org.springframework.shell.component.view.event.EventLoop;
import org.springframework.shell.style.ThemeResolver;
/**
* {@link Scenario} participates in a catalog showcase.
@@ -43,8 +44,10 @@ public interface Scenario {
* Configure scenario.
*
* @param eventloop eventloop for scenario
* @param themeResolver theme resolver for scenario
* @param themeName theme name for scenario
* @return scenario for chaining
*/
Scenario configure(EventLoop eventloop);
Scenario configure(EventLoop eventloop, ThemeResolver themeResolver, String themeName);
}

View File

@@ -31,6 +31,7 @@ public class SimpleBoxViewScenario extends AbstractScenario {
@Override
public View build() {
BoxView box = new BoxView();
configure(box);
box.setTitle("Title");
box.setShowBorder(true);
box.setBackgroundColor(Color.KHAKI4);

View File

@@ -29,31 +29,37 @@ public class SimpleGridViewScenario extends AbstractScenario {
@Override
public View build() {
BoxView menu = new BoxView();
configure(menu);
menu.setBackgroundColor(Color.KHAKI4);
menu.setTitle("Menu");
menu.setShowBorder(true);
BoxView main = new BoxView();
configure(main);
main.setBackgroundColor(Color.KHAKI4);
main.setTitle("Main");
main.setShowBorder(true);
BoxView sideBar = new BoxView();
configure(sideBar);
sideBar.setBackgroundColor(Color.KHAKI4);
sideBar.setTitle("Sidebar");
sideBar.setShowBorder(true);
BoxView header = new BoxView();
configure(header);
header.setBackgroundColor(Color.KHAKI4);
header.setTitle("Header");
header.setShowBorder(true);
BoxView footer = new BoxView();
configure(footer);
footer.setBackgroundColor(Color.KHAKI4);
footer.setTitle("Footer");
footer.setShowBorder(true);
GridView grid = new GridView();
configure(grid);
grid.setBackgroundColor(Color.KHAKI3);
grid.setTitle("Grid");
grid.setShowBorder(true);