ConfigItem upgrade
[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.utils.resources.Meta.Format;
8
9 /**
10 * A graphical item that reflect a configuration option from the given
11 * {@link Bundle}.
12 *
13 * @author niki
14 *
15 * @param <E>
16 * the type of {@link Bundle} to edit
17 */
18 public class MetaInfo<E extends Enum<E>> implements Iterable<MetaInfo<E>> {
19 private final Bundle<E> bundle;
20 private final E id;
21
22 private Meta meta;
23 private List<MetaInfo<E>> children = new ArrayList<MetaInfo<E>>();
24
25 private String value;
26 private List<Runnable> reloadedListeners = new ArrayList<Runnable>();
27 private List<Runnable> saveListeners = new ArrayList<Runnable>();
28
29 private String name;
30 private String description;
31
32 /**
33 * Create a new {@link MetaInfo} from a value (without children).
34 * <p>
35 * For instance, you can call
36 * <tt>new MetaInfo(Config.class, configBundle, Config.MY_VALUE)</tt>.
37 *
38 * @param type
39 * the type of enum the value is
40 * @param bundle
41 * the bundle this value belongs to
42 * @param id
43 * the value itself
44 */
45 public MetaInfo(Class<E> type, Bundle<E> bundle, E id) {
46 this.bundle = bundle;
47 this.id = id;
48
49 try {
50 this.meta = type.getDeclaredField(id.name()).getAnnotation(
51 Meta.class);
52 } catch (NoSuchFieldException e) {
53 } catch (SecurityException e) {
54 }
55
56 // We consider that if a description bundle is used, everything is in it
57
58 String description = null;
59 if (bundle.getDescriptionBundle() != null) {
60 description = bundle.getDescriptionBundle().getString(id);
61 if (description != null && description.trim().isEmpty()) {
62 description = null;
63 }
64 }
65 if (description == null) {
66 description = meta.description();
67 if (description == null) {
68 description = "";
69 }
70 }
71
72 String name = idToName(id, null);
73
74 // Special rules for groups:
75 if (meta.group()) {
76 String groupName = description.split("\n")[0];
77 description = description.substring(groupName.length()).trim();
78 if (!groupName.isEmpty()) {
79 name = groupName;
80 }
81 }
82
83 if (meta.def() != null && !meta.def().isEmpty()) {
84 if (!description.isEmpty()) {
85 description += "\n\n";
86 }
87 description += "(Default value: " + meta.def() + ")";
88 }
89
90 this.name = name;
91 this.description = description;
92
93 reload();
94 }
95
96 /**
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).
99 * <p>
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).
102 * <p>
103 * Never NULL.
104 *
105 *
106 * @return the name, never NULL
107 */
108 public String getName() {
109 return name;
110 }
111
112 /**
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 =
115 * file size...).
116 * <p>
117 * For group, the first line ('\\n'-separated) will be used as a title while
118 * the rest will be the description.
119 * <p>
120 * If a default value is known, it will be specified here, too.
121 * <p>
122 * Never NULL.
123 *
124 * @return the description, not NULL
125 */
126 public String getDescription() {
127 return description;
128 }
129
130 /**
131 * The format this item is supposed to follow
132 *
133 * @return the format
134 */
135 public Format getFormat() {
136 return meta.format();
137 }
138
139 /**
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}
142 * items.
143 * <p>
144 * Will always allow an empty string in addition to the rest.
145 *
146 * @return the list of values
147 */
148 public String[] getAllowedValues() {
149 String[] list = meta.list();
150
151 String[] withEmpty = new String[list.length + 1];
152 withEmpty[0] = "";
153 for (int i = 0; i < list.length; i++) {
154 withEmpty[i + 1] = list[i];
155 }
156
157 return withEmpty;
158 }
159
160 /**
161 * This item is a comma-separated list of values instead of a single value.
162 * <p>
163 * The list items are separated by a comma, each surrounded by
164 * double-quotes, with backslashes and double-quotes escaped by a backslash.
165 * <p>
166 * Example: <tt>"un", "deux"</tt>
167 *
168 * @return TRUE if it is
169 */
170 public boolean isArray() {
171 return meta.array();
172 }
173
174 /**
175 * This item is only used as a group, not as an option.
176 * <p>
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.
180 *
181 * @return TRUE if it is a group
182 */
183 public boolean isGroup() {
184 return meta.group();
185 }
186
187 /**
188 * The value stored by this item, as a {@link String}.
189 *
190 * @param useDefaultIfEmpty
191 * use the default value instead of NULL if the setting is not
192 * set
193 *
194 * @return the value
195 */
196 public String getString(boolean useDefaultIfEmpty) {
197 if (value == null && useDefaultIfEmpty) {
198 return getDefaultString();
199 }
200
201 return value;
202 }
203
204 /**
205 * The default value of this item, as a {@link String}.
206 *
207 * @return the default value
208 */
209 public String getDefaultString() {
210 return meta.def();
211 }
212
213 /**
214 * The value stored by this item, as a {@link Boolean}.
215 *
216 * @param useDefaultIfEmpty
217 * use the default value instead of NULL if the setting is not
218 * set
219 *
220 * @return the value
221 */
222 public Boolean getBoolean(boolean useDefaultIfEmpty) {
223 return BundleHelper.parseBoolean(getString(useDefaultIfEmpty));
224 }
225
226 /**
227 * The default value of this item, as a {@link Boolean}.
228 *
229 * @return the default value
230 */
231 public Boolean getDefaultBoolean() {
232 return BundleHelper.parseBoolean(getDefaultString());
233 }
234
235 /**
236 * The value stored by this item, as a {@link Character}.
237 *
238 * @param useDefaultIfEmpty
239 * use the default value instead of NULL if the setting is not
240 * set
241 *
242 * @return the value
243 */
244 public Character getCharacter(boolean useDefaultIfEmpty) {
245 return BundleHelper.parseCharacter(getString(useDefaultIfEmpty));
246 }
247
248 /**
249 * The default value of this item, as a {@link Character}.
250 *
251 * @return the default value
252 */
253 public Character getDefaultCharacter() {
254 return BundleHelper.parseCharacter(getDefaultString());
255 }
256
257 /**
258 * The value stored by this item, as an {@link Integer}.
259 *
260 * @param useDefaultIfEmpty
261 * use the default value instead of NULL if the setting is not
262 * set
263 *
264 * @return the value
265 */
266 public Integer getInteger(boolean useDefaultIfEmpty) {
267 return BundleHelper.parseInteger(getString(useDefaultIfEmpty));
268 }
269
270 /**
271 * The default value of this item, as an {@link Integer}.
272 *
273 * @return the default value
274 */
275 public Integer getDefaultInteger() {
276 return BundleHelper.parseInteger(getDefaultString());
277 }
278
279 /**
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.
282 * <p>
283 * The returned colour value is an ARGB value.
284 *
285 * @param useDefaultIfEmpty
286 * use the default value instead of NULL if the setting is not
287 * set
288 *
289 * @return the value
290 */
291 public Integer getColor(boolean useDefaultIfEmpty) {
292 return BundleHelper.parseColor(getString(useDefaultIfEmpty));
293 }
294
295 /**
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.
298 * <p>
299 * The returned colour value is an ARGB value.
300 *
301 * @return the value
302 */
303 public Integer getDefaultColor() {
304 return BundleHelper.parseColor(getDefaultString());
305 }
306
307 /**
308 * A {@link String} representation of the list of values.
309 * <p>
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.
312 *
313 * @param useDefaultIfEmpty
314 * use the default value instead of NULL if the setting is not
315 * set
316 *
317 * @return the value
318 */
319 public List<String> getList(boolean useDefaultIfEmpty) {
320 return BundleHelper.parseList(getString(useDefaultIfEmpty));
321 }
322
323 /**
324 * A {@link String} representation of the default list of values.
325 * <p>
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.
328 *
329 * @return the value
330 */
331 public List<String> getDefaultList() {
332 return BundleHelper.parseList(getDefaultString());
333 }
334
335 /**
336 * The value stored by this item, as a {@link String}.
337 *
338 * @param value
339 * the new value
340 */
341 public void setString(String value) {
342 this.value = value;
343 }
344
345 /**
346 * The value stored by this item, as a {@link Boolean}.
347 *
348 * @param value
349 * the new value
350 */
351 public void setBoolean(boolean value) {
352 setString(BundleHelper.fromBoolean(value));
353 }
354
355 /**
356 * The value stored by this item, as a {@link Character}.
357 *
358 * @param value
359 * the new value
360 */
361 public void setCharacter(char value) {
362 setString(BundleHelper.fromCharacter(value));
363 }
364
365 /**
366 * The value stored by this item, as an {@link Integer}.
367 *
368 * @param value
369 * the new value
370 */
371 public void setInteger(int value) {
372 setString(BundleHelper.fromInteger(value));
373 }
374
375 /**
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.
378 * <p>
379 * The returned colour value is an ARGB value.
380 *
381 * @param value
382 * the value
383 */
384 public void setColor(int value) {
385 setString(BundleHelper.fromColor(value));
386 }
387
388 /**
389 * A {@link String} representation of the default list of values.
390 * <p>
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.
393 *
394 * @param value
395 * the {@link String} representation
396 *
397 */
398 public void setList(List<String> value) {
399 setString(BundleHelper.fromList(value));
400 }
401
402 /**
403 * Reload the value from the {@link Bundle}, so the last value that was
404 * saved will be used.
405 */
406 public void reload() {
407 value = bundle.getString(id);
408 for (Runnable listener : reloadedListeners) {
409 try {
410 listener.run();
411 } catch (Exception e) {
412 // TODO: error management?
413 e.printStackTrace();
414 }
415 }
416 }
417
418 /**
419 * Add a listener that will be called <b>after</b> a reload operation.
420 * <p>
421 * You could use it to refresh the UI for instance.
422 *
423 * @param listener
424 * the listener
425 */
426 public void addReloadedListener(Runnable listener) {
427 reloadedListeners.add(listener);
428 }
429
430 /**
431 * Save the current value to the {@link Bundle}.
432 */
433 public void save() {
434 for (Runnable listener : saveListeners) {
435 try {
436 listener.run();
437 } catch (Exception e) {
438 // TODO: error management?
439 e.printStackTrace();
440 }
441 }
442 bundle.setString(id, value);
443 }
444
445 /**
446 * Add a listener that will be called <b>before</b> a save operation.
447 * <p>
448 * You could use it to make some modification to the stored value before it
449 * is saved.
450 *
451 * @param listener
452 * the listener
453 */
454 public void addSaveListener(Runnable listener) {
455 saveListeners.add(listener);
456 }
457
458 /**
459 * The sub-items if any (if no sub-items, will return an empty list).
460 * <p>
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}.
463 * <p>
464 * For instance:
465 * <ul>
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>
469 * </ul>
470 *
471 * @return the sub-items if any
472 */
473 public List<MetaInfo<E>> getChildren() {
474 return children;
475 }
476
477 @Override
478 public Iterator<MetaInfo<E>> iterator() {
479 return children.iterator();
480 }
481
482 /**
483 * Create a list of {@link MetaInfo}, one for each of the item in the given
484 * {@link Bundle}.
485 *
486 * @param <E>
487 * the type of {@link Bundle} to edit
488 * @param type
489 * a class instance of the item type to work on
490 * @param bundle
491 * the {@link Bundle} to sort through
492 *
493 * @return the list
494 */
495 static public <E extends Enum<E>> List<MetaInfo<E>> getItems(Class<E> type,
496 Bundle<E> bundle) {
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);
501 list.add(info);
502 shadow.add(info);
503 }
504
505 for (int i = 0; i < list.size(); i++) {
506 MetaInfo<E> info = list.get(i);
507
508 MetaInfo<E> parent = findParent(info, shadow);
509 if (parent != null) {
510 list.remove(i--);
511 parent.children.add(info);
512 info.name = idToName(info.id, parent.id);
513 }
514 }
515
516 return list;
517 }
518
519 /**
520 * Find the longest parent of the given {@link MetaInfo}, which means:
521 * <ul>
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>
526 * </ul>
527 *
528 * @param <E>
529 * the kind of enum
530 * @param info
531 * the info to look for a parent for
532 * @param candidates
533 * the list of potential parents
534 *
535 * @return the longest parent or NULL if no parent is found
536 */
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)) {
545 if (group == null
546 || group.id.toString().length() < candidateId
547 .length()) {
548 group = pcandidate;
549 }
550 }
551 }
552 }
553
554 return group;
555 }
556
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());
561 }
562
563 if (name.length() > 0) {
564 name = name.substring(0, 1).toUpperCase()
565 + name.substring(1).toLowerCase();
566 }
567
568 name = name.replace("_", " ");
569
570 return name.trim();
571 }
572 }