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 String description
;
32 private boolean dirty
;
35 * Create a new {@link MetaInfo} from a value (without children).
37 * For instance, you can call
38 * <tt>new MetaInfo(Config.class, configBundle, Config.MY_VALUE)</tt>.
41 * the type of enum the value is
43 * the bundle this value belongs to
47 public MetaInfo(Class
<E
> type
, Bundle
<E
> bundle
, E id
) {
52 this.meta
= type
.getDeclaredField(id
.name()).getAnnotation(
54 } catch (NoSuchFieldException e
) {
55 } catch (SecurityException e
) {
58 // We consider that if a description bundle is used, everything is in it
60 String description
= null;
61 if (bundle
.getDescriptionBundle() != null) {
62 description
= bundle
.getDescriptionBundle().getString(id
);
63 if (description
!= null && description
.trim().isEmpty()) {
67 if (description
== null) {
68 description
= meta
.description();
69 if (description
== null) {
74 String name
= idToName(id
, null);
76 // Special rules for groups:
78 String groupName
= description
.split("\n")[0];
79 description
= description
.substring(groupName
.length()).trim();
80 if (!groupName
.isEmpty()) {
85 if (meta
.def() != null && !meta
.def().isEmpty()) {
86 if (!description
.isEmpty()) {
87 description
+= "\n\n";
89 description
+= "(Default value: " + meta
.def() + ")";
93 this.description
= description
;
99 * For normal items, this is the name of this item, deduced from its ID (or
100 * in other words, it is the ID but presented in a displayable form).
102 * For group items, this is the first line of the description if it is not
103 * empty (else, it is the ID in the same way as normal items).
108 * @return the name, never NULL
110 public String
getName() {
115 * A description for this item: what it is or does, how to explain that item
116 * to the user including what can be used here (i.e., %s = file name, %d =
119 * For group, the first line ('\\n'-separated) will be used as a title while
120 * the rest will be the description.
122 * If a default value is known, it will be specified here, too.
126 * @return the description, not NULL
128 public String
getDescription() {
133 * The format this item is supposed to follow
137 public Format
getFormat() {
138 return meta
.format();
142 * The allowed list of values that a {@link Format#FIXED_LIST} item is
143 * allowed to be, or a list of suggestions for {@link Format#COMBO_LIST}
144 * items. Also works for {@link Format#LOCALE}.
146 * Will always allow an empty string in addition to the rest.
148 * @return the list of values
150 public String
[] getAllowedValues() {
151 String
[] list
= meta
.list();
153 String
[] withEmpty
= new String
[list
.length
+ 1];
155 for (int i
= 0; i
< list
.length
; i
++) {
156 withEmpty
[i
+ 1] = list
[i
];
163 * Return all the languages known by the program for this bundle.
165 * This only works for {@link TransBundle}, and will return an empty list if
166 * this is not a {@link TransBundle}.
168 * @return the known language codes
170 public List
<String
> getKnownLanguages() {
171 if (bundle
instanceof TransBundle
) {
172 return ((TransBundle
<E
>) bundle
).getKnownLanguages();
175 return new ArrayList
<String
>();
179 * This item is a comma-separated list of values instead of a single value.
181 * The list items are separated by a comma, each surrounded by
182 * double-quotes, with backslashes and double-quotes escaped by a backslash.
184 * Example: <tt>"un", "deux"</tt>
186 * @return TRUE if it is
188 public boolean isArray() {
193 * A manual flag to specify if the data has been changed or not, which can
194 * be used by {@link MetaInfo#save(boolean)}.
196 * @return TRUE if it is dirty (if it has changed)
198 public boolean isDirty() {
203 * A manual flag to specify that the data has been changed, which can be
204 * used by {@link MetaInfo#save(boolean)}.
206 public void setDirty() {
211 * The number of items in this item if it {@link MetaInfo#isArray()}, or -1
214 * @param useDefaultIfEmpty
215 * check the size of the default list instead if the list is
218 * @return -1 or the number of items
220 public int getListSize(boolean useDefaultIfEmpty
) {
225 return BundleHelper
.getListSize(getString(-1, useDefaultIfEmpty
));
229 * This item is only used as a group, not as an option.
231 * For instance, you could have LANGUAGE_CODE as a group for which you won't
232 * use the value in the program, and LANGUAGE_CODE_FR, LANGUAGE_CODE_EN
233 * inside for which the value must be set.
235 * @return TRUE if it is a group
237 public boolean isGroup() {
242 * The value stored by this item, as a {@link String}.
245 * the item number to get for an array of values, or -1 to get
246 * the whole value (has no effect if {@link MetaInfo#isArray()}
248 * @param useDefaultIfEmpty
249 * use the default value instead of NULL if the setting is not
254 public String
getString(int item
, boolean useDefaultIfEmpty
) {
255 if (isArray() && item
>= 0) {
256 List
<String
> values
= BundleHelper
.parseList(value
, -1);
257 if (values
!= null && item
< values
.size()) {
258 return values
.get(item
);
261 if (useDefaultIfEmpty
) {
262 return getDefaultString(item
);
268 if (value
== null && useDefaultIfEmpty
) {
269 return getDefaultString(item
);
276 * The default value of this item, as a {@link String}.
279 * the item number to get for an array of values, or -1 to get
280 * the whole value (has no effect if {@link MetaInfo#isArray()}
283 * @return the default value
285 public String
getDefaultString(int item
) {
286 if (isArray() && item
>= 0) {
287 List
<String
> values
= BundleHelper
.parseList(meta
.def(), item
);
288 if (values
!= null && item
< values
.size()) {
289 return values
.get(item
);
299 * The value stored by this item, as a {@link Boolean}.
302 * the item number to get for an array of values, or -1 to get
303 * the whole value (has no effect if {@link MetaInfo#isArray()}
305 * @param useDefaultIfEmpty
306 * use the default value instead of NULL if the setting is not
311 public Boolean
getBoolean(int item
, boolean useDefaultIfEmpty
) {
313 .parseBoolean(getString(item
, useDefaultIfEmpty
), -1);
317 * The default value of this item, as a {@link Boolean}.
320 * the item number to get for an array of values, or -1 to get
321 * the whole value (has no effect if {@link MetaInfo#isArray()}
324 * @return the default value
326 public Boolean
getDefaultBoolean(int item
) {
327 return BundleHelper
.parseBoolean(getDefaultString(item
), -1);
331 * The value stored by this item, as a {@link Character}.
334 * the item number to get for an array of values, or -1 to get
335 * the whole value (has no effect if {@link MetaInfo#isArray()}
337 * @param useDefaultIfEmpty
338 * use the default value instead of NULL if the setting is not
343 public Character
getCharacter(int item
, boolean useDefaultIfEmpty
) {
344 return BundleHelper
.parseCharacter(getString(item
, useDefaultIfEmpty
),
349 * The default value of this item, as a {@link Character}.
352 * the item number to get for an array of values, or -1 to get
353 * the whole value (has no effect if {@link MetaInfo#isArray()}
356 * @return the default value
358 public Character
getDefaultCharacter(int item
) {
359 return BundleHelper
.parseCharacter(getDefaultString(item
), -1);
363 * The value stored by this item, as an {@link Integer}.
366 * the item number to get for an array of values, or -1 to get
367 * the whole value (has no effect if {@link MetaInfo#isArray()}
369 * @param useDefaultIfEmpty
370 * use the default value instead of NULL if the setting is not
375 public Integer
getInteger(int item
, boolean useDefaultIfEmpty
) {
377 .parseInteger(getString(item
, useDefaultIfEmpty
), -1);
381 * The default value of this item, as an {@link Integer}.
384 * the item number to get for an array of values, or -1 to get
385 * the whole value (has no effect if {@link MetaInfo#isArray()}
388 * @return the default value
390 public Integer
getDefaultInteger(int item
) {
391 return BundleHelper
.parseInteger(getDefaultString(item
), -1);
395 * The value stored by this item, as a colour (represented here as an
396 * {@link Integer}) if it represents a colour, or NULL if it doesn't.
398 * The returned colour value is an ARGB value.
401 * the item number to get for an array of values, or -1 to get
402 * the whole value (has no effect if {@link MetaInfo#isArray()}
404 * @param useDefaultIfEmpty
405 * use the default value instead of NULL if the setting is not
410 public Integer
getColor(int item
, boolean useDefaultIfEmpty
) {
411 return BundleHelper
.parseColor(getString(item
, useDefaultIfEmpty
), -1);
415 * The default value stored by this item, as a colour (represented here as
416 * an {@link Integer}) if it represents a colour, or NULL if it doesn't.
418 * The returned colour value is an ARGB value.
421 * the item number to get for an array of values, or -1 to get
422 * the whole value (has no effect if {@link MetaInfo#isArray()}
427 public Integer
getDefaultColor(int item
) {
428 return BundleHelper
.parseColor(getDefaultString(item
), -1);
432 * A {@link String} representation of the list of values.
434 * The list of values is comma-separated and each value is surrounded by
435 * double-quotes; backslashes and double-quotes are escaped by a backslash.
438 * the item number to get for an array of values, or -1 to get
439 * the whole value (has no effect if {@link MetaInfo#isArray()}
441 * @param useDefaultIfEmpty
442 * use the default value instead of NULL if the setting is not
447 public List
<String
> getList(int item
, boolean useDefaultIfEmpty
) {
448 return BundleHelper
.parseList(getString(item
, useDefaultIfEmpty
), -1);
452 * A {@link String} representation of the default list of values.
454 * The list of values is comma-separated and each value is surrounded by
455 * double-quotes; backslashes and double-quotes are escaped by a backslash.
458 * the item number to get for an array of values, or -1 to get
459 * the whole value (has no effect if {@link MetaInfo#isArray()}
464 public List
<String
> getDefaultList(int item
) {
465 return BundleHelper
.parseList(getDefaultString(item
), -1);
469 * The value stored by this item, as a {@link String}.
474 * the item number to set for an array of values, or -1 to set
475 * the whole value (has no effect if {@link MetaInfo#isArray()}
478 public void setString(String value
, int item
) {
479 if (isArray() && item
>= 0) {
480 List
<String
> values
= BundleHelper
.parseList(this.value
, -1);
481 for (int i
= values
.size(); i
<= item
; i
++) {
484 values
.set(item
, value
);
485 this.value
= BundleHelper
.fromList(values
);
492 * The value stored by this item, as a {@link Boolean}.
497 * the item number to set for an array of values, or -1 to set
498 * the whole value (has no effect if {@link MetaInfo#isArray()}
501 public void setBoolean(boolean value
, int item
) {
502 setString(BundleHelper
.fromBoolean(value
), item
);
506 * The value stored by this item, as a {@link Character}.
511 * the item number to set for an array of values, or -1 to set
512 * the whole value (has no effect if {@link MetaInfo#isArray()}
515 public void setCharacter(char value
, int item
) {
516 setString(BundleHelper
.fromCharacter(value
), item
);
520 * The value stored by this item, as an {@link Integer}.
525 * the item number to set for an array of values, or -1 to set
526 * the whole value (has no effect if {@link MetaInfo#isArray()}
529 public void setInteger(int value
, int item
) {
530 setString(BundleHelper
.fromInteger(value
), item
);
534 * The value stored by this item, as a colour (represented here as an
535 * {@link Integer}) if it represents a colour, or NULL if it doesn't.
537 * The colour value is an ARGB value.
542 * the item number to set for an array of values, or -1 to set
543 * the whole value (has no effect if {@link MetaInfo#isArray()}
546 public void setColor(int value
, int item
) {
547 setString(BundleHelper
.fromColor(value
), item
);
551 * A {@link String} representation of the default list of values.
553 * The list of values is comma-separated and each value is surrounded by
554 * double-quotes; backslashes and double-quotes are escaped by a backslash.
557 * the {@link String} representation
559 * the item number to set for an array of values, or -1 to set
560 * the whole value (has no effect if {@link MetaInfo#isArray()}
563 public void setList(List
<String
> value
, int item
) {
564 setString(BundleHelper
.fromList(value
), item
);
568 * Reload the value from the {@link Bundle}, so the last value that was
569 * saved will be used.
571 public void reload() {
572 if (bundle
.isSet(id
, false)) {
573 value
= bundle
.getString(id
);
578 for (Runnable listener
: reloadedListeners
) {
581 } catch (Exception e
) {
588 * Add a listener that will be called <b>after</b> a reload operation.
590 * You could use it to refresh the UI for instance.
595 public void addReloadedListener(Runnable listener
) {
596 reloadedListeners
.add(listener
);
600 * Save the current value to the {@link Bundle}.
602 * Note that listeners will be called <b>before</b> the dirty check and
603 * <b>before</b> saving the value.
606 * only save the data if the dirty flag is set (will reset the
609 public void save(boolean onlyIfDirty
) {
610 for (Runnable listener
: saveListeners
) {
613 } catch (Exception e
) {
618 if (!onlyIfDirty
|| isDirty()) {
619 bundle
.setString(id
, value
);
624 * Add a listener that will be called <b>before</b> a save operation.
626 * You could use it to make some modification to the stored value before it
632 public void addSaveListener(Runnable listener
) {
633 saveListeners
.add(listener
);
637 * The sub-items if any (if no sub-items, will return an empty list).
639 * Sub-items are declared when a {@link Meta} has an ID that starts with the
640 * ID of a {@link Meta#group()} {@link MetaInfo}.
644 * <li>{@link Meta} <tt>MY_PREFIX</tt> is a {@link Meta#group()}</li>
645 * <li>{@link Meta} <tt>MY_PREFIX_DESCRIPTION</tt> is another {@link Meta}</li>
646 * <li><tt>MY_PREFIX_DESCRIPTION</tt> will be a child of <tt>MY_PREFIX</tt></li>
649 * @return the sub-items if any
651 public List
<MetaInfo
<E
>> getChildren() {
656 public Iterator
<MetaInfo
<E
>> iterator() {
657 return children
.iterator();
661 * Create a list of {@link MetaInfo}, one for each of the item in the given
665 * the type of {@link Bundle} to edit
667 * a class instance of the item type to work on
669 * the {@link Bundle} to sort through
673 static public <E
extends Enum
<E
>> List
<MetaInfo
<E
>> getItems(Class
<E
> type
,
675 List
<MetaInfo
<E
>> list
= new ArrayList
<MetaInfo
<E
>>();
676 List
<MetaInfo
<E
>> shadow
= new ArrayList
<MetaInfo
<E
>>();
677 for (E id
: type
.getEnumConstants()) {
678 MetaInfo
<E
> info
= new MetaInfo
<E
>(type
, bundle
, id
);
683 for (int i
= 0; i
< list
.size(); i
++) {
684 MetaInfo
<E
> info
= list
.get(i
);
686 MetaInfo
<E
> parent
= findParent(info
, shadow
);
687 if (parent
!= null) {
689 parent
.children
.add(info
);
690 info
.name
= idToName(info
.id
, parent
.id
);
698 * Find the longest parent of the given {@link MetaInfo}, which means:
700 * <li>the parent is a {@link Meta#group()}</li>
701 * <li>the parent Id is a substring of the Id of the given {@link MetaInfo}</li>
702 * <li>there is no other parent sharing a substring for this
703 * {@link MetaInfo} with a longer Id</li>
709 * the info to look for a parent for
711 * the list of potential parents
713 * @return the longest parent or NULL if no parent is found
715 static private <E
extends Enum
<E
>> MetaInfo
<E
> findParent(MetaInfo
<E
> info
,
716 List
<MetaInfo
<E
>> candidates
) {
717 String id
= info
.id
.toString();
718 MetaInfo
<E
> group
= null;
719 for (MetaInfo
<E
> pcandidate
: candidates
) {
720 if (pcandidate
.isGroup()) {
721 String candidateId
= pcandidate
.id
.toString();
722 if (!id
.equals(candidateId
) && id
.startsWith(candidateId
)) {
724 || group
.id
.toString().length() < candidateId
735 static private <E
extends Enum
<E
>> String
idToName(E id
, E prefix
) {
736 String name
= id
.toString();
737 if (prefix
!= null && name
.startsWith(prefix
.toString())) {
738 name
= name
.substring(prefix
.toString().length());
741 if (name
.length() > 0) {
742 name
= name
.substring(0, 1).toUpperCase()
743 + name
.substring(1).toLowerCase();
746 name
= name
.replace("_", " ");