Commit | Line | Data |
---|---|---|
9e834013 NR |
1 | package be.nikiroo.utils.resources; |
2 | ||
3 | import java.util.ArrayList; | |
0877d6f5 | 4 | import java.util.Iterator; |
9e834013 NR |
5 | import java.util.List; |
6 | ||
7 | import be.nikiroo.utils.resources.Meta.Format; | |
8 | ||
9 | /** | |
10 | * A graphical item that reflect a configuration option from the given | |
11 | * {@link Bundle}. | |
12 | * | |
13 | * @author niki | |
14 | * | |
15 | * @param <E> | |
16 | * the type of {@link Bundle} to edit | |
17 | */ | |
0877d6f5 | 18 | public class MetaInfo<E extends Enum<E>> implements Iterable<MetaInfo<E>> { |
9e834013 NR |
19 | private final Bundle<E> bundle; |
20 | private final E id; | |
21 | ||
22 | private Meta meta; | |
0877d6f5 | 23 | private List<MetaInfo<E>> children = new ArrayList<MetaInfo<E>>(); |
9e834013 NR |
24 | |
25 | private String value; | |
8517b60c NR |
26 | private List<Runnable> reloadedListeners = new ArrayList<Runnable>(); |
27 | private List<Runnable> saveListeners = new ArrayList<Runnable>(); | |
9e834013 NR |
28 | |
29 | private String name; | |
30 | private String description; | |
31 | ||
76b51de9 NR |
32 | /** |
33 | * Create a new {@link MetaInfo} from a value (without children). | |
34 | * <p> | |
35 | * For instance, you can call | |
36 | * <tt>new MetaInfo(Config.class, configBundle, Config.MY_VALUE)</tt>. | |
37 | * | |
38 | * @param type | |
39 | * the type of enum the value is | |
40 | * @param bundle | |
41 | * the bundle this value belongs to | |
42 | * @param id | |
43 | * the value itself | |
44 | */ | |
9e834013 NR |
45 | public MetaInfo(Class<E> type, Bundle<E> bundle, E id) { |
46 | this.bundle = bundle; | |
47 | this.id = id; | |
48 | ||
49 | try { | |
50 | this.meta = type.getDeclaredField(id.name()).getAnnotation( | |
51 | Meta.class); | |
52 | } catch (NoSuchFieldException e) { | |
53 | } catch (SecurityException e) { | |
54 | } | |
55 | ||
56 | // We consider that if a description bundle is used, everything is in it | |
57 | ||
58 | String description = null; | |
59 | if (bundle.getDescriptionBundle() != null) { | |
60 | description = bundle.getDescriptionBundle().getString(id); | |
61 | if (description != null && description.trim().isEmpty()) { | |
62 | description = null; | |
63 | } | |
64 | } | |
9e834013 NR |
65 | if (description == null) { |
66 | description = meta.description(); | |
0877d6f5 NR |
67 | if (description == null) { |
68 | description = ""; | |
69 | } | |
d18e136e NR |
70 | } |
71 | ||
72 | String name = idToName(id, null); | |
73 | ||
74 | // Special rules for groups: | |
75 | if (meta.group()) { | |
76 | String groupName = description.split("\n")[0]; | |
77 | description = description.substring(groupName.length()).trim(); | |
78 | if (!groupName.isEmpty()) { | |
79 | name = groupName; | |
9e834013 NR |
80 | } |
81 | } | |
82 | ||
d18e136e NR |
83 | if (meta.def() != null && !meta.def().isEmpty()) { |
84 | if (!description.isEmpty()) { | |
85 | description += "\n\n"; | |
86 | } | |
87 | description += "(Default value: " + meta.def() + ")"; | |
9e834013 NR |
88 | } |
89 | ||
90 | this.name = name; | |
91 | this.description = description; | |
92 | ||
93 | reload(); | |
94 | } | |
95 | ||
96 | /** | |
d18e136e NR |
97 | * For normal items, this is the name of this item, deduced from its ID (or |
98 | * in other words, it is the ID but presented in a displayable form). | |
99 | * <p> | |
100 | * For group items, this is the first line of the description if it is not | |
101 | * empty (else, it is the ID in the same way as normal items). | |
9e834013 | 102 | * <p> |
d18e136e | 103 | * Never NULL. |
9e834013 | 104 | * |
d18e136e NR |
105 | * |
106 | * @return the name, never NULL | |
9e834013 NR |
107 | */ |
108 | public String getName() { | |
109 | return name; | |
110 | } | |
111 | ||
112 | /** | |
d18e136e NR |
113 | * A description for this item: what it is or does, how to explain that item |
114 | * to the user including what can be used here (i.e., %s = file name, %d = | |
115 | * file size...). | |
116 | * <p> | |
117 | * For group, the first line ('\\n'-separated) will be used as a title while | |
118 | * the rest will be the description. | |
119 | * <p> | |
120 | * If a default value is known, it will be specified here, too. | |
121 | * <p> | |
122 | * Never NULL. | |
9e834013 | 123 | * |
d18e136e | 124 | * @return the description, not NULL |
9e834013 NR |
125 | */ |
126 | public String getDescription() { | |
127 | return description; | |
128 | } | |
129 | ||
76b51de9 NR |
130 | /** |
131 | * The format this item is supposed to follow | |
132 | * | |
133 | * @return the format | |
134 | */ | |
9e834013 NR |
135 | public Format getFormat() { |
136 | return meta.format(); | |
137 | } | |
138 | ||
76b51de9 NR |
139 | /** |
140 | * The allowed list of values that a {@link Format#FIXED_LIST} item is | |
141 | * allowed to be, or a list of suggestions for {@link Format#COMBO_LIST} | |
142 | * items. | |
d18e136e NR |
143 | * <p> |
144 | * Will always allow an empty string in addition to the rest. | |
76b51de9 NR |
145 | * |
146 | * @return the list of values | |
147 | */ | |
0877d6f5 | 148 | public String[] getAllowedValues() { |
d18e136e NR |
149 | String[] list = meta.list(); |
150 | ||
151 | String[] withEmpty = new String[list.length + 1]; | |
152 | withEmpty[0] = ""; | |
153 | for (int i = 0; i < list.length; i++) { | |
154 | withEmpty[i + 1] = list[i]; | |
155 | } | |
156 | ||
157 | return withEmpty; | |
0877d6f5 NR |
158 | } |
159 | ||
76b51de9 NR |
160 | /** |
161 | * This item is a comma-separated list of values instead of a single value. | |
162 | * <p> | |
163 | * The list items are separated by a comma, each surrounded by | |
164 | * double-quotes, with backslashes and double-quotes escaped by a backslash. | |
165 | * <p> | |
166 | * Example: <tt>"un", "deux"</tt> | |
167 | * | |
168 | * @return TRUE if it is | |
169 | */ | |
0877d6f5 NR |
170 | public boolean isArray() { |
171 | return meta.array(); | |
172 | } | |
173 | ||
76b51de9 NR |
174 | /** |
175 | * This item is only used as a group, not as an option. | |
176 | * <p> | |
177 | * For instance, you could have LANGUAGE_CODE as a group for which you won't | |
178 | * use the value in the program, and LANGUAGE_CODE_FR, LANGUAGE_CODE_EN | |
179 | * inside for which the value must be set. | |
180 | * | |
181 | * @return TRUE if it is a group | |
182 | */ | |
183 | public boolean isGroup() { | |
184 | return meta.group(); | |
185 | } | |
186 | ||
9e834013 NR |
187 | /** |
188 | * The value stored by this item, as a {@link String}. | |
189 | * | |
d18e136e NR |
190 | * @param useDefaultIfEmpty |
191 | * use the default value instead of NULL if the setting is not | |
192 | * set | |
193 | * | |
9e834013 NR |
194 | * @return the value |
195 | */ | |
d18e136e NR |
196 | public String getString(boolean useDefaultIfEmpty) { |
197 | if (value == null && useDefaultIfEmpty) { | |
198 | return getDefaultString(); | |
199 | } | |
200 | ||
9e834013 NR |
201 | return value; |
202 | } | |
203 | ||
76b51de9 NR |
204 | /** |
205 | * The default value of this item, as a {@link String}. | |
206 | * | |
207 | * @return the default value | |
208 | */ | |
9e834013 NR |
209 | public String getDefaultString() { |
210 | return meta.def(); | |
211 | } | |
212 | ||
76b51de9 NR |
213 | /** |
214 | * The value stored by this item, as a {@link Boolean}. | |
215 | * | |
d18e136e NR |
216 | * @param useDefaultIfEmpty |
217 | * use the default value instead of NULL if the setting is not | |
218 | * set | |
219 | * | |
76b51de9 NR |
220 | * @return the value |
221 | */ | |
d18e136e NR |
222 | public Boolean getBoolean(boolean useDefaultIfEmpty) { |
223 | return BundleHelper.parseBoolean(getString(useDefaultIfEmpty)); | |
9e834013 NR |
224 | } |
225 | ||
76b51de9 NR |
226 | /** |
227 | * The default value of this item, as a {@link Boolean}. | |
228 | * | |
229 | * @return the default value | |
230 | */ | |
9e834013 NR |
231 | public Boolean getDefaultBoolean() { |
232 | return BundleHelper.parseBoolean(getDefaultString()); | |
233 | } | |
234 | ||
76b51de9 NR |
235 | /** |
236 | * The value stored by this item, as a {@link Character}. | |
237 | * | |
d18e136e NR |
238 | * @param useDefaultIfEmpty |
239 | * use the default value instead of NULL if the setting is not | |
240 | * set | |
241 | * | |
76b51de9 NR |
242 | * @return the value |
243 | */ | |
d18e136e NR |
244 | public Character getCharacter(boolean useDefaultIfEmpty) { |
245 | return BundleHelper.parseCharacter(getString(useDefaultIfEmpty)); | |
9e834013 NR |
246 | } |
247 | ||
76b51de9 NR |
248 | /** |
249 | * The default value of this item, as a {@link Character}. | |
250 | * | |
251 | * @return the default value | |
252 | */ | |
9e834013 NR |
253 | public Character getDefaultCharacter() { |
254 | return BundleHelper.parseCharacter(getDefaultString()); | |
255 | } | |
256 | ||
76b51de9 NR |
257 | /** |
258 | * The value stored by this item, as an {@link Integer}. | |
259 | * | |
d18e136e NR |
260 | * @param useDefaultIfEmpty |
261 | * use the default value instead of NULL if the setting is not | |
262 | * set | |
263 | * | |
76b51de9 NR |
264 | * @return the value |
265 | */ | |
d18e136e NR |
266 | public Integer getInteger(boolean useDefaultIfEmpty) { |
267 | return BundleHelper.parseInteger(getString(useDefaultIfEmpty)); | |
9e834013 NR |
268 | } |
269 | ||
76b51de9 NR |
270 | /** |
271 | * The default value of this item, as an {@link Integer}. | |
272 | * | |
273 | * @return the default value | |
274 | */ | |
9e834013 NR |
275 | public Integer getDefaultInteger() { |
276 | return BundleHelper.parseInteger(getDefaultString()); | |
277 | } | |
278 | ||
76b51de9 NR |
279 | /** |
280 | * The value stored by this item, as a colour (represented here as an | |
281 | * {@link Integer}) if it represents a colour, or NULL if it doesn't. | |
282 | * <p> | |
283 | * The returned colour value is an ARGB value. | |
284 | * | |
d18e136e NR |
285 | * @param useDefaultIfEmpty |
286 | * use the default value instead of NULL if the setting is not | |
287 | * set | |
288 | * | |
76b51de9 NR |
289 | * @return the value |
290 | */ | |
d18e136e NR |
291 | public Integer getColor(boolean useDefaultIfEmpty) { |
292 | return BundleHelper.parseColor(getString(useDefaultIfEmpty)); | |
9e834013 NR |
293 | } |
294 | ||
76b51de9 NR |
295 | /** |
296 | * The default value stored by this item, as a colour (represented here as | |
297 | * an {@link Integer}) if it represents a colour, or NULL if it doesn't. | |
298 | * <p> | |
299 | * The returned colour value is an ARGB value. | |
300 | * | |
301 | * @return the value | |
302 | */ | |
9e834013 NR |
303 | public Integer getDefaultColor() { |
304 | return BundleHelper.parseColor(getDefaultString()); | |
305 | } | |
306 | ||
76b51de9 NR |
307 | /** |
308 | * A {@link String} representation of the list of values. | |
309 | * <p> | |
310 | * The list of values is comma-separated and each value is surrounded by | |
311 | * double-quotes; backslashes and double-quotes are escaped by a backslash. | |
312 | * | |
d18e136e NR |
313 | * @param useDefaultIfEmpty |
314 | * use the default value instead of NULL if the setting is not | |
315 | * set | |
316 | * | |
76b51de9 NR |
317 | * @return the value |
318 | */ | |
d18e136e NR |
319 | public List<String> getList(boolean useDefaultIfEmpty) { |
320 | return BundleHelper.parseList(getString(useDefaultIfEmpty)); | |
9e834013 NR |
321 | } |
322 | ||
76b51de9 NR |
323 | /** |
324 | * A {@link String} representation of the default list of values. | |
325 | * <p> | |
326 | * The list of values is comma-separated and each value is surrounded by | |
327 | * double-quotes; backslashes and double-quotes are escaped by a backslash. | |
328 | * | |
329 | * @return the value | |
330 | */ | |
9e834013 NR |
331 | public List<String> getDefaultList() { |
332 | return BundleHelper.parseList(getDefaultString()); | |
333 | } | |
334 | ||
335 | /** | |
336 | * The value stored by this item, as a {@link String}. | |
337 | * | |
338 | * @param value | |
339 | * the new value | |
340 | */ | |
341 | public void setString(String value) { | |
342 | this.value = value; | |
343 | } | |
344 | ||
76b51de9 NR |
345 | /** |
346 | * The value stored by this item, as a {@link Boolean}. | |
347 | * | |
348 | * @param value | |
349 | * the new value | |
350 | */ | |
9e834013 NR |
351 | public void setBoolean(boolean value) { |
352 | setString(BundleHelper.fromBoolean(value)); | |
353 | } | |
354 | ||
76b51de9 NR |
355 | /** |
356 | * The value stored by this item, as a {@link Character}. | |
357 | * | |
358 | * @param value | |
359 | * the new value | |
360 | */ | |
9e834013 NR |
361 | public void setCharacter(char value) { |
362 | setString(BundleHelper.fromCharacter(value)); | |
363 | } | |
364 | ||
76b51de9 NR |
365 | /** |
366 | * The value stored by this item, as an {@link Integer}. | |
367 | * | |
368 | * @param value | |
369 | * the new value | |
370 | */ | |
9e834013 NR |
371 | public void setInteger(int value) { |
372 | setString(BundleHelper.fromInteger(value)); | |
373 | } | |
374 | ||
76b51de9 NR |
375 | /** |
376 | * The value stored by this item, as a colour (represented here as an | |
377 | * {@link Integer}) if it represents a colour, or NULL if it doesn't. | |
378 | * <p> | |
379 | * The returned colour value is an ARGB value. | |
380 | * | |
381 | * @param value | |
382 | * the value | |
383 | */ | |
9e834013 NR |
384 | public void setColor(int value) { |
385 | setString(BundleHelper.fromColor(value)); | |
386 | } | |
387 | ||
76b51de9 NR |
388 | /** |
389 | * A {@link String} representation of the default list of values. | |
390 | * <p> | |
391 | * The list of values is comma-separated and each value is surrounded by | |
392 | * double-quotes; backslashes and double-quotes are escaped by a backslash. | |
393 | * | |
394 | * @param value | |
395 | * the {@link String} representation | |
396 | * | |
397 | */ | |
9e834013 NR |
398 | public void setList(List<String> value) { |
399 | setString(BundleHelper.fromList(value)); | |
400 | } | |
401 | ||
402 | /** | |
76b51de9 NR |
403 | * Reload the value from the {@link Bundle}, so the last value that was |
404 | * saved will be used. | |
9e834013 NR |
405 | */ |
406 | public void reload() { | |
fde375c1 NR |
407 | if (bundle.isSet(id, false)) { |
408 | value = bundle.getString(id); | |
409 | } else { | |
410 | value = null; | |
411 | } | |
412 | ||
8517b60c | 413 | for (Runnable listener : reloadedListeners) { |
9e834013 NR |
414 | try { |
415 | listener.run(); | |
416 | } catch (Exception e) { | |
417 | // TODO: error management? | |
418 | e.printStackTrace(); | |
419 | } | |
420 | } | |
421 | } | |
422 | ||
76b51de9 NR |
423 | /** |
424 | * Add a listener that will be called <b>after</b> a reload operation. | |
425 | * <p> | |
426 | * You could use it to refresh the UI for instance. | |
427 | * | |
428 | * @param listener | |
429 | * the listener | |
430 | */ | |
8517b60c NR |
431 | public void addReloadedListener(Runnable listener) { |
432 | reloadedListeners.add(listener); | |
9e834013 NR |
433 | } |
434 | ||
435 | /** | |
436 | * Save the current value to the {@link Bundle}. | |
437 | */ | |
438 | public void save() { | |
8517b60c NR |
439 | for (Runnable listener : saveListeners) { |
440 | try { | |
441 | listener.run(); | |
442 | } catch (Exception e) { | |
443 | // TODO: error management? | |
444 | e.printStackTrace(); | |
445 | } | |
446 | } | |
9e834013 NR |
447 | bundle.setString(id, value); |
448 | } | |
449 | ||
76b51de9 NR |
450 | /** |
451 | * Add a listener that will be called <b>before</b> a save operation. | |
452 | * <p> | |
453 | * You could use it to make some modification to the stored value before it | |
454 | * is saved. | |
455 | * | |
456 | * @param listener | |
457 | * the listener | |
458 | */ | |
8517b60c NR |
459 | public void addSaveListener(Runnable listener) { |
460 | saveListeners.add(listener); | |
461 | } | |
462 | ||
76b51de9 NR |
463 | /** |
464 | * The sub-items if any (if no sub-items, will return an empty list). | |
465 | * <p> | |
466 | * Sub-items are declared when a {@link Meta} has an ID that starts with the | |
467 | * ID of a {@link Meta#group()} {@link MetaInfo}. | |
468 | * <p> | |
469 | * For instance: | |
470 | * <ul> | |
471 | * <li>{@link Meta} <tt>MY_PREFIX</tt> is a {@link Meta#group()}</li> | |
472 | * <li>{@link Meta} <tt>MY_PREFIX_DESCRIPTION</tt> is another {@link Meta}</li> | |
473 | * <li><tt>MY_PREFIX_DESCRIPTION</tt> will be a child of <tt>MY_PREFIX</tt></li> | |
474 | * </ul> | |
475 | * | |
476 | * @return the sub-items if any | |
477 | */ | |
478 | public List<MetaInfo<E>> getChildren() { | |
479 | return children; | |
480 | } | |
481 | ||
0877d6f5 NR |
482 | @Override |
483 | public Iterator<MetaInfo<E>> iterator() { | |
484 | return children.iterator(); | |
485 | } | |
486 | ||
9e834013 NR |
487 | /** |
488 | * Create a list of {@link MetaInfo}, one for each of the item in the given | |
489 | * {@link Bundle}. | |
490 | * | |
491 | * @param <E> | |
492 | * the type of {@link Bundle} to edit | |
493 | * @param type | |
494 | * a class instance of the item type to work on | |
495 | * @param bundle | |
496 | * the {@link Bundle} to sort through | |
497 | * | |
498 | * @return the list | |
499 | */ | |
500 | static public <E extends Enum<E>> List<MetaInfo<E>> getItems(Class<E> type, | |
501 | Bundle<E> bundle) { | |
502 | List<MetaInfo<E>> list = new ArrayList<MetaInfo<E>>(); | |
76b51de9 | 503 | List<MetaInfo<E>> shadow = new ArrayList<MetaInfo<E>>(); |
9e834013 | 504 | for (E id : type.getEnumConstants()) { |
76b51de9 NR |
505 | MetaInfo<E> info = new MetaInfo<E>(type, bundle, id); |
506 | list.add(info); | |
507 | shadow.add(info); | |
9e834013 NR |
508 | } |
509 | ||
76b51de9 NR |
510 | for (int i = 0; i < list.size(); i++) { |
511 | MetaInfo<E> info = list.get(i); | |
8517b60c | 512 | |
76b51de9 NR |
513 | MetaInfo<E> parent = findParent(info, shadow); |
514 | if (parent != null) { | |
515 | list.remove(i--); | |
516 | parent.children.add(info); | |
d18e136e | 517 | info.name = idToName(info.id, parent.id); |
8517b60c NR |
518 | } |
519 | } | |
520 | ||
76b51de9 | 521 | return list; |
8517b60c NR |
522 | } |
523 | ||
76b51de9 NR |
524 | /** |
525 | * Find the longest parent of the given {@link MetaInfo}, which means: | |
526 | * <ul> | |
527 | * <li>the parent is a {@link Meta#group()}</li> | |
528 | * <li>the parent Id is a substring of the Id of the given {@link MetaInfo}</li> | |
529 | * <li>there is no other parent sharing a substring for this | |
530 | * {@link MetaInfo} with a longer Id</li> | |
531 | * </ul> | |
532 | * | |
533 | * @param <E> | |
534 | * the kind of enum | |
535 | * @param info | |
536 | * the info to look for a parent for | |
537 | * @param candidates | |
538 | * the list of potential parents | |
539 | * | |
540 | * @return the longest parent or NULL if no parent is found | |
541 | */ | |
8517b60c | 542 | static private <E extends Enum<E>> MetaInfo<E> findParent(MetaInfo<E> info, |
76b51de9 NR |
543 | List<MetaInfo<E>> candidates) { |
544 | String id = info.id.toString(); | |
8517b60c NR |
545 | MetaInfo<E> group = null; |
546 | for (MetaInfo<E> pcandidate : candidates) { | |
76b51de9 NR |
547 | if (pcandidate.isGroup()) { |
548 | String candidateId = pcandidate.id.toString(); | |
549 | if (!id.equals(candidateId) && id.startsWith(candidateId)) { | |
550 | if (group == null | |
551 | || group.id.toString().length() < candidateId | |
552 | .length()) { | |
553 | group = pcandidate; | |
554 | } | |
8517b60c NR |
555 | } |
556 | } | |
557 | } | |
558 | ||
559 | return group; | |
560 | } | |
d18e136e NR |
561 | |
562 | static private <E extends Enum<E>> String idToName(E id, E prefix) { | |
563 | String name = id.toString(); | |
564 | if (prefix != null && name.startsWith(prefix.toString())) { | |
565 | name = name.substring(prefix.toString().length()); | |
566 | } | |
567 | ||
568 | if (name.length() > 0) { | |
569 | name = name.substring(0, 1).toUpperCase() | |
570 | + name.substring(1).toLowerCase(); | |
571 | } | |
572 | ||
573 | name = name.replace("_", " "); | |
574 | ||
575 | return name.trim(); | |
576 | } | |
9e834013 | 577 | } |