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