From ef713638b219fa50cac104be181e04d0cc9e1186 Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Fri, 4 Aug 2023 14:19:33 +0100 Subject: [PATCH] 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 --- .../component/view/control/AbstractView.java | 22 +--- .../shell/component/view/control/AppView.java | 17 +++ .../shell/component/view/control/BoxView.java | 35 ++++-- .../shell/component/view/control/Control.java | 17 +++ .../component/view/control/ListView.java | 4 +- .../component/view/control/MenuBarView.java | 17 ++- .../component/view/control/MenuView.java | 4 +- .../view/control/cell/AbstractControl.java | 103 ++++++++++++++++++ .../component/view/screen/DefaultScreen.java | 15 ++- .../shell/style/StyleSettings.java | 16 ++- .../shell/style/ThemeRegistry.java | 12 +- .../shell/style/ThemeResolver.java | 75 ++++++++++++- .../shell/style/ThemeResolverTests.java | 60 +++++++++- .../shell/style/ThemeSettingsTests.java | 7 +- .../shell/samples/catalog/Catalog.java | 51 +++++++-- .../shell/samples/catalog/CatalogCommand.java | 7 +- .../samples/catalog/CatalogConfiguration.java | 40 +++++++ .../catalog/scenario/AbstractScenario.java | 21 +++- .../samples/catalog/scenario/Scenario.java | 5 +- .../scenario/box/SimpleBoxViewScenario.java | 1 + .../scenario/grid/SimpleGridViewScenario.java | 6 + 21 files changed, 480 insertions(+), 55 deletions(-) diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/AbstractView.java b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/AbstractView.java index 60def015..0cfa6ad6 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/AbstractView.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/AbstractView.java @@ -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 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; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/AppView.java b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/AppView.java index 5fc46137..e80d8dee 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/AppView.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/AppView.java @@ -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); diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/BoxView.java b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/BoxView.java index 0151a12a..fe78dd08 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/BoxView.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/BoxView.java @@ -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)) { diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/Control.java b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/Control.java index 5080bf08..912d7a54 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/Control.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/Control.java @@ -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); + } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/ListView.java b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/ListView.java index abe9d88b..bf0a3973 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/ListView.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/ListView.java @@ -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 extends BoxView { Rectangle rect = getInnerRect(); int y = rect.y(); + int selectedStyle = resolveThemeStyle(StyleSettings.TAG_HIGHLIGHT, ScreenItem.STYLE_BOLD); int i = 0; for (ListCell 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); diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/MenuBarView.java b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/MenuBarView.java index 941c9517..e50b28b8 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/MenuBarView.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/MenuBarView.java @@ -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); diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/MenuView.java b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/MenuView.java index 8cbb6426..a0346183 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/MenuView.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/MenuView.java @@ -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) { diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/cell/AbstractControl.java b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/cell/AbstractControl.java index 64deb8a3..1c7abf9a 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/cell/AbstractControl.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/view/control/cell/AbstractControl.java @@ -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 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); + } + } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/view/screen/DefaultScreen.java b/spring-shell-core/src/main/java/org/springframework/shell/component/view/screen/DefaultScreen.java index 3a491dc2..fbf6fa8e 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/view/screen/DefaultScreen.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/view/screen/DefaultScreen.java @@ -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; + } } } } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/style/StyleSettings.java b/spring-shell-core/src/main/java/org/springframework/shell/style/StyleSettings.java index 084dd31a..610ee3f5 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/style/StyleSettings.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/style/StyleSettings.java @@ -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 }; } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/style/ThemeRegistry.java b/spring-shell-core/src/main/java/org/springframework/shell/style/ThemeRegistry.java index 798c43c7..64f8ca18 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/style/ThemeRegistry.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/style/ThemeRegistry.java @@ -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 getThemeNames() { + return themes.keySet(); + } } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/style/ThemeResolver.java b/spring-shell-core/src/main/java/org/springframework/shell/style/ThemeResolver.java index 8935da89..2ad9ff14 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/style/ThemeResolver.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/style/ThemeResolver.java @@ -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 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. * diff --git a/spring-shell-core/src/test/java/org/springframework/shell/style/ThemeResolverTests.java b/spring-shell-core/src/test/java/org/springframework/shell/style/ThemeResolverTests.java index 8bff220f..84246a9c 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/style/ThemeResolverTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/style/ThemeResolverTests.java @@ -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); + } + } diff --git a/spring-shell-core/src/test/java/org/springframework/shell/style/ThemeSettingsTests.java b/spring-shell-core/src/test/java/org/springframework/shell/style/ThemeSettingsTests.java index a2ff085d..fcce4b50 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/style/ThemeSettingsTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/style/ThemeSettingsTests.java @@ -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(); } } diff --git a/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/Catalog.java b/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/Catalog.java index 93a19764..28fca330 100644 --- a/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/Catalog.java +++ b/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/Catalog.java @@ -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 categories; + private ListView scenarios; + private AppView app; private EventLoop eventLoop; + private ThemeResolver themeResolver; + private String activeThemeName = "default"; - public Catalog(Terminal terminal, List scenarios) { + public Catalog(Terminal terminal, List 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 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 buildCategorySelector(EventLoop eventLoop) { ListView categories = new ListView<>(); + categories.setThemeResolver(themeResolver); + categories.setThemeName(activeThemeName); categories.setEventLoop(eventLoop); List items = List.copyOf(categoryMap.keySet()); categories.setItems(items); @@ -234,6 +250,8 @@ public class Catalog { private ListView buildScenarioSelector(EventLoop eventLoop) { ListView 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"); diff --git a/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/CatalogCommand.java b/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/CatalogCommand.java index e5be446e..888071f6 100644 --- a/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/CatalogCommand.java +++ b/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/CatalogCommand.java @@ -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 scenarios; + private final ThemeResolver themeResolver; - public CatalogCommand(List scenarios) { + public CatalogCommand(List 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(); } } diff --git a/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/CatalogConfiguration.java b/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/CatalogConfiguration.java index 0fe93d75..28abb490 100644 --- a/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/CatalogConfiguration.java +++ b/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/CatalogConfiguration.java @@ -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"; + } + + } + } diff --git a/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/scenario/AbstractScenario.java b/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/scenario/AbstractScenario.java index 4680ca8f..1fdb5d1f 100644 --- a/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/scenario/AbstractScenario.java +++ b/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/scenario/AbstractScenario.java @@ -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()); + } } diff --git a/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/scenario/Scenario.java b/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/scenario/Scenario.java index 3f2c74f2..42c7abd7 100644 --- a/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/scenario/Scenario.java +++ b/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/scenario/Scenario.java @@ -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); } diff --git a/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/scenario/box/SimpleBoxViewScenario.java b/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/scenario/box/SimpleBoxViewScenario.java index 171aa2c1..f11c47c1 100644 --- a/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/scenario/box/SimpleBoxViewScenario.java +++ b/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/scenario/box/SimpleBoxViewScenario.java @@ -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); diff --git a/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/scenario/grid/SimpleGridViewScenario.java b/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/scenario/grid/SimpleGridViewScenario.java index 17cfe0ce..c6754938 100644 --- a/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/scenario/grid/SimpleGridViewScenario.java +++ b/spring-shell-samples/spring-shell-sample-catalog/src/main/java/org/springframework/shell/samples/catalog/scenario/grid/SimpleGridViewScenario.java @@ -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);