/*
* This file is part of lanterna (http://code.google.com/p/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 .
*
* Copyright (C) 2010-2015 Martin
*/
package com.googlecode.lanterna.graphics;
import com.googlecode.lanterna.SGR;
import com.googlecode.lanterna.TextColor;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* This implementation of Theme reads its definitions from a {@code Properties} object.
* @author Martin
*/
public final class PropertiesTheme 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 static final Pattern INDEXED_COLOR = Pattern.compile("#[0-9]{1,3}");
private static final Pattern RGB_COLOR = Pattern.compile("#[0-9a-fA-F]{6}");
private final ThemeTreeNode rootNode;
/**
* Creates a new {@code PropertiesTheme} that is initialized by the properties value
* @param properties Properties to initialize this theme with
*/
public PropertiesTheme(Properties properties) {
rootNode = new ThemeTreeNode();
rootNode.foregroundMap.put(STYLE_NORMAL, TextColor.ANSI.WHITE);
rootNode.backgroundMap.put(STYLE_NORMAL, TextColor.ANSI.BLACK);
for(String key: properties.stringPropertyNames()) {
String definition = getDefinition(key);
ThemeTreeNode node = getNode(definition);
node.apply(getStyle(key), properties.getProperty(key));
}
}
private ThemeTreeNode getNode(String definition) {
ThemeTreeNode parentNode;
if(definition.equals("")) {
return rootNode;
}
else if(definition.contains(".")) {
String parent = definition.substring(0, definition.lastIndexOf("."));
parentNode = getNode(parent);
definition = definition.substring(definition.lastIndexOf(".") + 1);
}
else {
parentNode = rootNode;
}
if(!parentNode.childMap.containsKey(definition)) {
parentNode.childMap.put(definition, new ThemeTreeNode());
}
return parentNode.childMap.get(definition);
}
private String getDefinition(String propertyName) {
if(!propertyName.contains(".")) {
return "";
}
else {
return propertyName.substring(0, propertyName.lastIndexOf("."));
}
}
private String getStyle(String propertyName) {
if(!propertyName.contains(".")) {
return propertyName;
}
else {
return propertyName.substring(propertyName.lastIndexOf(".") + 1);
}
}
@Override
public ThemeDefinition getDefaultDefinition() {
return new DefinitionImpl(Collections.singletonList(rootNode));
}
@Override
public ThemeDefinition getDefinition(Class> clazz) {
String name = clazz.getName();
List path = new ArrayList();
ThemeTreeNode currentNode = rootNode;
while(!name.equals("")) {
path.add(currentNode);
String nextNodeName = name;
if(nextNodeName.contains(".")) {
nextNodeName = nextNodeName.substring(0, name.indexOf("."));
name = name.substring(name.indexOf(".") + 1);
}
if(currentNode.childMap.containsKey(nextNodeName)) {
currentNode = currentNode.childMap.get(nextNodeName);
}
else {
break;
}
}
return new DefinitionImpl(path);
}
private class DefinitionImpl implements ThemeDefinition {
final List path;
DefinitionImpl(List path) {
this.path = path;
}
@Override
public ThemeStyle getNormal() {
return new StyleImpl(path, STYLE_NORMAL);
}
@Override
public ThemeStyle getPreLight() {
return new StyleImpl(path, STYLE_PRELIGHT);
}
@Override
public ThemeStyle getSelected() {
return new StyleImpl(path, STYLE_SELECTED);
}
@Override
public ThemeStyle getActive() {
return new StyleImpl(path, STYLE_ACTIVE);
}
@Override
public ThemeStyle getInsensitive() {
return new StyleImpl(path, STYLE_INSENSITIVE);
}
@Override
public ThemeStyle getCustom(String name) {
ThemeTreeNode lastElement = path.get(path.size() - 1);
if(lastElement.sgrMap.containsKey(name) ||
lastElement.foregroundMap.containsKey(name) ||
lastElement.backgroundMap.containsKey(name)) {
return new StyleImpl(path, name);
}
// If there was no custom style with this name, just return the normal one
return getNormal();
}
@Override
public char getCharacter(String name, char fallback) {
Character character = path.get(path.size() - 1).characterMap.get(name);
if(character == null) {
return fallback;
}
return character;
}
@Override
public String getRenderer() {
return path.get(path.size() - 1).renderer;
}
}
private class StyleImpl implements ThemeStyle {
private final List path;
private final String name;
private StyleImpl(List path, String name) {
this.path = path;
this.name = name;
}
@Override
public TextColor getForeground() {
ListIterator iterator = path.listIterator(path.size());
while(iterator.hasPrevious()) {
ThemeTreeNode node = iterator.previous();
if(node.foregroundMap.containsKey(name)) {
return node.foregroundMap.get(name);
}
}
if(!name.equals(STYLE_NORMAL)) {
return new StyleImpl(path, STYLE_NORMAL).getForeground();
}
return TextColor.ANSI.WHITE;
}
@Override
public TextColor getBackground() {
ListIterator iterator = path.listIterator(path.size());
while(iterator.hasPrevious()) {
ThemeTreeNode node = iterator.previous();
if(node.backgroundMap.containsKey(name)) {
return node.backgroundMap.get(name);
}
}
if(!name.equals(STYLE_NORMAL)) {
return new StyleImpl(path, STYLE_NORMAL).getBackground();
}
return TextColor.ANSI.BLACK;
}
@Override
public EnumSet getSGRs() {
ListIterator iterator = path.listIterator(path.size());
while(iterator.hasPrevious()) {
ThemeTreeNode node = iterator.previous();
if(node.sgrMap.containsKey(name)) {
return node.sgrMap.get(name);
}
}
if(!name.equals(STYLE_NORMAL)) {
return new StyleImpl(path, STYLE_NORMAL).getSGRs();
}
return EnumSet.noneOf(SGR.class);
}
}
private static class ThemeTreeNode {
private final Map childMap;
private final Map foregroundMap;
private final Map backgroundMap;
private final Map> sgrMap;
private final Map characterMap;
private String renderer;
private ThemeTreeNode() {
childMap = new HashMap();
foregroundMap = new HashMap();
backgroundMap = new HashMap();
sgrMap = new HashMap>();
characterMap = new HashMap();
renderer = null;
}
public 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;
if(styleComponent.toLowerCase().trim().equals("foreground")) {
foregroundMap.put(getCategory(group), parseValue(value));
}
else if(styleComponent.toLowerCase().trim().equals("background")) {
backgroundMap.put(getCategory(group), parseValue(value));
}
else if(styleComponent.toLowerCase().trim().equals("sgr")) {
sgrMap.put(getCategory(group), parseSGR(value));
}
else if(styleComponent.toLowerCase().trim().equals("char")) {
characterMap.put(getCategory(group), value.isEmpty() ? null : value.charAt(0));
}
else if(styleComponent.toLowerCase().trim().equals("renderer")) {
renderer = value.trim().isEmpty() ? null : value.trim();
}
else {
throw new IllegalArgumentException("Unknown style component \"" + styleComponent + "\" in style \"" + style + "\"");
}
}
private TextColor parseValue(String value) {
value = value.trim();
if(RGB_COLOR.matcher(value).matches()) {
int r = Integer.parseInt(value.substring(1, 3), 16);
int g = Integer.parseInt(value.substring(3, 5), 16);
int b = Integer.parseInt(value.substring(5, 7), 16);
return new TextColor.RGB(r, g, b);
}
else if(INDEXED_COLOR.matcher(value).matches()) {
int index = Integer.parseInt(value.substring(1));
return new TextColor.Indexed(index);
}
try {
return TextColor.ANSI.valueOf(value.toUpperCase());
}
catch(IllegalArgumentException e) {
throw new IllegalArgumentException("Unknown color definition \"" + value + "\"", e);
}
}
private EnumSet parseSGR(String value) {
value = value.trim();
String[] sgrEntries = value.split(",");
EnumSet 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;
}
}
}