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}
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 * This item is a comma-separated list of values instead of a single value.
165 * The list items are separated by a comma, each surrounded by
166 * double-quotes, with backslashes and double-quotes escaped by a backslash.
168 * Example: <tt>"un", "deux"</tt>
170 * @return TRUE if it is
172 public boolean isArray() {
177 * A manual flag to specify if the data has been changed or not, which can
178 * be used by {@link MetaInfo#save(boolean)}.
180 * @return TRUE if it is dirty (if it has changed)
182 public boolean isDirty() {
187 * A manual flag to specify that the data has been changed, which can be
188 * used by {@link MetaInfo#save(boolean)}.
190 public void setDirty() {
195 * The number of items in this item if it {@link MetaInfo#isArray()}, or -1
198 * @param useDefaultIfEmpty
199 * check the size of the default list instead if the list is
202 * @return -1 or the number of items
204 public int getListSize(boolean useDefaultIfEmpty
) {
209 return BundleHelper
.getListSize(getString(-1, useDefaultIfEmpty
));
213 * This item is only used as a group, not as an option.
215 * For instance, you could have LANGUAGE_CODE as a group for which you won't
216 * use the value in the program, and LANGUAGE_CODE_FR, LANGUAGE_CODE_EN
217 * inside for which the value must be set.
219 * @return TRUE if it is a group
221 public boolean isGroup() {
226 * The value stored by this item, as a {@link String}.
229 * the item number to get for an array of values, or -1 to get
230 * the whole value (has no effect if {@link MetaInfo#isArray()}
232 * @param useDefaultIfEmpty
233 * use the default value instead of NULL if the setting is not
238 public String
getString(int item
, boolean useDefaultIfEmpty
) {
239 if (isArray() && item
>= 0) {
240 List
<String
> values
= BundleHelper
.parseList(value
, -1);
241 if (values
!= null && item
< values
.size()) {
242 return values
.get(item
);
245 if (useDefaultIfEmpty
) {
246 return getDefaultString(item
);
252 if (value
== null && useDefaultIfEmpty
) {
253 return getDefaultString(item
);
260 * The default value of this item, as a {@link String}.
263 * the item number to get for an array of values, or -1 to get
264 * the whole value (has no effect if {@link MetaInfo#isArray()}
267 * @return the default value
269 public String
getDefaultString(int item
) {
270 if (isArray() && item
>= 0) {
271 List
<String
> values
= BundleHelper
.parseList(meta
.def(), item
);
272 if (values
!= null && item
< values
.size()) {
273 return values
.get(item
);
283 * The value stored by this item, as a {@link Boolean}.
286 * the item number to get for an array of values, or -1 to get
287 * the whole value (has no effect if {@link MetaInfo#isArray()}
289 * @param useDefaultIfEmpty
290 * use the default value instead of NULL if the setting is not
295 public Boolean
getBoolean(int item
, boolean useDefaultIfEmpty
) {
297 .parseBoolean(getString(item
, useDefaultIfEmpty
), -1);
301 * The default value of this item, as a {@link Boolean}.
304 * the item number to get for an array of values, or -1 to get
305 * the whole value (has no effect if {@link MetaInfo#isArray()}
308 * @return the default value
310 public Boolean
getDefaultBoolean(int item
) {
311 return BundleHelper
.parseBoolean(getDefaultString(item
), -1);
315 * The value stored by this item, as a {@link Character}.
318 * the item number to get for an array of values, or -1 to get
319 * the whole value (has no effect if {@link MetaInfo#isArray()}
321 * @param useDefaultIfEmpty
322 * use the default value instead of NULL if the setting is not
327 public Character
getCharacter(int item
, boolean useDefaultIfEmpty
) {
328 return BundleHelper
.parseCharacter(getString(item
, useDefaultIfEmpty
),
333 * The default value of this item, as a {@link Character}.
336 * the item number to get for an array of values, or -1 to get
337 * the whole value (has no effect if {@link MetaInfo#isArray()}
340 * @return the default value
342 public Character
getDefaultCharacter(int item
) {
343 return BundleHelper
.parseCharacter(getDefaultString(item
), -1);
347 * The value stored by this item, as an {@link Integer}.
350 * the item number to get for an array of values, or -1 to get
351 * the whole value (has no effect if {@link MetaInfo#isArray()}
353 * @param useDefaultIfEmpty
354 * use the default value instead of NULL if the setting is not
359 public Integer
getInteger(int item
, boolean useDefaultIfEmpty
) {
361 .parseInteger(getString(item
, useDefaultIfEmpty
), -1);
365 * The default value of this item, as an {@link Integer}.
368 * the item number to get for an array of values, or -1 to get
369 * the whole value (has no effect if {@link MetaInfo#isArray()}
372 * @return the default value
374 public Integer
getDefaultInteger(int item
) {
375 return BundleHelper
.parseInteger(getDefaultString(item
), -1);
379 * The value stored by this item, as a colour (represented here as an
380 * {@link Integer}) if it represents a colour, or NULL if it doesn't.
382 * The returned colour value is an ARGB value.
385 * the item number to get for an array of values, or -1 to get
386 * the whole value (has no effect if {@link MetaInfo#isArray()}
388 * @param useDefaultIfEmpty
389 * use the default value instead of NULL if the setting is not
394 public Integer
getColor(int item
, boolean useDefaultIfEmpty
) {
395 return BundleHelper
.parseColor(getString(item
, useDefaultIfEmpty
), -1);
399 * The default value stored by this item, as a colour (represented here as
400 * an {@link Integer}) if it represents a colour, or NULL if it doesn't.
402 * The returned colour value is an ARGB value.
405 * the item number to get for an array of values, or -1 to get
406 * the whole value (has no effect if {@link MetaInfo#isArray()}
411 public Integer
getDefaultColor(int item
) {
412 return BundleHelper
.parseColor(getDefaultString(item
), -1);
416 * A {@link String} representation of the list of values.
418 * The list of values is comma-separated and each value is surrounded by
419 * double-quotes; backslashes and double-quotes are escaped by a backslash.
422 * the item number to get for an array of values, or -1 to get
423 * the whole value (has no effect if {@link MetaInfo#isArray()}
425 * @param useDefaultIfEmpty
426 * use the default value instead of NULL if the setting is not
431 public List
<String
> getList(int item
, boolean useDefaultIfEmpty
) {
432 return BundleHelper
.parseList(getString(item
, useDefaultIfEmpty
), -1);
436 * A {@link String} representation of the default list of values.
438 * The list of values is comma-separated and each value is surrounded by
439 * double-quotes; backslashes and double-quotes are escaped by a backslash.
442 * the item number to get for an array of values, or -1 to get
443 * the whole value (has no effect if {@link MetaInfo#isArray()}
448 public List
<String
> getDefaultList(int item
) {
449 return BundleHelper
.parseList(getDefaultString(item
), -1);
453 * The value stored by this item, as a {@link String}.
458 * the item number to set for an array of values, or -1 to set
459 * the whole value (has no effect if {@link MetaInfo#isArray()}
462 public void setString(String value
, int item
) {
463 if (isArray() && item
>= 0) {
464 List
<String
> values
= BundleHelper
.parseList(this.value
, -1);
465 for (int i
= values
.size(); i
<= item
; i
++) {
468 values
.set(item
, value
);
469 this.value
= BundleHelper
.fromList(values
);
476 * The value stored by this item, as a {@link Boolean}.
481 * the item number to set for an array of values, or -1 to set
482 * the whole value (has no effect if {@link MetaInfo#isArray()}
485 public void setBoolean(boolean value
, int item
) {
486 setString(BundleHelper
.fromBoolean(value
), item
);
490 * The value stored by this item, as a {@link Character}.
495 * the item number to set for an array of values, or -1 to set
496 * the whole value (has no effect if {@link MetaInfo#isArray()}
499 public void setCharacter(char value
, int item
) {
500 setString(BundleHelper
.fromCharacter(value
), item
);
504 * The value stored by this item, as an {@link Integer}.
509 * the item number to set for an array of values, or -1 to set
510 * the whole value (has no effect if {@link MetaInfo#isArray()}
513 public void setInteger(int value
, int item
) {
514 setString(BundleHelper
.fromInteger(value
), item
);
518 * The value stored by this item, as a colour (represented here as an
519 * {@link Integer}) if it represents a colour, or NULL if it doesn't.
521 * The colour value is an ARGB value.
526 * the item number to set for an array of values, or -1 to set
527 * the whole value (has no effect if {@link MetaInfo#isArray()}
530 public void setColor(int value
, int item
) {
531 setString(BundleHelper
.fromColor(value
), item
);
535 * A {@link String} representation of the default list of values.
537 * The list of values is comma-separated and each value is surrounded by
538 * double-quotes; backslashes and double-quotes are escaped by a backslash.
541 * the {@link String} representation
543 * the item number to set for an array of values, or -1 to set
544 * the whole value (has no effect if {@link MetaInfo#isArray()}
547 public void setList(List
<String
> value
, int item
) {
548 setString(BundleHelper
.fromList(value
), item
);
552 * Reload the value from the {@link Bundle}, so the last value that was
553 * saved will be used.
555 public void reload() {
556 if (bundle
.isSet(id
, false)) {
557 value
= bundle
.getString(id
);
562 for (Runnable listener
: reloadedListeners
) {
565 } catch (Exception e
) {
572 * Add a listener that will be called <b>after</b> a reload operation.
574 * You could use it to refresh the UI for instance.
579 public void addReloadedListener(Runnable listener
) {
580 reloadedListeners
.add(listener
);
584 * Save the current value to the {@link Bundle}.
586 * Note that listeners will be called <b>before</b> the dirty check and
587 * <b>before</b> saving the value.
590 * only save the data if the dirty flag is set (will reset the
593 public void save(boolean onlyIfDirty
) {
594 for (Runnable listener
: saveListeners
) {
597 } catch (Exception e
) {
602 if (!onlyIfDirty
|| isDirty()) {
603 bundle
.setString(id
, value
);
608 * Add a listener that will be called <b>before</b> a save operation.
610 * You could use it to make some modification to the stored value before it
616 public void addSaveListener(Runnable listener
) {
617 saveListeners
.add(listener
);
621 * The sub-items if any (if no sub-items, will return an empty list).
623 * Sub-items are declared when a {@link Meta} has an ID that starts with the
624 * ID of a {@link Meta#group()} {@link MetaInfo}.
628 * <li>{@link Meta} <tt>MY_PREFIX</tt> is a {@link Meta#group()}</li>
629 * <li>{@link Meta} <tt>MY_PREFIX_DESCRIPTION</tt> is another {@link Meta}</li>
630 * <li><tt>MY_PREFIX_DESCRIPTION</tt> will be a child of <tt>MY_PREFIX</tt></li>
633 * @return the sub-items if any
635 public List
<MetaInfo
<E
>> getChildren() {
640 public Iterator
<MetaInfo
<E
>> iterator() {
641 return children
.iterator();
645 * Create a list of {@link MetaInfo}, one for each of the item in the given
649 * the type of {@link Bundle} to edit
651 * a class instance of the item type to work on
653 * the {@link Bundle} to sort through
657 static public <E
extends Enum
<E
>> List
<MetaInfo
<E
>> getItems(Class
<E
> type
,
659 List
<MetaInfo
<E
>> list
= new ArrayList
<MetaInfo
<E
>>();
660 List
<MetaInfo
<E
>> shadow
= new ArrayList
<MetaInfo
<E
>>();
661 for (E id
: type
.getEnumConstants()) {
662 MetaInfo
<E
> info
= new MetaInfo
<E
>(type
, bundle
, id
);
667 for (int i
= 0; i
< list
.size(); i
++) {
668 MetaInfo
<E
> info
= list
.get(i
);
670 MetaInfo
<E
> parent
= findParent(info
, shadow
);
671 if (parent
!= null) {
673 parent
.children
.add(info
);
674 info
.name
= idToName(info
.id
, parent
.id
);
682 * Find the longest parent of the given {@link MetaInfo}, which means:
684 * <li>the parent is a {@link Meta#group()}</li>
685 * <li>the parent Id is a substring of the Id of the given {@link MetaInfo}</li>
686 * <li>there is no other parent sharing a substring for this
687 * {@link MetaInfo} with a longer Id</li>
693 * the info to look for a parent for
695 * the list of potential parents
697 * @return the longest parent or NULL if no parent is found
699 static private <E
extends Enum
<E
>> MetaInfo
<E
> findParent(MetaInfo
<E
> info
,
700 List
<MetaInfo
<E
>> candidates
) {
701 String id
= info
.id
.toString();
702 MetaInfo
<E
> group
= null;
703 for (MetaInfo
<E
> pcandidate
: candidates
) {
704 if (pcandidate
.isGroup()) {
705 String candidateId
= pcandidate
.id
.toString();
706 if (!id
.equals(candidateId
) && id
.startsWith(candidateId
)) {
708 || group
.id
.toString().length() < candidateId
719 static private <E
extends Enum
<E
>> String
idToName(E id
, E prefix
) {
720 String name
= id
.toString();
721 if (prefix
!= null && name
.startsWith(prefix
.toString())) {
722 name
= name
.substring(prefix
.toString().length());
725 if (name
.length() > 0) {
726 name
= name
.substring(0, 1).toUpperCase()
727 + name
.substring(1).toLowerCase();
730 name
= name
.replace("_", " ");