2 * This file is part of lanterna (http://code.google.com/p/lanterna/).
4 * lanterna is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Lesser General Public License for more details.
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 * Copyright (C) 2010-2015 Martin
19 package com
.googlecode
.lanterna
.graphics
;
21 import com
.googlecode
.lanterna
.SGR
;
22 import com
.googlecode
.lanterna
.TextColor
;
25 import java
.util
.regex
.Matcher
;
26 import java
.util
.regex
.Pattern
;
29 * This implementation of Theme reads its definitions from a {@code Properties} object.
32 public final class PropertiesTheme
implements Theme
{
33 private static final String STYLE_NORMAL
= "";
34 private static final String STYLE_PRELIGHT
= "PRELIGHT";
35 private static final String STYLE_SELECTED
= "SELECTED";
36 private static final String STYLE_ACTIVE
= "ACTIVE";
37 private static final String STYLE_INSENSITIVE
= "INSENSITIVE";
39 private static final Pattern STYLE_FORMAT
= Pattern
.compile("([a-zA-Z]+)(\\[([a-zA-Z0-9-_]+)\\])?");
40 private static final Pattern INDEXED_COLOR
= Pattern
.compile("#[0-9]{1,3}");
41 private static final Pattern RGB_COLOR
= Pattern
.compile("#[0-9a-fA-F]{6}");
43 private final ThemeTreeNode rootNode
;
46 * Creates a new {@code PropertiesTheme} that is initialized by the properties value
47 * @param properties Properties to initialize this theme with
49 public PropertiesTheme(Properties properties
) {
50 rootNode
= new ThemeTreeNode();
51 rootNode
.foregroundMap
.put(STYLE_NORMAL
, TextColor
.ANSI
.WHITE
);
52 rootNode
.backgroundMap
.put(STYLE_NORMAL
, TextColor
.ANSI
.BLACK
);
54 for(String key
: properties
.stringPropertyNames()) {
55 String definition
= getDefinition(key
);
56 ThemeTreeNode node
= getNode(definition
);
57 node
.apply(getStyle(key
), properties
.getProperty(key
));
61 private ThemeTreeNode
getNode(String definition
) {
62 ThemeTreeNode parentNode
;
63 if(definition
.equals("")) {
66 else if(definition
.contains(".")) {
67 String parent
= definition
.substring(0, definition
.lastIndexOf("."));
68 parentNode
= getNode(parent
);
69 definition
= definition
.substring(definition
.lastIndexOf(".") + 1);
72 parentNode
= rootNode
;
74 if(!parentNode
.childMap
.containsKey(definition
)) {
75 parentNode
.childMap
.put(definition
, new ThemeTreeNode());
77 return parentNode
.childMap
.get(definition
);
80 private String
getDefinition(String propertyName
) {
81 if(!propertyName
.contains(".")) {
85 return propertyName
.substring(0, propertyName
.lastIndexOf("."));
89 private String
getStyle(String propertyName
) {
90 if(!propertyName
.contains(".")) {
94 return propertyName
.substring(propertyName
.lastIndexOf(".") + 1);
99 public ThemeDefinition
getDefaultDefinition() {
100 return new DefinitionImpl(Collections
.singletonList(rootNode
));
104 public ThemeDefinition
getDefinition(Class
<?
> clazz
) {
105 String name
= clazz
.getName();
106 List
<ThemeTreeNode
> path
= new ArrayList
<ThemeTreeNode
>();
107 ThemeTreeNode currentNode
= rootNode
;
108 while(!name
.equals("")) {
109 path
.add(currentNode
);
110 String nextNodeName
= name
;
111 if(nextNodeName
.contains(".")) {
112 nextNodeName
= nextNodeName
.substring(0, name
.indexOf("."));
113 name
= name
.substring(name
.indexOf(".") + 1);
115 if(currentNode
.childMap
.containsKey(nextNodeName
)) {
116 currentNode
= currentNode
.childMap
.get(nextNodeName
);
122 return new DefinitionImpl(path
);
126 private class DefinitionImpl
implements ThemeDefinition
{
127 final List
<ThemeTreeNode
> path
;
129 DefinitionImpl(List
<ThemeTreeNode
> path
) {
134 public ThemeStyle
getNormal() {
135 return new StyleImpl(path
, STYLE_NORMAL
);
139 public ThemeStyle
getPreLight() {
140 return new StyleImpl(path
, STYLE_PRELIGHT
);
144 public ThemeStyle
getSelected() {
145 return new StyleImpl(path
, STYLE_SELECTED
);
149 public ThemeStyle
getActive() {
150 return new StyleImpl(path
, STYLE_ACTIVE
);
154 public ThemeStyle
getInsensitive() {
155 return new StyleImpl(path
, STYLE_INSENSITIVE
);
159 public ThemeStyle
getCustom(String name
) {
160 ThemeTreeNode lastElement
= path
.get(path
.size() - 1);
161 if(lastElement
.sgrMap
.containsKey(name
) ||
162 lastElement
.foregroundMap
.containsKey(name
) ||
163 lastElement
.backgroundMap
.containsKey(name
)) {
164 return new StyleImpl(path
, name
);
170 public char getCharacter(String name
, char fallback
) {
171 Character character
= path
.get(path
.size() - 1).characterMap
.get(name
);
172 if(character
== null) {
179 public String
getRenderer() {
180 return path
.get(path
.size() - 1).renderer
;
184 private class StyleImpl
implements ThemeStyle
{
185 private final List
<ThemeTreeNode
> path
;
186 private final String name
;
188 private StyleImpl(List
<ThemeTreeNode
> path
, String name
) {
194 public TextColor
getForeground() {
195 ListIterator
<ThemeTreeNode
> iterator
= path
.listIterator(path
.size());
196 while(iterator
.hasPrevious()) {
197 ThemeTreeNode node
= iterator
.previous();
198 if(node
.foregroundMap
.containsKey(name
)) {
199 return node
.foregroundMap
.get(name
);
202 if(!name
.equals(STYLE_NORMAL
)) {
203 return new StyleImpl(path
, STYLE_NORMAL
).getForeground();
205 return TextColor
.ANSI
.WHITE
;
209 public TextColor
getBackground() {
210 ListIterator
<ThemeTreeNode
> iterator
= path
.listIterator(path
.size());
211 while(iterator
.hasPrevious()) {
212 ThemeTreeNode node
= iterator
.previous();
213 if(node
.backgroundMap
.containsKey(name
)) {
214 return node
.backgroundMap
.get(name
);
217 if(!name
.equals(STYLE_NORMAL
)) {
218 return new StyleImpl(path
, STYLE_NORMAL
).getBackground();
220 return TextColor
.ANSI
.BLACK
;
224 public EnumSet
<SGR
> getSGRs() {
225 ListIterator
<ThemeTreeNode
> iterator
= path
.listIterator(path
.size());
226 while(iterator
.hasPrevious()) {
227 ThemeTreeNode node
= iterator
.previous();
228 if(node
.sgrMap
.containsKey(name
)) {
229 return node
.sgrMap
.get(name
);
232 if(!name
.equals(STYLE_NORMAL
)) {
233 return new StyleImpl(path
, STYLE_NORMAL
).getSGRs();
235 return EnumSet
.noneOf(SGR
.class);
239 private static class ThemeTreeNode
{
240 private final Map
<String
, ThemeTreeNode
> childMap
;
241 private final Map
<String
, TextColor
> foregroundMap
;
242 private final Map
<String
, TextColor
> backgroundMap
;
243 private final Map
<String
, EnumSet
<SGR
>> sgrMap
;
244 private final Map
<String
, Character
> characterMap
;
245 private String renderer
;
247 private ThemeTreeNode() {
248 childMap
= new HashMap
<String
, ThemeTreeNode
>();
249 foregroundMap
= new HashMap
<String
, TextColor
>();
250 backgroundMap
= new HashMap
<String
, TextColor
>();
251 sgrMap
= new HashMap
<String
, EnumSet
<SGR
>>();
252 characterMap
= new HashMap
<String
, Character
>();
256 public void apply(String style
, String value
) {
257 value
= value
.trim();
258 Matcher matcher
= STYLE_FORMAT
.matcher(style
);
259 if(!matcher
.matches()) {
260 throw new IllegalArgumentException("Unknown style declaration: " + style
);
262 String styleComponent
= matcher
.group(1);
263 String group
= matcher
.groupCount() > 2 ? matcher
.group(3) : null;
264 if(styleComponent
.toLowerCase().trim().equals("foreground")) {
265 foregroundMap
.put(getCategory(group
), parseValue(value
));
267 else if(styleComponent
.toLowerCase().trim().equals("background")) {
268 backgroundMap
.put(getCategory(group
), parseValue(value
));
270 else if(styleComponent
.toLowerCase().trim().equals("sgr")) {
271 sgrMap
.put(getCategory(group
), parseSGR(value
));
273 else if(styleComponent
.toLowerCase().trim().equals("char")) {
274 characterMap
.put(getCategory(group
), value
.isEmpty() ?
null : value
.charAt(0));
276 else if(styleComponent
.toLowerCase().trim().equals("renderer")) {
277 renderer
= value
.trim().isEmpty() ?
null : value
.trim();
280 throw new IllegalArgumentException("Unknown style component \"" + styleComponent
+ "\" in style \"" + style
+ "\"");
284 private TextColor
parseValue(String value
) {
285 value
= value
.trim();
286 if(RGB_COLOR
.matcher(value
).matches()) {
287 int r
= Integer
.parseInt(value
.substring(1, 3), 16);
288 int g
= Integer
.parseInt(value
.substring(3, 5), 16);
289 int b
= Integer
.parseInt(value
.substring(5, 7), 16);
290 return new TextColor
.RGB(r
, g
, b
);
292 else if(INDEXED_COLOR
.matcher(value
).matches()) {
293 int index
= Integer
.parseInt(value
.substring(1));
294 return new TextColor
.Indexed(index
);
297 return TextColor
.ANSI
.valueOf(value
.toUpperCase());
299 catch(IllegalArgumentException e
) {
300 throw new IllegalArgumentException("Unknown color definition \"" + value
+ "\"", e
);
304 private EnumSet
<SGR
> parseSGR(String value
) {
305 value
= value
.trim();
306 String
[] sgrEntries
= value
.split(",");
307 EnumSet
<SGR
> sgrSet
= EnumSet
.noneOf(SGR
.class);
308 for(String entry
: sgrEntries
) {
309 entry
= entry
.trim().toUpperCase();
310 if(!entry
.isEmpty()) {
312 sgrSet
.add(SGR
.valueOf(entry
));
314 catch(IllegalArgumentException e
) {
315 throw new IllegalArgumentException("Unknown SGR code \"" + entry
+ "\"", e
);
322 private String
getCategory(String group
) {
326 for(String style
: Arrays
.asList(STYLE_ACTIVE
, STYLE_INSENSITIVE
, STYLE_PRELIGHT
, STYLE_NORMAL
, STYLE_SELECTED
)) {
327 if(group
.toUpperCase().equals(style
)) {