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:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user