40f61d1a8a11eff296233bc213fc029701025859
[jvcard.git] / src / com / googlecode / lanterna / graphics / PropertiesTheme.java
1 /*
2 * This file is part of lanterna (http://code.google.com/p/lanterna/).
3 *
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.
8 *
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.
13 *
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/>.
16 *
17 * Copyright (C) 2010-2015 Martin
18 */
19 package com.googlecode.lanterna.graphics;
20
21 import com.googlecode.lanterna.SGR;
22 import com.googlecode.lanterna.TextColor;
23
24 import java.util.*;
25 import java.util.regex.Matcher;
26 import java.util.regex.Pattern;
27
28 /**
29 * This implementation of Theme reads its definitions from a {@code Properties} object.
30 * @author Martin
31 */
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";
38
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}");
42
43 private final ThemeTreeNode rootNode;
44
45 /**
46 * Creates a new {@code PropertiesTheme} that is initialized by the properties value
47 * @param properties Properties to initialize this theme with
48 */
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);
53
54 for(String key: properties.stringPropertyNames()) {
55 String definition = getDefinition(key);
56 ThemeTreeNode node = getNode(definition);
57 node.apply(getStyle(key), properties.getProperty(key));
58 }
59 }
60
61 private ThemeTreeNode getNode(String definition) {
62 ThemeTreeNode parentNode;
63 if(definition.equals("")) {
64 return rootNode;
65 }
66 else if(definition.contains(".")) {
67 String parent = definition.substring(0, definition.lastIndexOf("."));
68 parentNode = getNode(parent);
69 definition = definition.substring(definition.lastIndexOf(".") + 1);
70 }
71 else {
72 parentNode = rootNode;
73 }
74 if(!parentNode.childMap.containsKey(definition)) {
75 parentNode.childMap.put(definition, new ThemeTreeNode());
76 }
77 return parentNode.childMap.get(definition);
78 }
79
80 private String getDefinition(String propertyName) {
81 if(!propertyName.contains(".")) {
82 return "";
83 }
84 else {
85 return propertyName.substring(0, propertyName.lastIndexOf("."));
86 }
87 }
88
89 private String getStyle(String propertyName) {
90 if(!propertyName.contains(".")) {
91 return propertyName;
92 }
93 else {
94 return propertyName.substring(propertyName.lastIndexOf(".") + 1);
95 }
96 }
97
98 @Override
99 public ThemeDefinition getDefaultDefinition() {
100 return new DefinitionImpl(Collections.singletonList(rootNode));
101 }
102
103 @Override
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);
114 }
115 if(currentNode.childMap.containsKey(nextNodeName)) {
116 currentNode = currentNode.childMap.get(nextNodeName);
117 }
118 else {
119 break;
120 }
121 }
122 return new DefinitionImpl(path);
123 }
124
125
126 private class DefinitionImpl implements ThemeDefinition {
127 final List<ThemeTreeNode> path;
128
129 DefinitionImpl(List<ThemeTreeNode> path) {
130 this.path = path;
131 }
132
133 @Override
134 public ThemeStyle getNormal() {
135 return new StyleImpl(path, STYLE_NORMAL);
136 }
137
138 @Override
139 public ThemeStyle getPreLight() {
140 return new StyleImpl(path, STYLE_PRELIGHT);
141 }
142
143 @Override
144 public ThemeStyle getSelected() {
145 return new StyleImpl(path, STYLE_SELECTED);
146 }
147
148 @Override
149 public ThemeStyle getActive() {
150 return new StyleImpl(path, STYLE_ACTIVE);
151 }
152
153 @Override
154 public ThemeStyle getInsensitive() {
155 return new StyleImpl(path, STYLE_INSENSITIVE);
156 }
157
158 @Override
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);
165 }
166 return null;
167 }
168
169 @Override
170 public char getCharacter(String name, char fallback) {
171 Character character = path.get(path.size() - 1).characterMap.get(name);
172 if(character == null) {
173 return fallback;
174 }
175 return character;
176 }
177
178 @Override
179 public String getRenderer() {
180 return path.get(path.size() - 1).renderer;
181 }
182 }
183
184 private class StyleImpl implements ThemeStyle {
185 private final List<ThemeTreeNode> path;
186 private final String name;
187
188 private StyleImpl(List<ThemeTreeNode> path, String name) {
189 this.path = path;
190 this.name = name;
191 }
192
193 @Override
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);
200 }
201 }
202 if(!name.equals(STYLE_NORMAL)) {
203 return new StyleImpl(path, STYLE_NORMAL).getForeground();
204 }
205 return TextColor.ANSI.WHITE;
206 }
207
208 @Override
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);
215 }
216 }
217 if(!name.equals(STYLE_NORMAL)) {
218 return new StyleImpl(path, STYLE_NORMAL).getBackground();
219 }
220 return TextColor.ANSI.BLACK;
221 }
222
223 @Override
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);
230 }
231 }
232 if(!name.equals(STYLE_NORMAL)) {
233 return new StyleImpl(path, STYLE_NORMAL).getSGRs();
234 }
235 return EnumSet.noneOf(SGR.class);
236 }
237 }
238
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;
246
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>();
253 renderer = null;
254 }
255
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);
261 }
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));
266 }
267 else if(styleComponent.toLowerCase().trim().equals("background")) {
268 backgroundMap.put(getCategory(group), parseValue(value));
269 }
270 else if(styleComponent.toLowerCase().trim().equals("sgr")) {
271 sgrMap.put(getCategory(group), parseSGR(value));
272 }
273 else if(styleComponent.toLowerCase().trim().equals("char")) {
274 characterMap.put(getCategory(group), value.isEmpty() ? null : value.charAt(0));
275 }
276 else if(styleComponent.toLowerCase().trim().equals("renderer")) {
277 renderer = value.trim().isEmpty() ? null : value.trim();
278 }
279 else {
280 throw new IllegalArgumentException("Unknown style component \"" + styleComponent + "\" in style \"" + style + "\"");
281 }
282 }
283
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);
291 }
292 else if(INDEXED_COLOR.matcher(value).matches()) {
293 int index = Integer.parseInt(value.substring(1));
294 return new TextColor.Indexed(index);
295 }
296 try {
297 return TextColor.ANSI.valueOf(value.toUpperCase());
298 }
299 catch(IllegalArgumentException e) {
300 throw new IllegalArgumentException("Unknown color definition \"" + value + "\"", e);
301 }
302 }
303
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()) {
311 try {
312 sgrSet.add(SGR.valueOf(entry));
313 }
314 catch(IllegalArgumentException e) {
315 throw new IllegalArgumentException("Unknown SGR code \"" + entry + "\"", e);
316 }
317 }
318 }
319 return sgrSet;
320 }
321
322 private String getCategory(String group) {
323 if(group == null) {
324 return STYLE_NORMAL;
325 }
326 for(String style: Arrays.asList(STYLE_ACTIVE, STYLE_INSENSITIVE, STYLE_PRELIGHT, STYLE_NORMAL, STYLE_SELECTED)) {
327 if(group.toUpperCase().equals(style)) {
328 return style;
329 }
330 }
331 return group;
332 }
333 }
334 }