/*
 * This file is part of lanterna (https://github.com/mabe02/lanterna).
 *
 * lanterna is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Copyright (C) 2010-2020 Martin Berglund
 */
package com.googlecode.lanterna.graphics;

import com.googlecode.lanterna.SGR;
import com.googlecode.lanterna.TextColor;
import com.googlecode.lanterna.gui2.Button;
import com.googlecode.lanterna.gui2.Component;
import com.googlecode.lanterna.gui2.ComponentRenderer;
import com.googlecode.lanterna.gui2.WindowDecorationRenderer;
import com.googlecode.lanterna.gui2.WindowPostRenderer;
import com.googlecode.lanterna.gui2.WindowShadowRenderer;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

Abstract Theme implementation that manages a hierarchical tree of theme nodes ties to Class objects. Sub-classes will inherit their theme properties from super-class definitions, the java.lang.Object class is considered the root of the tree and as such is the fallback for all other classes.

You normally use this class through PropertyTheme, which is the default implementation bundled with Lanterna.

Author:Martin
/** * Abstract {@link Theme} implementation that manages a hierarchical tree of theme nodes ties to Class objects. * Sub-classes will inherit their theme properties from super-class definitions, the java.lang.Object class is * considered the root of the tree and as such is the fallback for all other classes. * <p> * You normally use this class through {@link PropertyTheme}, which is the default implementation bundled with Lanterna. * @author Martin */
public abstract class AbstractTheme implements Theme { private static final String STYLE_NORMAL = ""; private static final String STYLE_PRELIGHT = "PRELIGHT"; private static final String STYLE_SELECTED = "SELECTED"; private static final String STYLE_ACTIVE = "ACTIVE"; private static final String STYLE_INSENSITIVE = "INSENSITIVE"; private static final Pattern STYLE_FORMAT = Pattern.compile("([a-zA-Z]+)(\\[([a-zA-Z0-9-_]+)])?"); private final ThemeTreeNode rootNode; private final WindowPostRenderer windowPostRenderer; private final WindowDecorationRenderer windowDecorationRenderer; protected AbstractTheme(WindowPostRenderer postRenderer, WindowDecorationRenderer decorationRenderer) { this.rootNode = new ThemeTreeNode(Object.class, null); this.windowPostRenderer = postRenderer; this.windowDecorationRenderer = decorationRenderer; rootNode.foregroundMap.put(STYLE_NORMAL, TextColor.ANSI.WHITE); rootNode.backgroundMap.put(STYLE_NORMAL, TextColor.ANSI.BLACK); classloadStandardRenderersForGraal(); } private void classloadStandardRenderersForGraal() { // This will make graal know about these classes which would otherwise only // be loaded through reflection WindowShadowRenderer.class.toString(); Button.DefaultButtonRenderer.class.toString(); Button.FlatButtonRenderer.class.toString(); Button.BorderedButtonRenderer.class.toString(); } protected boolean addStyle(String definition, String style, String value) { ThemeTreeNode node = getNode(definition); if(node == null) { return false; } node.apply(style, value); return true; } private ThemeTreeNode getNode(String definition) { try { if(definition == null || definition.trim().isEmpty()) { return getNode(Object.class); } else { return getNode(Class.forName(definition)); } } catch(ClassNotFoundException e) { return null; } } private ThemeTreeNode getNode(Class<?> definition) { if(definition == Object.class) { return rootNode; } ThemeTreeNode parent = getNode(definition.getSuperclass()); if(parent.childMap.containsKey(definition)) { return parent.childMap.get(definition); } ThemeTreeNode node = new ThemeTreeNode(definition, parent); parent.childMap.put(definition, node); return node; } @Override public ThemeDefinition getDefaultDefinition() { return new DefinitionImpl(rootNode); } @Override public ThemeDefinition getDefinition(Class<?> clazz) { LinkedList<Class<?>> hierarchy = new LinkedList<>(); while(clazz != null && clazz != Object.class) { hierarchy.addFirst(clazz); clazz = clazz.getSuperclass(); } ThemeTreeNode node = rootNode; for(Class<?> aClass : hierarchy) { if(node.childMap.containsKey(aClass)) { node = node.childMap.get(aClass); } else { break; } } return new DefinitionImpl(node); } @Override public WindowPostRenderer getWindowPostRenderer() { return windowPostRenderer; } @Override public WindowDecorationRenderer getWindowDecorationRenderer() { return windowDecorationRenderer; } protected static Object instanceByClassName(String className) { if(className == null || className.trim().isEmpty()) { return null; } try { return Class.forName(className).newInstance(); } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) { throw new RuntimeException(e); } }
Returns a list of redundant theme entries in this theme. A redundant entry means that it doesn't need to be specified because there is a parent node in the hierarchy which has the same property so if the redundant entry wasn't there, the parent node would be picked up and the end result would be the same.
Returns:List of redundant theme entries
/** * Returns a list of redundant theme entries in this theme. A redundant entry means that it doesn't need to be * specified because there is a parent node in the hierarchy which has the same property so if the redundant entry * wasn't there, the parent node would be picked up and the end result would be the same. * @return List of redundant theme entries */
public List<String> findRedundantDeclarations() { List<String> result = new ArrayList<>(); for(ThemeTreeNode node: rootNode.childMap.values()) { findRedundantDeclarations(result, node); } Collections.sort(result); return result; } private void findRedundantDeclarations(List<String> result, ThemeTreeNode node) { for(String style: node.foregroundMap.keySet()) { String formattedStyle = "[" + style + "]"; if(formattedStyle.length() == 2) { formattedStyle = ""; } TextColor color = node.foregroundMap.get(style); TextColor colorFromParent = new StyleImpl(node.parent, style).getForeground(); if(color.equals(colorFromParent)) { result.add(node.clazz.getName() + ".foreground" + formattedStyle); } } for(String style: node.backgroundMap.keySet()) { String formattedStyle = "[" + style + "]"; if(formattedStyle.length() == 2) { formattedStyle = ""; } TextColor color = node.backgroundMap.get(style); TextColor colorFromParent = new StyleImpl(node.parent, style).getBackground(); if(color.equals(colorFromParent)) { result.add(node.clazz.getName() + ".background" + formattedStyle); } } for(String style: node.sgrMap.keySet()) { String formattedStyle = "[" + style + "]"; if(formattedStyle.length() == 2) { formattedStyle = ""; } EnumSet<SGR> sgrs = node.sgrMap.get(style); EnumSet<SGR> sgrsFromParent = new StyleImpl(node.parent, style).getSGRs(); if(sgrs.equals(sgrsFromParent)) { result.add(node.clazz.getName() + ".sgr" + formattedStyle); } } for(ThemeTreeNode childNode: node.childMap.values()) { findRedundantDeclarations(result, childNode); } } private class DefinitionImpl implements ThemeDefinition { final ThemeTreeNode node; public DefinitionImpl(ThemeTreeNode node) { this.node = node; } @Override public ThemeStyle getNormal() { return new StyleImpl(node, STYLE_NORMAL); } @Override public ThemeStyle getPreLight() { return new StyleImpl(node, STYLE_PRELIGHT); } @Override public ThemeStyle getSelected() { return new StyleImpl(node, STYLE_SELECTED); } @Override public ThemeStyle getActive() { return new StyleImpl(node, STYLE_ACTIVE); } @Override public ThemeStyle getInsensitive() { return new StyleImpl(node, STYLE_INSENSITIVE); } @Override public ThemeStyle getCustom(String name) { return new StyleImpl(node, name); } @Override public ThemeStyle getCustom(String name, ThemeStyle defaultValue) { ThemeStyle customStyle = getCustom(name); if(customStyle == null) { customStyle = defaultValue; } return customStyle; } @Override public char getCharacter(String name, char fallback) { Character character = node.characterMap.get(name); if(character == null) { if(node == rootNode) { return fallback; } else { return new DefinitionImpl(node.parent).getCharacter(name, fallback); } } return character; } @Override public boolean isCursorVisible() { Boolean cursorVisible = node.cursorVisible; if(cursorVisible == null) { if(node == rootNode) { return true; } else { return new DefinitionImpl(node.parent).isCursorVisible(); } } return cursorVisible; } @Override public boolean getBooleanProperty(String name, boolean defaultValue) { String propertyValue = node.propertyMap.get(name); if(propertyValue == null) { if(node == rootNode) { return defaultValue; } else { return new DefinitionImpl(node.parent).getBooleanProperty(name, defaultValue); } } return Boolean.parseBoolean(propertyValue); } @SuppressWarnings("unchecked") @Override public <T extends Component> ComponentRenderer<T> getRenderer(Class<T> type) { String rendererClass = node.renderer; if(rendererClass == null) { if(node == rootNode) { return null; } else { return new DefinitionImpl(node.parent).getRenderer(type); } } return (ComponentRenderer<T>)instanceByClassName(rendererClass); } } private class StyleImpl implements ThemeStyle { private final ThemeTreeNode styleNode; private final String name; private StyleImpl(ThemeTreeNode node, String name) { this.styleNode = node; this.name = name; } @Override public TextColor getForeground() { ThemeTreeNode node = styleNode; while(node != null) { if(node.foregroundMap.containsKey(name)) { return node.foregroundMap.get(name); } node = node.parent; } TextColor fallback = rootNode.foregroundMap.get(STYLE_NORMAL); if(fallback == null) { fallback = TextColor.ANSI.WHITE; } return fallback; } @Override public TextColor getBackground() { ThemeTreeNode node = styleNode; while(node != null) { if(node.backgroundMap.containsKey(name)) { return node.backgroundMap.get(name); } node = node.parent; } TextColor fallback = rootNode.backgroundMap.get(STYLE_NORMAL); if(fallback == null) { fallback = TextColor.ANSI.BLACK; } return fallback; } @Override public EnumSet<SGR> getSGRs() { ThemeTreeNode node = styleNode; while(node != null) { if(node.sgrMap.containsKey(name)) { return EnumSet.copyOf(node.sgrMap.get(name)); } node = node.parent; } EnumSet<SGR> fallback = rootNode.sgrMap.get(STYLE_NORMAL); if(fallback == null) { fallback = EnumSet.noneOf(SGR.class); } return EnumSet.copyOf(fallback); } } private static class ThemeTreeNode { private final Class<?> clazz; private final ThemeTreeNode parent; private final Map<Class<?>, ThemeTreeNode> childMap; private final Map<String, TextColor> foregroundMap; private final Map<String, TextColor> backgroundMap; private final Map<String, EnumSet<SGR>> sgrMap; private final Map<String, Character> characterMap; private final Map<String, String> propertyMap; private Boolean cursorVisible; private String renderer; private ThemeTreeNode(Class<?> clazz, ThemeTreeNode parent) { this.clazz = clazz; this.parent = parent; this.childMap = new HashMap<>(); this.foregroundMap = new HashMap<>(); this.backgroundMap = new HashMap<>(); this.sgrMap = new HashMap<>(); this.characterMap = new HashMap<>(); this.propertyMap = new HashMap<>(); this.cursorVisible = true; this.renderer = null; } private void apply(String style, String value) { value = value.trim(); Matcher matcher = STYLE_FORMAT.matcher(style); if(!matcher.matches()) { throw new IllegalArgumentException("Unknown style declaration: " + style); } String styleComponent = matcher.group(1); String group = matcher.groupCount() > 2 ? matcher.group(3) : null; switch (styleComponent.toLowerCase().trim()) { case "foreground": foregroundMap.put(getCategory(group), parseValue(value)); break; case "background": backgroundMap.put(getCategory(group), parseValue(value)); break; case "sgr": sgrMap.put(getCategory(group), parseSGR(value)); break; case "char": characterMap.put(getCategory(group), value.isEmpty() ? ' ' : value.charAt(0)); break; case "cursor": cursorVisible = Boolean.parseBoolean(value); break; case "property": propertyMap.put(getCategory(group), value.isEmpty() ? null : value.trim()); break; case "renderer": renderer = value.trim().isEmpty() ? null : value.trim(); break; case "postrenderer": case "windowdecoration": // Don't do anything with this now, we might use it later break; default: throw new IllegalArgumentException("Unknown style component \"" + styleComponent + "\" in style \"" + style + "\""); } } private TextColor parseValue(String value) { return TextColor.Factory.fromString(value); } private EnumSet<SGR> parseSGR(String value) { value = value.trim(); String[] sgrEntries = value.split(","); EnumSet<SGR> sgrSet = EnumSet.noneOf(SGR.class); for(String entry: sgrEntries) { entry = entry.trim().toUpperCase(); if(!entry.isEmpty()) { try { sgrSet.add(SGR.valueOf(entry)); } catch(IllegalArgumentException e) { throw new IllegalArgumentException("Unknown SGR code \"" + entry + "\"", e); } } } return sgrSet; } private String getCategory(String group) { if(group == null) { return STYLE_NORMAL; } for(String style: Arrays.asList(STYLE_ACTIVE, STYLE_INSENSITIVE, STYLE_PRELIGHT, STYLE_NORMAL, STYLE_SELECTED)) { if(group.toUpperCase().equals(style)) { return style; } } return group; } } }