Commit | Line | Data |
---|---|---|
a3b510ab NR |
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 | } | |
b71d1368 NR |
166 | // If there was no custom style with this name, just return the normal one |
167 | return getNormal(); | |
a3b510ab NR |
168 | } |
169 | ||
170 | @Override | |
171 | public char getCharacter(String name, char fallback) { | |
172 | Character character = path.get(path.size() - 1).characterMap.get(name); | |
173 | if(character == null) { | |
174 | return fallback; | |
175 | } | |
176 | return character; | |
177 | } | |
178 | ||
179 | @Override | |
180 | public String getRenderer() { | |
181 | return path.get(path.size() - 1).renderer; | |
182 | } | |
183 | } | |
184 | ||
185 | private class StyleImpl implements ThemeStyle { | |
186 | private final List<ThemeTreeNode> path; | |
187 | private final String name; | |
188 | ||
189 | private StyleImpl(List<ThemeTreeNode> path, String name) { | |
190 | this.path = path; | |
191 | this.name = name; | |
192 | } | |
193 | ||
194 | @Override | |
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); | |
201 | } | |
202 | } | |
203 | if(!name.equals(STYLE_NORMAL)) { | |
204 | return new StyleImpl(path, STYLE_NORMAL).getForeground(); | |
205 | } | |
206 | return TextColor.ANSI.WHITE; | |
207 | } | |
208 | ||
209 | @Override | |
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); | |
216 | } | |
217 | } | |
218 | if(!name.equals(STYLE_NORMAL)) { | |
219 | return new StyleImpl(path, STYLE_NORMAL).getBackground(); | |
220 | } | |
221 | return TextColor.ANSI.BLACK; | |
222 | } | |
223 | ||
224 | @Override | |
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); | |
231 | } | |
232 | } | |
233 | if(!name.equals(STYLE_NORMAL)) { | |
234 | return new StyleImpl(path, STYLE_NORMAL).getSGRs(); | |
235 | } | |
236 | return EnumSet.noneOf(SGR.class); | |
237 | } | |
238 | } | |
239 | ||
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; | |
247 | ||
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>(); | |
254 | renderer = null; | |
255 | } | |
256 | ||
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); | |
262 | } | |
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)); | |
267 | } | |
268 | else if(styleComponent.toLowerCase().trim().equals("background")) { | |
269 | backgroundMap.put(getCategory(group), parseValue(value)); | |
270 | } | |
271 | else if(styleComponent.toLowerCase().trim().equals("sgr")) { | |
272 | sgrMap.put(getCategory(group), parseSGR(value)); | |
273 | } | |
274 | else if(styleComponent.toLowerCase().trim().equals("char")) { | |
275 | characterMap.put(getCategory(group), value.isEmpty() ? null : value.charAt(0)); | |
276 | } | |
277 | else if(styleComponent.toLowerCase().trim().equals("renderer")) { | |
278 | renderer = value.trim().isEmpty() ? null : value.trim(); | |
279 | } | |
280 | else { | |
281 | throw new IllegalArgumentException("Unknown style component \"" + styleComponent + "\" in style \"" + style + "\""); | |
282 | } | |
283 | } | |
284 | ||
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); | |
292 | } | |
293 | else if(INDEXED_COLOR.matcher(value).matches()) { | |
294 | int index = Integer.parseInt(value.substring(1)); | |
295 | return new TextColor.Indexed(index); | |
296 | } | |
297 | try { | |
298 | return TextColor.ANSI.valueOf(value.toUpperCase()); | |
299 | } | |
300 | catch(IllegalArgumentException e) { | |
301 | throw new IllegalArgumentException("Unknown color definition \"" + value + "\"", e); | |
302 | } | |
303 | } | |
304 | ||
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()) { | |
312 | try { | |
313 | sgrSet.add(SGR.valueOf(entry)); | |
314 | } | |
315 | catch(IllegalArgumentException e) { | |
316 | throw new IllegalArgumentException("Unknown SGR code \"" + entry + "\"", e); | |
317 | } | |
318 | } | |
319 | } | |
320 | return sgrSet; | |
321 | } | |
322 | ||
323 | private String getCategory(String group) { | |
324 | if(group == null) { | |
325 | return STYLE_NORMAL; | |
326 | } | |
327 | for(String style: Arrays.asList(STYLE_ACTIVE, STYLE_INSENSITIVE, STYLE_PRELIGHT, STYLE_NORMAL, STYLE_SELECTED)) { | |
328 | if(group.toUpperCase().equals(style)) { | |
329 | return style; | |
330 | } | |
331 | } | |
332 | return group; | |
333 | } | |
334 | } | |
335 | } |