1 package be
.nikiroo
.utils
.resources
;
3 import java
.util
.ArrayList
;
4 import java
.util
.Iterator
;
7 import be
.nikiroo
.utils
.resources
.Meta
.Format
;
10 * A graphical item that reflect a configuration option from the given
16 * the type of {@link Bundle} to edit
18 public class MetaInfo
<E
extends Enum
<E
>> implements Iterable
<MetaInfo
<E
>> {
19 private final Bundle
<E
> bundle
;
23 private List
<MetaInfo
<E
>> children
= new ArrayList
<MetaInfo
<E
>>();
26 private List
<Runnable
> reloadedListeners
= new ArrayList
<Runnable
>();
27 private List
<Runnable
> saveListeners
= new ArrayList
<Runnable
>();
30 private boolean hidden
;
31 private String description
;
33 private boolean dirty
;
36 * Create a new {@link MetaInfo} from a value (without children).
38 * For instance, you can call
39 * <tt>new MetaInfo(Config.class, configBundle, Config.MY_VALUE)</tt>.
42 * the type of enum the value is
44 * the bundle this value belongs to
48 public MetaInfo(Class
<E
> type
, Bundle
<E
> bundle
, E id
) {
53 this.meta
= type
.getDeclaredField(id
.name()).getAnnotation(
55 } catch (NoSuchFieldException e
) {
56 } catch (SecurityException e
) {
59 // We consider that if a description bundle is used, everything is in it
61 String description
= null;
62 if (bundle
.getDescriptionBundle() != null) {
63 description
= bundle
.getDescriptionBundle().getString(id
);
64 if (description
!= null && description
.trim().isEmpty()) {
68 if (description
== null) {
69 description
= meta
.description();
70 if (description
== null) {
75 String name
= idToName(id
, null);
77 // Special rules for groups:
79 String groupName
= description
.split("\n")[0];
80 description
= description
.substring(groupName
.length()).trim();
81 if (!groupName
.isEmpty()) {
86 if (meta
.def() != null && !meta
.def().isEmpty()) {
87 if (!description
.isEmpty()) {
88 description
+= "\n\n";
90 description
+= "(Default value: " + meta
.def() + ")";
94 this.hidden
= meta
.hidden();
95 this.description
= description
;
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).
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).
110 * @return the name, never NULL
112 public String
getName() {
117 * This item should be hidden from the user (she will still be able to
118 * modify it if she opens the file manually).
120 * @return TRUE if it should stay hidden
122 public boolean isHidden() {
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 =
131 * For group, the first line ('\\n'-separated) will be used as a title while
132 * the rest will be the description.
134 * If a default value is known, it will be specified here, too.
138 * @return the description, not NULL
140 public String
getDescription() {
145 * The format this item is supposed to follow
149 public Format
getFormat() {
150 return meta
.format();
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}.
158 * Will always allow an empty string in addition to the rest.
160 * @return the list of values
162 public String
[] getAllowedValues() {
163 String
[] list
= meta
.list();
165 String
[] withEmpty
= new String
[list
.length
+ 1];
167 for (int i
= 0; i
< list
.length
; i
++) {
168 withEmpty
[i
+ 1] = list
[i
];
175 * Return all the languages known by the program for this bundle.
177 * This only works for {@link TransBundle}, and will return an empty list if
178 * this is not a {@link TransBundle}.
180 * @return the known language codes
182 public List
<String
> getKnownLanguages() {
183 if (bundle
instanceof TransBundle
) {
184 return ((TransBundle
<E
>) bundle
).getKnownLanguages();
187 return new ArrayList
<String
>();
191 * This item is a comma-separated list of values instead of a single value.
193 * The list items are separated by a comma, each surrounded by
194 * double-quotes, with backslashes and double-quotes escaped by a backslash.
196 * Example: <tt>"un", "deux"</tt>
198 * @return TRUE if it is
200 public boolean isArray() {
205 * A manual flag to specify if the data has been changed or not, which can
206 * be used by {@link MetaInfo#save(boolean)}.
208 * @return TRUE if it is dirty (if it has changed)
210 public boolean isDirty() {
215 * A manual flag to specify that the data has been changed, which can be
216 * used by {@link MetaInfo#save(boolean)}.
218 public void setDirty() {
223 * The number of items in this item if it {@link MetaInfo#isArray()}, or -1
226 * @param useDefaultIfEmpty
227 * check the size of the default list instead if the list is
230 * @return -1 or the number of items
232 public int getListSize(boolean useDefaultIfEmpty
) {
237 return BundleHelper
.getListSize(getString(-1, useDefaultIfEmpty
));
241 * This item is only used as a group, not as an option.
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.
247 * @return TRUE if it is a group
249 public boolean isGroup() {
254 * The value stored by this item, as a {@link String}.
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()}
260 * @param useDefaultIfEmpty
261 * use the default value instead of NULL if the setting is not
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
);
273 if (useDefaultIfEmpty
) {
274 return getDefaultString(item
);
280 if (value
== null && useDefaultIfEmpty
) {
281 return getDefaultString(item
);
288 * The default value of this item, as a {@link String}.
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()}
295 * @return the default value
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
);
311 * The value stored by this item, as a {@link Boolean}.
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()}
317 * @param useDefaultIfEmpty
318 * use the default value instead of NULL if the setting is not
323 public Boolean
getBoolean(int item
, boolean useDefaultIfEmpty
) {
325 .parseBoolean(getString(item
, useDefaultIfEmpty
), -1);
329 * The default value of this item, as a {@link Boolean}.
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()}
336 * @return the default value
338 public Boolean
getDefaultBoolean(int item
) {
339 return BundleHelper
.parseBoolean(getDefaultString(item
), -1);
343 * The value stored by this item, as a {@link Character}.
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()}
349 * @param useDefaultIfEmpty
350 * use the default value instead of NULL if the setting is not
355 public Character
getCharacter(int item
, boolean useDefaultIfEmpty
) {
356 return BundleHelper
.parseCharacter(getString(item
, useDefaultIfEmpty
),
361 * The default value of this item, as a {@link Character}.
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()}
368 * @return the default value
370 public Character
getDefaultCharacter(int item
) {
371 return BundleHelper
.parseCharacter(getDefaultString(item
), -1);
375 * The value stored by this item, as an {@link Integer}.
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()}
381 * @param useDefaultIfEmpty
382 * use the default value instead of NULL if the setting is not
387 public Integer
getInteger(int item
, boolean useDefaultIfEmpty
) {
389 .parseInteger(getString(item
, useDefaultIfEmpty
), -1);
393 * The default value of this item, as an {@link Integer}.
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()}
400 * @return the default value
402 public Integer
getDefaultInteger(int item
) {
403 return BundleHelper
.parseInteger(getDefaultString(item
), -1);
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.
410 * The returned colour value is an ARGB value.
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()}
416 * @param useDefaultIfEmpty
417 * use the default value instead of NULL if the setting is not
422 public Integer
getColor(int item
, boolean useDefaultIfEmpty
) {
423 return BundleHelper
.parseColor(getString(item
, useDefaultIfEmpty
), -1);
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.
430 * The returned colour value is an ARGB value.
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()}
439 public Integer
getDefaultColor(int item
) {
440 return BundleHelper
.parseColor(getDefaultString(item
), -1);
444 * A {@link String} representation of the list of values.
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.
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()}
453 * @param useDefaultIfEmpty
454 * use the default value instead of NULL if the setting is not
459 public List
<String
> getList(int item
, boolean useDefaultIfEmpty
) {
460 return BundleHelper
.parseList(getString(item
, useDefaultIfEmpty
), -1);
464 * A {@link String} representation of the default list of values.
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.
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()}
476 public List
<String
> getDefaultList(int item
) {
477 return BundleHelper
.parseList(getDefaultString(item
), -1);
481 * The value stored by this item, as a {@link String}.
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()}
490 public void setString(String value
, int item
) {
491 if (isArray() && item
>= 0) {
492 this.value
= BundleHelper
.fromList(this.value
, value
, item
);
499 * The value stored by this item, as a {@link Boolean}.
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()}
508 public void setBoolean(boolean value
, int item
) {
509 setString(BundleHelper
.fromBoolean(value
), item
);
513 * The value stored by this item, as a {@link Character}.
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()}
522 public void setCharacter(char value
, int item
) {
523 setString(BundleHelper
.fromCharacter(value
), item
);
527 * The value stored by this item, as an {@link Integer}.
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()}
536 public void setInteger(int value
, int item
) {
537 setString(BundleHelper
.fromInteger(value
), item
);
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.
544 * The colour value is an ARGB value.
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()}
553 public void setColor(int value
, int item
) {
554 setString(BundleHelper
.fromColor(value
), item
);
558 * A {@link String} representation of the default list of values.
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.
564 * the {@link String} representation
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()}
570 public void setList(List
<String
> value
, int item
) {
571 setString(BundleHelper
.fromList(value
), item
);
575 * Reload the value from the {@link Bundle}, so the last value that was
576 * saved will be used.
578 public void reload() {
579 if (bundle
.isSet(id
, false)) {
580 value
= bundle
.getString(id
);
585 // Copy the list so we can create new listener in a listener
586 for (Runnable listener
: new ArrayList
<Runnable
>(reloadedListeners
)) {
589 } catch (Exception e
) {
596 * Add a listener that will be called <b>after</b> a reload operation.
598 * You could use it to refresh the UI for instance.
603 public void addReloadedListener(Runnable listener
) {
604 reloadedListeners
.add(listener
);
608 * Save the current value to the {@link Bundle}.
610 * Note that listeners will be called <b>before</b> the dirty check and
611 * <b>before</b> saving the value.
614 * only save the data if the dirty flag is set (will reset the
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
)) {
622 } catch (Exception e
) {
627 if (!onlyIfDirty
|| isDirty()) {
628 bundle
.setString(id
, value
);
633 * Add a listener that will be called <b>before</b> a save operation.
635 * You could use it to make some modification to the stored value before it
641 public void addSaveListener(Runnable listener
) {
642 saveListeners
.add(listener
);
646 * The sub-items if any (if no sub-items, will return an empty list).
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}.
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>
658 * @return the sub-items if any
660 public List
<MetaInfo
<E
>> getChildren() {
665 * The number of sub-items, if any.
667 * @return the number or 0
670 return children
.size();
674 public Iterator
<MetaInfo
<E
>> iterator() {
675 return children
.iterator();
679 * Create a list of {@link MetaInfo}, one for each of the item in the given
683 * the type of {@link Bundle} to edit
685 * a class instance of the item type to work on
687 * the {@link Bundle} to sort through
691 static public <E
extends Enum
<E
>> List
<MetaInfo
<E
>> getItems(Class
<E
> type
,
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
);
703 for (int i
= 0; i
< list
.size(); i
++) {
704 MetaInfo
<E
> info
= list
.get(i
);
706 MetaInfo
<E
> parent
= findParent(info
, shadow
);
707 if (parent
!= null) {
709 parent
.children
.add(info
);
710 info
.name
= idToName(info
.id
, parent
.id
);
718 * Find the longest parent of the given {@link MetaInfo}, which means:
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>
729 * the info to look for a parent for
731 * the list of potential parents
733 * @return the longest parent or NULL if no parent is found
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
)) {
744 || group
.id
.toString().length() < candidateId
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());
761 if (name
.length() > 0) {
762 name
= name
.substring(0, 1).toUpperCase()
763 + name
.substring(1).toLowerCase();
766 name
= name
.replace("_", " ");