fix ConfigItems
[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 if (bundle.isSet(id, false)) {
408 value = bundle.getString(id);
409 } else {
410 value = null;
411 }
412
413 for (Runnable listener : reloadedListeners) {
414 try {
415 listener.run();
416 } catch (Exception e) {
417 // TODO: error management?
418 e.printStackTrace();
419 }
420 }
421 }
422
423 /**
424 * Add a listener that will be called <b>after</b> a reload operation.
425 * <p>
426 * You could use it to refresh the UI for instance.
427 *
428 * @param listener
429 * the listener
430 */
431 public void addReloadedListener(Runnable listener) {
432 reloadedListeners.add(listener);
433 }
434
435 /**
436 * Save the current value to the {@link Bundle}.
437 */
438 public void save() {
439 for (Runnable listener : saveListeners) {
440 try {
441 listener.run();
442 } catch (Exception e) {
443 // TODO: error management?
444 e.printStackTrace();
445 }
446 }
447 bundle.setString(id, value);
448 }
449
450 /**
451 * Add a listener that will be called <b>before</b> a save operation.
452 * <p>
453 * You could use it to make some modification to the stored value before it
454 * is saved.
455 *
456 * @param listener
457 * the listener
458 */
459 public void addSaveListener(Runnable listener) {
460 saveListeners.add(listener);
461 }
462
463 /**
464 * The sub-items if any (if no sub-items, will return an empty list).
465 * <p>
466 * Sub-items are declared when a {@link Meta} has an ID that starts with the
467 * ID of a {@link Meta#group()} {@link MetaInfo}.
468 * <p>
469 * For instance:
470 * <ul>
471 * <li>{@link Meta} <tt>MY_PREFIX</tt> is a {@link Meta#group()}</li>
472 * <li>{@link Meta} <tt>MY_PREFIX_DESCRIPTION</tt> is another {@link Meta}</li>
473 * <li><tt>MY_PREFIX_DESCRIPTION</tt> will be a child of <tt>MY_PREFIX</tt></li>
474 * </ul>
475 *
476 * @return the sub-items if any
477 */
478 public List<MetaInfo<E>> getChildren() {
479 return children;
480 }
481
482 @Override
483 public Iterator<MetaInfo<E>> iterator() {
484 return children.iterator();
485 }
486
487 /**
488 * Create a list of {@link MetaInfo}, one for each of the item in the given
489 * {@link Bundle}.
490 *
491 * @param <E>
492 * the type of {@link Bundle} to edit
493 * @param type
494 * a class instance of the item type to work on
495 * @param bundle
496 * the {@link Bundle} to sort through
497 *
498 * @return the list
499 */
500 static public <E extends Enum<E>> List<MetaInfo<E>> getItems(Class<E> type,
501 Bundle<E> bundle) {
502 List<MetaInfo<E>> list = new ArrayList<MetaInfo<E>>();
503 List<MetaInfo<E>> shadow = new ArrayList<MetaInfo<E>>();
504 for (E id : type.getEnumConstants()) {
505 MetaInfo<E> info = new MetaInfo<E>(type, bundle, id);
506 list.add(info);
507 shadow.add(info);
508 }
509
510 for (int i = 0; i < list.size(); i++) {
511 MetaInfo<E> info = list.get(i);
512
513 MetaInfo<E> parent = findParent(info, shadow);
514 if (parent != null) {
515 list.remove(i--);
516 parent.children.add(info);
517 info.name = idToName(info.id, parent.id);
518 }
519 }
520
521 return list;
522 }
523
524 /**
525 * Find the longest parent of the given {@link MetaInfo}, which means:
526 * <ul>
527 * <li>the parent is a {@link Meta#group()}</li>
528 * <li>the parent Id is a substring of the Id of the given {@link MetaInfo}</li>
529 * <li>there is no other parent sharing a substring for this
530 * {@link MetaInfo} with a longer Id</li>
531 * </ul>
532 *
533 * @param <E>
534 * the kind of enum
535 * @param info
536 * the info to look for a parent for
537 * @param candidates
538 * the list of potential parents
539 *
540 * @return the longest parent or NULL if no parent is found
541 */
542 static private <E extends Enum<E>> MetaInfo<E> findParent(MetaInfo<E> info,
543 List<MetaInfo<E>> candidates) {
544 String id = info.id.toString();
545 MetaInfo<E> group = null;
546 for (MetaInfo<E> pcandidate : candidates) {
547 if (pcandidate.isGroup()) {
548 String candidateId = pcandidate.id.toString();
549 if (!id.equals(candidateId) && id.startsWith(candidateId)) {
550 if (group == null
551 || group.id.toString().length() < candidateId
552 .length()) {
553 group = pcandidate;
554 }
555 }
556 }
557 }
558
559 return group;
560 }
561
562 static private <E extends Enum<E>> String idToName(E id, E prefix) {
563 String name = id.toString();
564 if (prefix != null && name.startsWith(prefix.toString())) {
565 name = name.substring(prefix.toString().length());
566 }
567
568 if (name.length() > 0) {
569 name = name.substring(0, 1).toUpperCase()
570 + name.substring(1).toLowerCase();
571 }
572
573 name = name.replace("_", " ");
574
575 return name.trim();
576 }
577 }