Merge branch 'subtree'
[fanfix.git] / src / be / nikiroo / utils / resources / MetaInfo.java
1 package be.nikiroo.utils.resources;
2
3 import java.util.ArrayList;
4 import java.util.Iterator;
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 */
18 public class MetaInfo<E extends Enum<E>> implements Iterable<MetaInfo<E>> {
19 private final Bundle<E> bundle;
20 private final E id;
21
22 private Meta meta;
23 private List<MetaInfo<E>> children = new ArrayList<MetaInfo<E>>();
24
25 private String value;
26 private List<Runnable> reloadedListeners = new ArrayList<Runnable>();
27 private List<Runnable> saveListeners = new ArrayList<Runnable>();
28
29 private String name;
30 private boolean hidden;
31 private String description;
32
33 private boolean dirty;
34
35 /**
36 * Create a new {@link MetaInfo} from a value (without children).
37 * <p>
38 * For instance, you can call
39 * <tt>new MetaInfo(Config.class, configBundle, Config.MY_VALUE)</tt>.
40 *
41 * @param type
42 * the type of enum the value is
43 * @param bundle
44 * the bundle this value belongs to
45 * @param id
46 * the value itself
47 */
48 public MetaInfo(Class<E> type, Bundle<E> bundle, E id) {
49 this.bundle = bundle;
50 this.id = id;
51
52 try {
53 this.meta = type.getDeclaredField(id.name()).getAnnotation(
54 Meta.class);
55 } catch (NoSuchFieldException e) {
56 } catch (SecurityException e) {
57 }
58
59 // We consider that if a description bundle is used, everything is in it
60
61 String description = null;
62 if (bundle.getDescriptionBundle() != null) {
63 description = bundle.getDescriptionBundle().getString(id);
64 if (description != null && description.trim().isEmpty()) {
65 description = null;
66 }
67 }
68 if (description == null) {
69 description = meta.description();
70 if (description == null) {
71 description = "";
72 }
73 }
74
75 String name = idToName(id, null);
76
77 // Special rules for groups:
78 if (meta.group()) {
79 String groupName = description.split("\n")[0];
80 description = description.substring(groupName.length()).trim();
81 if (!groupName.isEmpty()) {
82 name = groupName;
83 }
84 }
85
86 if (meta.def() != null && !meta.def().isEmpty()) {
87 if (!description.isEmpty()) {
88 description += "\n\n";
89 }
90 description += "(Default value: " + meta.def() + ")";
91 }
92
93 this.name = name;
94 this.hidden = meta.hidden();
95 this.description = description;
96
97 reload();
98 }
99
100 /**
101 * For normal items, this is the name of this item, deduced from its ID (or
102 * in other words, it is the ID but presented in a displayable form).
103 * <p>
104 * For group items, this is the first line of the description if it is not
105 * empty (else, it is the ID in the same way as normal items).
106 * <p>
107 * Never NULL.
108 *
109 *
110 * @return the name, never NULL
111 */
112 public String getName() {
113 return name;
114 }
115
116 /**
117 * This item should be hidden from the user (she will still be able to
118 * modify it if she opens the file manually).
119 *
120 * @return TRUE if it should stay hidden
121 */
122 public boolean isHidden() {
123 return hidden;
124 }
125
126 /**
127 * A description for this item: what it is or does, how to explain that item
128 * to the user including what can be used here (i.e., %s = file name, %d =
129 * file size...).
130 * <p>
131 * For group, the first line ('\\n'-separated) will be used as a title while
132 * the rest will be the description.
133 * <p>
134 * If a default value is known, it will be specified here, too.
135 * <p>
136 * Never NULL.
137 *
138 * @return the description, not NULL
139 */
140 public String getDescription() {
141 return description;
142 }
143
144 /**
145 * The format this item is supposed to follow
146 *
147 * @return the format
148 */
149 public Format getFormat() {
150 return meta.format();
151 }
152
153 /**
154 * The allowed list of values that a {@link Format#FIXED_LIST} item is
155 * allowed to be, or a list of suggestions for {@link Format#COMBO_LIST}
156 * items. Also works for {@link Format#LOCALE}.
157 * <p>
158 * Will always allow an empty string in addition to the rest.
159 *
160 * @return the list of values
161 */
162 public String[] getAllowedValues() {
163 String[] list = meta.list();
164
165 String[] withEmpty = new String[list.length + 1];
166 withEmpty[0] = "";
167 for (int i = 0; i < list.length; i++) {
168 withEmpty[i + 1] = list[i];
169 }
170
171 return withEmpty;
172 }
173
174 /**
175 * Return all the languages known by the program for this bundle.
176 * <p>
177 * This only works for {@link TransBundle}, and will return an empty list if
178 * this is not a {@link TransBundle}.
179 *
180 * @return the known language codes
181 */
182 public List<String> getKnownLanguages() {
183 if (bundle instanceof TransBundle) {
184 return ((TransBundle<E>) bundle).getKnownLanguages();
185 }
186
187 return new ArrayList<String>();
188 }
189
190 /**
191 * This item is a comma-separated list of values instead of a single value.
192 * <p>
193 * The list items are separated by a comma, each surrounded by
194 * double-quotes, with backslashes and double-quotes escaped by a backslash.
195 * <p>
196 * Example: <tt>"un", "deux"</tt>
197 *
198 * @return TRUE if it is
199 */
200 public boolean isArray() {
201 return meta.array();
202 }
203
204 /**
205 * A manual flag to specify if the data has been changed or not, which can
206 * be used by {@link MetaInfo#save(boolean)}.
207 *
208 * @return TRUE if it is dirty (if it has changed)
209 */
210 public boolean isDirty() {
211 return dirty;
212 }
213
214 /**
215 * A manual flag to specify that the data has been changed, which can be
216 * used by {@link MetaInfo#save(boolean)}.
217 */
218 public void setDirty() {
219 this.dirty = true;
220 }
221
222 /**
223 * The number of items in this item if it {@link MetaInfo#isArray()}, or -1
224 * if not.
225 *
226 * @param useDefaultIfEmpty
227 * check the size of the default list instead if the list is
228 * empty
229 *
230 * @return -1 or the number of items
231 */
232 public int getListSize(boolean useDefaultIfEmpty) {
233 if (!isArray()) {
234 return -1;
235 }
236
237 return BundleHelper.getListSize(getString(-1, useDefaultIfEmpty));
238 }
239
240 /**
241 * This item is only used as a group, not as an option.
242 * <p>
243 * For instance, you could have LANGUAGE_CODE as a group for which you won't
244 * use the value in the program, and LANGUAGE_CODE_FR, LANGUAGE_CODE_EN
245 * inside for which the value must be set.
246 *
247 * @return TRUE if it is a group
248 */
249 public boolean isGroup() {
250 return meta.group();
251 }
252
253 /**
254 * The value stored by this item, as a {@link String}.
255 *
256 * @param item
257 * the item number to get for an array of values, or -1 to get
258 * the whole value (has no effect if {@link MetaInfo#isArray()}
259 * is FALSE)
260 * @param useDefaultIfEmpty
261 * use the default value instead of NULL if the setting is not
262 * set
263 *
264 * @return the value
265 */
266 public String getString(int item, boolean useDefaultIfEmpty) {
267 if (isArray() && item >= 0) {
268 List<String> values = BundleHelper.parseList(value, -1);
269 if (values != null && item < values.size()) {
270 return values.get(item);
271 }
272
273 if (useDefaultIfEmpty) {
274 return getDefaultString(item);
275 }
276
277 return null;
278 }
279
280 if (value == null && useDefaultIfEmpty) {
281 return getDefaultString(item);
282 }
283
284 return value;
285 }
286
287 /**
288 * The default value of this item, as a {@link String}.
289 *
290 * @param item
291 * the item number to get for an array of values, or -1 to get
292 * the whole value (has no effect if {@link MetaInfo#isArray()}
293 * is FALSE)
294 *
295 * @return the default value
296 */
297 public String getDefaultString(int item) {
298 if (isArray() && item >= 0) {
299 List<String> values = BundleHelper.parseList(meta.def(), item);
300 if (values != null && item < values.size()) {
301 return values.get(item);
302 }
303
304 return null;
305 }
306
307 return meta.def();
308 }
309
310 /**
311 * The value stored by this item, as a {@link Boolean}.
312 *
313 * @param item
314 * the item number to get for an array of values, or -1 to get
315 * the whole value (has no effect if {@link MetaInfo#isArray()}
316 * is FALSE)
317 * @param useDefaultIfEmpty
318 * use the default value instead of NULL if the setting is not
319 * set
320 *
321 * @return the value
322 */
323 public Boolean getBoolean(int item, boolean useDefaultIfEmpty) {
324 return BundleHelper
325 .parseBoolean(getString(item, useDefaultIfEmpty), -1);
326 }
327
328 /**
329 * The default value of this item, as a {@link Boolean}.
330 *
331 * @param item
332 * the item number to get for an array of values, or -1 to get
333 * the whole value (has no effect if {@link MetaInfo#isArray()}
334 * is FALSE)
335 *
336 * @return the default value
337 */
338 public Boolean getDefaultBoolean(int item) {
339 return BundleHelper.parseBoolean(getDefaultString(item), -1);
340 }
341
342 /**
343 * The value stored by this item, as a {@link Character}.
344 *
345 * @param item
346 * the item number to get for an array of values, or -1 to get
347 * the whole value (has no effect if {@link MetaInfo#isArray()}
348 * is FALSE)
349 * @param useDefaultIfEmpty
350 * use the default value instead of NULL if the setting is not
351 * set
352 *
353 * @return the value
354 */
355 public Character getCharacter(int item, boolean useDefaultIfEmpty) {
356 return BundleHelper.parseCharacter(getString(item, useDefaultIfEmpty),
357 -1);
358 }
359
360 /**
361 * The default value of this item, as a {@link Character}.
362 *
363 * @param item
364 * the item number to get for an array of values, or -1 to get
365 * the whole value (has no effect if {@link MetaInfo#isArray()}
366 * is FALSE)
367 *
368 * @return the default value
369 */
370 public Character getDefaultCharacter(int item) {
371 return BundleHelper.parseCharacter(getDefaultString(item), -1);
372 }
373
374 /**
375 * The value stored by this item, as an {@link Integer}.
376 *
377 * @param item
378 * the item number to get for an array of values, or -1 to get
379 * the whole value (has no effect if {@link MetaInfo#isArray()}
380 * is FALSE)
381 * @param useDefaultIfEmpty
382 * use the default value instead of NULL if the setting is not
383 * set
384 *
385 * @return the value
386 */
387 public Integer getInteger(int item, boolean useDefaultIfEmpty) {
388 return BundleHelper
389 .parseInteger(getString(item, useDefaultIfEmpty), -1);
390 }
391
392 /**
393 * The default value of this item, as an {@link Integer}.
394 *
395 * @param item
396 * the item number to get for an array of values, or -1 to get
397 * the whole value (has no effect if {@link MetaInfo#isArray()}
398 * is FALSE)
399 *
400 * @return the default value
401 */
402 public Integer getDefaultInteger(int item) {
403 return BundleHelper.parseInteger(getDefaultString(item), -1);
404 }
405
406 /**
407 * The value stored by this item, as a colour (represented here as an
408 * {@link Integer}) if it represents a colour, or NULL if it doesn't.
409 * <p>
410 * The returned colour value is an ARGB value.
411 *
412 * @param item
413 * the item number to get for an array of values, or -1 to get
414 * the whole value (has no effect if {@link MetaInfo#isArray()}
415 * is FALSE)
416 * @param useDefaultIfEmpty
417 * use the default value instead of NULL if the setting is not
418 * set
419 *
420 * @return the value
421 */
422 public Integer getColor(int item, boolean useDefaultIfEmpty) {
423 return BundleHelper.parseColor(getString(item, useDefaultIfEmpty), -1);
424 }
425
426 /**
427 * The default value stored by this item, as a colour (represented here as
428 * an {@link Integer}) if it represents a colour, or NULL if it doesn't.
429 * <p>
430 * The returned colour value is an ARGB value.
431 *
432 * @param item
433 * the item number to get for an array of values, or -1 to get
434 * the whole value (has no effect if {@link MetaInfo#isArray()}
435 * is FALSE)
436 *
437 * @return the value
438 */
439 public Integer getDefaultColor(int item) {
440 return BundleHelper.parseColor(getDefaultString(item), -1);
441 }
442
443 /**
444 * A {@link String} representation of the list of values.
445 * <p>
446 * The list of values is comma-separated and each value is surrounded by
447 * double-quotes; backslashes and double-quotes are escaped by a backslash.
448 *
449 * @param item
450 * the item number to get for an array of values, or -1 to get
451 * the whole value (has no effect if {@link MetaInfo#isArray()}
452 * is FALSE)
453 * @param useDefaultIfEmpty
454 * use the default value instead of NULL if the setting is not
455 * set
456 *
457 * @return the value
458 */
459 public List<String> getList(int item, boolean useDefaultIfEmpty) {
460 return BundleHelper.parseList(getString(item, useDefaultIfEmpty), -1);
461 }
462
463 /**
464 * A {@link String} representation of the default list of values.
465 * <p>
466 * The list of values is comma-separated and each value is surrounded by
467 * double-quotes; backslashes and double-quotes are escaped by a backslash.
468 *
469 * @param item
470 * the item number to get for an array of values, or -1 to get
471 * the whole value (has no effect if {@link MetaInfo#isArray()}
472 * is FALSE)
473 *
474 * @return the value
475 */
476 public List<String> getDefaultList(int item) {
477 return BundleHelper.parseList(getDefaultString(item), -1);
478 }
479
480 /**
481 * The value stored by this item, as a {@link String}.
482 *
483 * @param value
484 * the new value
485 * @param item
486 * the item number to set for an array of values, or -1 to set
487 * the whole value (has no effect if {@link MetaInfo#isArray()}
488 * is FALSE)
489 */
490 public void setString(String value, int item) {
491 if (isArray() && item >= 0) {
492 this.value = BundleHelper.fromList(this.value, value, item);
493 } else {
494 this.value = value;
495 }
496 }
497
498 /**
499 * The value stored by this item, as a {@link Boolean}.
500 *
501 * @param value
502 * the new value
503 * @param item
504 * the item number to set for an array of values, or -1 to set
505 * the whole value (has no effect if {@link MetaInfo#isArray()}
506 * is FALSE)
507 */
508 public void setBoolean(boolean value, int item) {
509 setString(BundleHelper.fromBoolean(value), item);
510 }
511
512 /**
513 * The value stored by this item, as a {@link Character}.
514 *
515 * @param value
516 * the new value
517 * @param item
518 * the item number to set for an array of values, or -1 to set
519 * the whole value (has no effect if {@link MetaInfo#isArray()}
520 * is FALSE)
521 */
522 public void setCharacter(char value, int item) {
523 setString(BundleHelper.fromCharacter(value), item);
524 }
525
526 /**
527 * The value stored by this item, as an {@link Integer}.
528 *
529 * @param value
530 * the new value
531 * @param item
532 * the item number to set for an array of values, or -1 to set
533 * the whole value (has no effect if {@link MetaInfo#isArray()}
534 * is FALSE)
535 */
536 public void setInteger(int value, int item) {
537 setString(BundleHelper.fromInteger(value), item);
538 }
539
540 /**
541 * The value stored by this item, as a colour (represented here as an
542 * {@link Integer}) if it represents a colour, or NULL if it doesn't.
543 * <p>
544 * The colour value is an ARGB value.
545 *
546 * @param value
547 * the value
548 * @param item
549 * the item number to set for an array of values, or -1 to set
550 * the whole value (has no effect if {@link MetaInfo#isArray()}
551 * is FALSE)
552 */
553 public void setColor(int value, int item) {
554 setString(BundleHelper.fromColor(value), item);
555 }
556
557 /**
558 * A {@link String} representation of the default list of values.
559 * <p>
560 * The list of values is comma-separated and each value is surrounded by
561 * double-quotes; backslashes and double-quotes are escaped by a backslash.
562 *
563 * @param value
564 * the {@link String} representation
565 * @param item
566 * the item number to set for an array of values, or -1 to set
567 * the whole value (has no effect if {@link MetaInfo#isArray()}
568 * is FALSE)
569 */
570 public void setList(List<String> value, int item) {
571 setString(BundleHelper.fromList(value), item);
572 }
573
574 /**
575 * Reload the value from the {@link Bundle}, so the last value that was
576 * saved will be used.
577 */
578 public void reload() {
579 if (bundle.isSet(id, false)) {
580 value = bundle.getString(id);
581 } else {
582 value = null;
583 }
584
585 // Copy the list so we can create new listener in a listener
586 for (Runnable listener : new ArrayList<Runnable>(reloadedListeners)) {
587 try {
588 listener.run();
589 } catch (Exception e) {
590 e.printStackTrace();
591 }
592 }
593 }
594
595 /**
596 * Add a listener that will be called <b>after</b> a reload operation.
597 * <p>
598 * You could use it to refresh the UI for instance.
599 *
600 * @param listener
601 * the listener
602 */
603 public void addReloadedListener(Runnable listener) {
604 reloadedListeners.add(listener);
605 }
606
607 /**
608 * Save the current value to the {@link Bundle}.
609 * <p>
610 * Note that listeners will be called <b>before</b> the dirty check and
611 * <b>before</b> saving the value.
612 *
613 * @param onlyIfDirty
614 * only save the data if the dirty flag is set (will reset the
615 * dirty flag)
616 */
617 public void save(boolean onlyIfDirty) {
618 // Copy the list so we can create new listener in a listener
619 for (Runnable listener : new ArrayList<Runnable>(saveListeners)) {
620 try {
621 listener.run();
622 } catch (Exception e) {
623 e.printStackTrace();
624 }
625 }
626
627 if (!onlyIfDirty || isDirty()) {
628 bundle.setString(id, value);
629 }
630 }
631
632 /**
633 * Add a listener that will be called <b>before</b> a save operation.
634 * <p>
635 * You could use it to make some modification to the stored value before it
636 * is saved.
637 *
638 * @param listener
639 * the listener
640 */
641 public void addSaveListener(Runnable listener) {
642 saveListeners.add(listener);
643 }
644
645 /**
646 * The sub-items if any (if no sub-items, will return an empty list).
647 * <p>
648 * Sub-items are declared when a {@link Meta} has an ID that starts with the
649 * ID of a {@link Meta#group()} {@link MetaInfo}.
650 * <p>
651 * For instance:
652 * <ul>
653 * <li>{@link Meta} <tt>MY_PREFIX</tt> is a {@link Meta#group()}</li>
654 * <li>{@link Meta} <tt>MY_PREFIX_DESCRIPTION</tt> is another {@link Meta}</li>
655 * <li><tt>MY_PREFIX_DESCRIPTION</tt> will be a child of <tt>MY_PREFIX</tt></li>
656 * </ul>
657 *
658 * @return the sub-items if any
659 */
660 public List<MetaInfo<E>> getChildren() {
661 return children;
662 }
663
664 /**
665 * The number of sub-items, if any.
666 *
667 * @return the number or 0
668 */
669 public int size() {
670 return children.size();
671 }
672
673 @Override
674 public Iterator<MetaInfo<E>> iterator() {
675 return children.iterator();
676 }
677
678 /**
679 * Create a list of {@link MetaInfo}, one for each of the item in the given
680 * {@link Bundle}.
681 *
682 * @param <E>
683 * the type of {@link Bundle} to edit
684 * @param type
685 * a class instance of the item type to work on
686 * @param bundle
687 * the {@link Bundle} to sort through
688 *
689 * @return the list
690 */
691 static public <E extends Enum<E>> List<MetaInfo<E>> getItems(Class<E> type,
692 Bundle<E> bundle) {
693 List<MetaInfo<E>> list = new ArrayList<MetaInfo<E>>();
694 List<MetaInfo<E>> shadow = new ArrayList<MetaInfo<E>>();
695 for (E id : type.getEnumConstants()) {
696 MetaInfo<E> info = new MetaInfo<E>(type, bundle, id);
697 if (!info.hidden) {
698 list.add(info);
699 shadow.add(info);
700 }
701 }
702
703 for (int i = 0; i < list.size(); i++) {
704 MetaInfo<E> info = list.get(i);
705
706 MetaInfo<E> parent = findParent(info, shadow);
707 if (parent != null) {
708 list.remove(i--);
709 parent.children.add(info);
710 info.name = idToName(info.id, parent.id);
711 }
712 }
713
714 return list;
715 }
716
717 /**
718 * Find the longest parent of the given {@link MetaInfo}, which means:
719 * <ul>
720 * <li>the parent is a {@link Meta#group()}</li>
721 * <li>the parent Id is a substring of the Id of the given {@link MetaInfo}</li>
722 * <li>there is no other parent sharing a substring for this
723 * {@link MetaInfo} with a longer Id</li>
724 * </ul>
725 *
726 * @param <E>
727 * the kind of enum
728 * @param info
729 * the info to look for a parent for
730 * @param candidates
731 * the list of potential parents
732 *
733 * @return the longest parent or NULL if no parent is found
734 */
735 static private <E extends Enum<E>> MetaInfo<E> findParent(MetaInfo<E> info,
736 List<MetaInfo<E>> candidates) {
737 String id = info.id.toString();
738 MetaInfo<E> group = null;
739 for (MetaInfo<E> pcandidate : candidates) {
740 if (pcandidate.isGroup()) {
741 String candidateId = pcandidate.id.toString();
742 if (!id.equals(candidateId) && id.startsWith(candidateId)) {
743 if (group == null
744 || group.id.toString().length() < candidateId
745 .length()) {
746 group = pcandidate;
747 }
748 }
749 }
750 }
751
752 return group;
753 }
754
755 static private <E extends Enum<E>> String idToName(E id, E prefix) {
756 String name = id.toString();
757 if (prefix != null && name.startsWith(prefix.toString())) {
758 name = name.substring(prefix.toString().length());
759 }
760
761 if (name.length() > 0) {
762 name = name.substring(0, 1).toUpperCase()
763 + name.substring(1).toLowerCase();
764 }
765
766 name = name.replace("_", " ");
767
768 return name.trim();
769 }
770 }