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
;
33 * Create a new {@link MetaInfo} from a value (without children).
35 * For instance, you can call
36 * <tt>new MetaInfo(Config.class, configBundle, Config.MY_VALUE)</tt>.
39 * the type of enum the value is
41 * the bundle this value belongs to
45 public MetaInfo(Class
<E
> type
, Bundle
<E
> bundle
, E id
) {
50 this.meta
= type
.getDeclaredField(id
.name()).getAnnotation(
52 } catch (NoSuchFieldException e
) {
53 } catch (SecurityException e
) {
56 // We consider that if a description bundle is used, everything is in it
58 String description
= null;
59 if (bundle
.getDescriptionBundle() != null) {
60 description
= bundle
.getDescriptionBundle().getString(id
);
61 if (description
!= null && description
.trim().isEmpty()) {
65 if (description
== null) {
66 description
= meta
.description();
67 if (description
== null) {
72 String name
= idToName(id
, null);
74 // Special rules for groups:
76 String groupName
= description
.split("\n")[0];
77 description
= description
.substring(groupName
.length()).trim();
78 if (!groupName
.isEmpty()) {
83 if (meta
.def() != null && !meta
.def().isEmpty()) {
84 if (!description
.isEmpty()) {
85 description
+= "\n\n";
87 description
+= "(Default value: " + meta
.def() + ")";
91 this.description
= description
;
97 * For normal items, this is the name of this item, deduced from its ID (or
98 * in other words, it is the ID but presented in a displayable form).
100 * For group items, this is the first line of the description if it is not
101 * empty (else, it is the ID in the same way as normal items).
106 * @return the name, never NULL
108 public String
getName() {
113 * A description for this item: what it is or does, how to explain that item
114 * to the user including what can be used here (i.e., %s = file name, %d =
117 * For group, the first line ('\\n'-separated) will be used as a title while
118 * the rest will be the description.
120 * If a default value is known, it will be specified here, too.
124 * @return the description, not NULL
126 public String
getDescription() {
131 * The format this item is supposed to follow
135 public Format
getFormat() {
136 return meta
.format();
140 * The allowed list of values that a {@link Format#FIXED_LIST} item is
141 * allowed to be, or a list of suggestions for {@link Format#COMBO_LIST}
144 * Will always allow an empty string in addition to the rest.
146 * @return the list of values
148 public String
[] getAllowedValues() {
149 String
[] list
= meta
.list();
151 String
[] withEmpty
= new String
[list
.length
+ 1];
153 for (int i
= 0; i
< list
.length
; i
++) {
154 withEmpty
[i
+ 1] = list
[i
];
161 * This item is a comma-separated list of values instead of a single value.
163 * The list items are separated by a comma, each surrounded by
164 * double-quotes, with backslashes and double-quotes escaped by a backslash.
166 * Example: <tt>"un", "deux"</tt>
168 * @return TRUE if it is
170 public boolean isArray() {
175 * This item is only used as a group, not as an option.
177 * For instance, you could have LANGUAGE_CODE as a group for which you won't
178 * use the value in the program, and LANGUAGE_CODE_FR, LANGUAGE_CODE_EN
179 * inside for which the value must be set.
181 * @return TRUE if it is a group
183 public boolean isGroup() {
188 * The value stored by this item, as a {@link String}.
190 * @param useDefaultIfEmpty
191 * use the default value instead of NULL if the setting is not
196 public String
getString(boolean useDefaultIfEmpty
) {
197 if (value
== null && useDefaultIfEmpty
) {
198 return getDefaultString();
205 * The default value of this item, as a {@link String}.
207 * @return the default value
209 public String
getDefaultString() {
214 * The value stored by this item, as a {@link Boolean}.
216 * @param useDefaultIfEmpty
217 * use the default value instead of NULL if the setting is not
222 public Boolean
getBoolean(boolean useDefaultIfEmpty
) {
223 return BundleHelper
.parseBoolean(getString(useDefaultIfEmpty
));
227 * The default value of this item, as a {@link Boolean}.
229 * @return the default value
231 public Boolean
getDefaultBoolean() {
232 return BundleHelper
.parseBoolean(getDefaultString());
236 * The value stored by this item, as a {@link Character}.
238 * @param useDefaultIfEmpty
239 * use the default value instead of NULL if the setting is not
244 public Character
getCharacter(boolean useDefaultIfEmpty
) {
245 return BundleHelper
.parseCharacter(getString(useDefaultIfEmpty
));
249 * The default value of this item, as a {@link Character}.
251 * @return the default value
253 public Character
getDefaultCharacter() {
254 return BundleHelper
.parseCharacter(getDefaultString());
258 * The value stored by this item, as an {@link Integer}.
260 * @param useDefaultIfEmpty
261 * use the default value instead of NULL if the setting is not
266 public Integer
getInteger(boolean useDefaultIfEmpty
) {
267 return BundleHelper
.parseInteger(getString(useDefaultIfEmpty
));
271 * The default value of this item, as an {@link Integer}.
273 * @return the default value
275 public Integer
getDefaultInteger() {
276 return BundleHelper
.parseInteger(getDefaultString());
280 * The value stored by this item, as a colour (represented here as an
281 * {@link Integer}) if it represents a colour, or NULL if it doesn't.
283 * The returned colour value is an ARGB value.
285 * @param useDefaultIfEmpty
286 * use the default value instead of NULL if the setting is not
291 public Integer
getColor(boolean useDefaultIfEmpty
) {
292 return BundleHelper
.parseColor(getString(useDefaultIfEmpty
));
296 * The default value stored by this item, as a colour (represented here as
297 * an {@link Integer}) if it represents a colour, or NULL if it doesn't.
299 * The returned colour value is an ARGB value.
303 public Integer
getDefaultColor() {
304 return BundleHelper
.parseColor(getDefaultString());
308 * A {@link String} representation of the list of values.
310 * The list of values is comma-separated and each value is surrounded by
311 * double-quotes; backslashes and double-quotes are escaped by a backslash.
313 * @param useDefaultIfEmpty
314 * use the default value instead of NULL if the setting is not
319 public List
<String
> getList(boolean useDefaultIfEmpty
) {
320 return BundleHelper
.parseList(getString(useDefaultIfEmpty
));
324 * A {@link String} representation of the default list of values.
326 * The list of values is comma-separated and each value is surrounded by
327 * double-quotes; backslashes and double-quotes are escaped by a backslash.
331 public List
<String
> getDefaultList() {
332 return BundleHelper
.parseList(getDefaultString());
336 * The value stored by this item, as a {@link String}.
341 public void setString(String value
) {
346 * The value stored by this item, as a {@link Boolean}.
351 public void setBoolean(boolean value
) {
352 setString(BundleHelper
.fromBoolean(value
));
356 * The value stored by this item, as a {@link Character}.
361 public void setCharacter(char value
) {
362 setString(BundleHelper
.fromCharacter(value
));
366 * The value stored by this item, as an {@link Integer}.
371 public void setInteger(int value
) {
372 setString(BundleHelper
.fromInteger(value
));
376 * The value stored by this item, as a colour (represented here as an
377 * {@link Integer}) if it represents a colour, or NULL if it doesn't.
379 * The returned colour value is an ARGB value.
384 public void setColor(int value
) {
385 setString(BundleHelper
.fromColor(value
));
389 * A {@link String} representation of the default list of values.
391 * The list of values is comma-separated and each value is surrounded by
392 * double-quotes; backslashes and double-quotes are escaped by a backslash.
395 * the {@link String} representation
398 public void setList(List
<String
> value
) {
399 setString(BundleHelper
.fromList(value
));
403 * Reload the value from the {@link Bundle}, so the last value that was
404 * saved will be used.
406 public void reload() {
407 value
= bundle
.getString(id
);
408 for (Runnable listener
: reloadedListeners
) {
411 } catch (Exception e
) {
412 // TODO: error management?
419 * Add a listener that will be called <b>after</b> a reload operation.
421 * You could use it to refresh the UI for instance.
426 public void addReloadedListener(Runnable listener
) {
427 reloadedListeners
.add(listener
);
431 * Save the current value to the {@link Bundle}.
434 for (Runnable listener
: saveListeners
) {
437 } catch (Exception e
) {
438 // TODO: error management?
442 bundle
.setString(id
, value
);
446 * Add a listener that will be called <b>before</b> a save operation.
448 * You could use it to make some modification to the stored value before it
454 public void addSaveListener(Runnable listener
) {
455 saveListeners
.add(listener
);
459 * The sub-items if any (if no sub-items, will return an empty list).
461 * Sub-items are declared when a {@link Meta} has an ID that starts with the
462 * ID of a {@link Meta#group()} {@link MetaInfo}.
466 * <li>{@link Meta} <tt>MY_PREFIX</tt> is a {@link Meta#group()}</li>
467 * <li>{@link Meta} <tt>MY_PREFIX_DESCRIPTION</tt> is another {@link Meta}</li>
468 * <li><tt>MY_PREFIX_DESCRIPTION</tt> will be a child of <tt>MY_PREFIX</tt></li>
471 * @return the sub-items if any
473 public List
<MetaInfo
<E
>> getChildren() {
478 public Iterator
<MetaInfo
<E
>> iterator() {
479 return children
.iterator();
483 * Create a list of {@link MetaInfo}, one for each of the item in the given
487 * the type of {@link Bundle} to edit
489 * a class instance of the item type to work on
491 * the {@link Bundle} to sort through
495 static public <E
extends Enum
<E
>> List
<MetaInfo
<E
>> getItems(Class
<E
> type
,
497 List
<MetaInfo
<E
>> list
= new ArrayList
<MetaInfo
<E
>>();
498 List
<MetaInfo
<E
>> shadow
= new ArrayList
<MetaInfo
<E
>>();
499 for (E id
: type
.getEnumConstants()) {
500 MetaInfo
<E
> info
= new MetaInfo
<E
>(type
, bundle
, id
);
505 for (int i
= 0; i
< list
.size(); i
++) {
506 MetaInfo
<E
> info
= list
.get(i
);
508 MetaInfo
<E
> parent
= findParent(info
, shadow
);
509 if (parent
!= null) {
511 parent
.children
.add(info
);
512 info
.name
= idToName(info
.id
, parent
.id
);
520 * Find the longest parent of the given {@link MetaInfo}, which means:
522 * <li>the parent is a {@link Meta#group()}</li>
523 * <li>the parent Id is a substring of the Id of the given {@link MetaInfo}</li>
524 * <li>there is no other parent sharing a substring for this
525 * {@link MetaInfo} with a longer Id</li>
531 * the info to look for a parent for
533 * the list of potential parents
535 * @return the longest parent or NULL if no parent is found
537 static private <E
extends Enum
<E
>> MetaInfo
<E
> findParent(MetaInfo
<E
> info
,
538 List
<MetaInfo
<E
>> candidates
) {
539 String id
= info
.id
.toString();
540 MetaInfo
<E
> group
= null;
541 for (MetaInfo
<E
> pcandidate
: candidates
) {
542 if (pcandidate
.isGroup()) {
543 String candidateId
= pcandidate
.id
.toString();
544 if (!id
.equals(candidateId
) && id
.startsWith(candidateId
)) {
546 || group
.id
.toString().length() < candidateId
557 static private <E
extends Enum
<E
>> String
idToName(E id
, E prefix
) {
558 String name
= id
.toString();
559 if (prefix
!= null && name
.startsWith(prefix
.toString())) {
560 name
= name
.substring(prefix
.toString().length());
563 if (name
.length() > 0) {
564 name
= name
.substring(0, 1).toUpperCase()
565 + name
.substring(1).toLowerCase();
568 name
= name
.replace("_", " ");