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
);
166 // If there was no custom style with this name, just return the normal one
171 public char getCharacter(String name
, char fallback
) {
172 Character character
= path
.get(path
.size() - 1).characterMap
.get(name
);
173 if(character
== null) {
180 public String
getRenderer() {
181 return path
.get(path
.size() - 1).renderer
;
185 private class StyleImpl
implements ThemeStyle
{
186 private final List
<ThemeTreeNode
> path
;
187 private final String name
;
189 private StyleImpl(List
<ThemeTreeNode
> path
, String name
) {
195 public TextColor
getForeground() {
196 ListIterator
<ThemeTreeNode
> iterator
= path
.listIterator(path
.size());
197 while(iterator
.hasPrevious()) {
198 ThemeTreeNode node
= iterator
.previous();
199 if(node
.foregroundMap
.containsKey(name
)) {
200 return node
.foregroundMap
.get(name
);
203 if(!name
.equals(STYLE_NORMAL
)) {
204 return new StyleImpl(path
, STYLE_NORMAL
).getForeground();
206 return TextColor
.ANSI
.WHITE
;
210 public TextColor
getBackground() {
211 ListIterator
<ThemeTreeNode
> iterator
= path
.listIterator(path
.size());
212 while(iterator
.hasPrevious()) {
213 ThemeTreeNode node
= iterator
.previous();
214 if(node
.backgroundMap
.containsKey(name
)) {
215 return node
.backgroundMap
.get(name
);
218 if(!name
.equals(STYLE_NORMAL
)) {
219 return new StyleImpl(path
, STYLE_NORMAL
).getBackground();
221 return TextColor
.ANSI
.BLACK
;
225 public EnumSet
<SGR
> getSGRs() {
226 ListIterator
<ThemeTreeNode
> iterator
= path
.listIterator(path
.size());
227 while(iterator
.hasPrevious()) {
228 ThemeTreeNode node
= iterator
.previous();
229 if(node
.sgrMap
.containsKey(name
)) {
230 return node
.sgrMap
.get(name
);
233 if(!name
.equals(STYLE_NORMAL
)) {
234 return new StyleImpl(path
, STYLE_NORMAL
).getSGRs();
236 return EnumSet
.noneOf(SGR
.class);
240 private static class ThemeTreeNode
{
241 private final Map
<String
, ThemeTreeNode
> childMap
;
242 private final Map
<String
, TextColor
> foregroundMap
;
243 private final Map
<String
, TextColor
> backgroundMap
;
244 private final Map
<String
, EnumSet
<SGR
>> sgrMap
;
245 private final Map
<String
, Character
> characterMap
;
246 private String renderer
;
248 private ThemeTreeNode() {
249 childMap
= new HashMap
<String
, ThemeTreeNode
>();
250 foregroundMap
= new HashMap
<String
, TextColor
>();
251 backgroundMap
= new HashMap
<String
, TextColor
>();
252 sgrMap
= new HashMap
<String
, EnumSet
<SGR
>>();
253 characterMap
= new HashMap
<String
, Character
>();
257 public void apply(String style
, String value
) {
258 value
= value
.trim();
259 Matcher matcher
= STYLE_FORMAT
.matcher(style
);
260 if(!matcher
.matches()) {
261 throw new IllegalArgumentException("Unknown style declaration: " + style
);
263 String styleComponent
= matcher
.group(1);
264 String group
= matcher
.groupCount() > 2 ? matcher
.group(3) : null;
265 if(styleComponent
.toLowerCase().trim().equals("foreground")) {
266 foregroundMap
.put(getCategory(group
), parseValue(value
));
268 else if(styleComponent
.toLowerCase().trim().equals("background")) {
269 backgroundMap
.put(getCategory(group
), parseValue(value
));
271 else if(styleComponent
.toLowerCase().trim().equals("sgr")) {
272 sgrMap
.put(getCategory(group
), parseSGR(value
));
274 else if(styleComponent
.toLowerCase().trim().equals("char")) {
275 characterMap
.put(getCategory(group
), value
.isEmpty() ?
null : value
.charAt(0));
277 else if(styleComponent
.toLowerCase().trim().equals("renderer")) {
278 renderer
= value
.trim().isEmpty() ?
null : value
.trim();
281 throw new IllegalArgumentException("Unknown style component \"" + styleComponent
+ "\" in style \"" + style
+ "\"");
285 private TextColor
parseValue(String value
) {
286 value
= value
.trim();
287 if(RGB_COLOR
.matcher(value
).matches()) {
288 int r
= Integer
.parseInt(value
.substring(1, 3), 16);
289 int g
= Integer
.parseInt(value
.substring(3, 5), 16);
290 int b
= Integer
.parseInt(value
.substring(5, 7), 16);
291 return new TextColor
.RGB(r
, g
, b
);
293 else if(INDEXED_COLOR
.matcher(value
).matches()) {
294 int index
= Integer
.parseInt(value
.substring(1));
295 return new TextColor
.Indexed(index
);
298 return TextColor
.ANSI
.valueOf(value
.toUpperCase());
300 catch(IllegalArgumentException e
) {
301 throw new IllegalArgumentException("Unknown color definition \"" + value
+ "\"", e
);
305 private EnumSet
<SGR
> parseSGR(String value
) {
306 value
= value
.trim();
307 String
[] sgrEntries
= value
.split(",");
308 EnumSet
<SGR
> sgrSet
= EnumSet
.noneOf(SGR
.class);
309 for(String entry
: sgrEntries
) {
310 entry
= entry
.trim().toUpperCase();
311 if(!entry
.isEmpty()) {
313 sgrSet
.add(SGR
.valueOf(entry
));
315 catch(IllegalArgumentException e
) {
316 throw new IllegalArgumentException("Unknown SGR code \"" + entry
+ "\"", e
);
323 private String
getCategory(String group
) {
327 for(String style
: Arrays
.asList(STYLE_ACTIVE
, STYLE_INSENSITIVE
, STYLE_PRELIGHT
, STYLE_NORMAL
, STYLE_SELECTED
)) {
328 if(group
.toUpperCase().equals(style
)) {