ConfigItem upgrade
[fanfix.git] / src / be / nikiroo / utils / resources / MetaInfo.java
CommitLineData
9e834013
NR
1package be.nikiroo.utils.resources;
2
3import java.util.ArrayList;
0877d6f5 4import java.util.Iterator;
9e834013
NR
5import java.util.List;
6
7import 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 */
0877d6f5 18public class MetaInfo<E extends Enum<E>> implements Iterable<MetaInfo<E>> {
9e834013
NR
19 private final Bundle<E> bundle;
20 private final E id;
21
22 private Meta meta;
0877d6f5 23 private List<MetaInfo<E>> children = new ArrayList<MetaInfo<E>>();
9e834013
NR
24
25 private String value;
8517b60c
NR
26 private List<Runnable> reloadedListeners = new ArrayList<Runnable>();
27 private List<Runnable> saveListeners = new ArrayList<Runnable>();
9e834013
NR
28
29 private String name;
30 private String description;
31
76b51de9
NR
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 */
9e834013
NR
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 }
9e834013
NR
65 if (description == null) {
66 description = meta.description();
0877d6f5
NR
67 if (description == null) {
68 description = "";
69 }
d18e136e
NR
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;
9e834013
NR
80 }
81 }
82
d18e136e
NR
83 if (meta.def() != null && !meta.def().isEmpty()) {
84 if (!description.isEmpty()) {
85 description += "\n\n";
86 }
87 description += "(Default value: " + meta.def() + ")";
9e834013
NR
88 }
89
90 this.name = name;
91 this.description = description;
92
93 reload();
94 }
95
96 /**
d18e136e
NR
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).
9e834013 102 * <p>
d18e136e 103 * Never NULL.
9e834013 104 *
d18e136e
NR
105 *
106 * @return the name, never NULL
9e834013
NR
107 */
108 public String getName() {
109 return name;
110 }
111
112 /**
d18e136e
NR
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.
9e834013 123 *
d18e136e 124 * @return the description, not NULL
9e834013
NR
125 */
126 public String getDescription() {
127 return description;
128 }
129
76b51de9
NR
130 /**
131 * The format this item is supposed to follow
132 *
133 * @return the format
134 */
9e834013
NR
135 public Format getFormat() {
136 return meta.format();
137 }
138
76b51de9
NR
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.
d18e136e
NR
143 * <p>
144 * Will always allow an empty string in addition to the rest.
76b51de9
NR
145 *
146 * @return the list of values
147 */
0877d6f5 148 public String[] getAllowedValues() {
d18e136e
NR
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;
0877d6f5
NR
158 }
159
76b51de9
NR
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 */
0877d6f5
NR
170 public boolean isArray() {
171 return meta.array();
172 }
173
76b51de9
NR
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
9e834013
NR
187 /**
188 * The value stored by this item, as a {@link String}.
189 *
d18e136e
NR
190 * @param useDefaultIfEmpty
191 * use the default value instead of NULL if the setting is not
192 * set
193 *
9e834013
NR
194 * @return the value
195 */
d18e136e
NR
196 public String getString(boolean useDefaultIfEmpty) {
197 if (value == null && useDefaultIfEmpty) {
198 return getDefaultString();
199 }
200
9e834013
NR
201 return value;
202 }
203
76b51de9
NR
204 /**
205 * The default value of this item, as a {@link String}.
206 *
207 * @return the default value
208 */
9e834013
NR
209 public String getDefaultString() {
210 return meta.def();
211 }
212
76b51de9
NR
213 /**
214 * The value stored by this item, as a {@link Boolean}.
215 *
d18e136e
NR
216 * @param useDefaultIfEmpty
217 * use the default value instead of NULL if the setting is not
218 * set
219 *
76b51de9
NR
220 * @return the value
221 */
d18e136e
NR
222 public Boolean getBoolean(boolean useDefaultIfEmpty) {
223 return BundleHelper.parseBoolean(getString(useDefaultIfEmpty));
9e834013
NR
224 }
225
76b51de9
NR
226 /**
227 * The default value of this item, as a {@link Boolean}.
228 *
229 * @return the default value
230 */
9e834013
NR
231 public Boolean getDefaultBoolean() {
232 return BundleHelper.parseBoolean(getDefaultString());
233 }
234
76b51de9
NR
235 /**
236 * The value stored by this item, as a {@link Character}.
237 *
d18e136e
NR
238 * @param useDefaultIfEmpty
239 * use the default value instead of NULL if the setting is not
240 * set
241 *
76b51de9
NR
242 * @return the value
243 */
d18e136e
NR
244 public Character getCharacter(boolean useDefaultIfEmpty) {
245 return BundleHelper.parseCharacter(getString(useDefaultIfEmpty));
9e834013
NR
246 }
247
76b51de9
NR
248 /**
249 * The default value of this item, as a {@link Character}.
250 *
251 * @return the default value
252 */
9e834013
NR
253 public Character getDefaultCharacter() {
254 return BundleHelper.parseCharacter(getDefaultString());
255 }
256
76b51de9
NR
257 /**
258 * The value stored by this item, as an {@link Integer}.
259 *
d18e136e
NR
260 * @param useDefaultIfEmpty
261 * use the default value instead of NULL if the setting is not
262 * set
263 *
76b51de9
NR
264 * @return the value
265 */
d18e136e
NR
266 public Integer getInteger(boolean useDefaultIfEmpty) {
267 return BundleHelper.parseInteger(getString(useDefaultIfEmpty));
9e834013
NR
268 }
269
76b51de9
NR
270 /**
271 * The default value of this item, as an {@link Integer}.
272 *
273 * @return the default value
274 */
9e834013
NR
275 public Integer getDefaultInteger() {
276 return BundleHelper.parseInteger(getDefaultString());
277 }
278
76b51de9
NR
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 *
d18e136e
NR
285 * @param useDefaultIfEmpty
286 * use the default value instead of NULL if the setting is not
287 * set
288 *
76b51de9
NR
289 * @return the value
290 */
d18e136e
NR
291 public Integer getColor(boolean useDefaultIfEmpty) {
292 return BundleHelper.parseColor(getString(useDefaultIfEmpty));
9e834013
NR
293 }
294
76b51de9
NR
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 */
9e834013
NR
303 public Integer getDefaultColor() {
304 return BundleHelper.parseColor(getDefaultString());
305 }
306
76b51de9
NR
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 *
d18e136e
NR
313 * @param useDefaultIfEmpty
314 * use the default value instead of NULL if the setting is not
315 * set
316 *
76b51de9
NR
317 * @return the value
318 */
d18e136e
NR
319 public List<String> getList(boolean useDefaultIfEmpty) {
320 return BundleHelper.parseList(getString(useDefaultIfEmpty));
9e834013
NR
321 }
322
76b51de9
NR
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 */
9e834013
NR
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
76b51de9
NR
345 /**
346 * The value stored by this item, as a {@link Boolean}.
347 *
348 * @param value
349 * the new value
350 */
9e834013
NR
351 public void setBoolean(boolean value) {
352 setString(BundleHelper.fromBoolean(value));
353 }
354
76b51de9
NR
355 /**
356 * The value stored by this item, as a {@link Character}.
357 *
358 * @param value
359 * the new value
360 */
9e834013
NR
361 public void setCharacter(char value) {
362 setString(BundleHelper.fromCharacter(value));
363 }
364
76b51de9
NR
365 /**
366 * The value stored by this item, as an {@link Integer}.
367 *
368 * @param value
369 * the new value
370 */
9e834013
NR
371 public void setInteger(int value) {
372 setString(BundleHelper.fromInteger(value));
373 }
374
76b51de9
NR
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 */
9e834013
NR
384 public void setColor(int value) {
385 setString(BundleHelper.fromColor(value));
386 }
387
76b51de9
NR
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 */
9e834013
NR
398 public void setList(List<String> value) {
399 setString(BundleHelper.fromList(value));
400 }
401
402 /**
76b51de9
NR
403 * Reload the value from the {@link Bundle}, so the last value that was
404 * saved will be used.
9e834013
NR
405 */
406 public void reload() {
407 value = bundle.getString(id);
8517b60c 408 for (Runnable listener : reloadedListeners) {
9e834013
NR
409 try {
410 listener.run();
411 } catch (Exception e) {
412 // TODO: error management?
413 e.printStackTrace();
414 }
415 }
416 }
417
76b51de9
NR
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 */
8517b60c
NR
426 public void addReloadedListener(Runnable listener) {
427 reloadedListeners.add(listener);
9e834013
NR
428 }
429
430 /**
431 * Save the current value to the {@link Bundle}.
432 */
433 public void save() {
8517b60c
NR
434 for (Runnable listener : saveListeners) {
435 try {
436 listener.run();
437 } catch (Exception e) {
438 // TODO: error management?
439 e.printStackTrace();
440 }
441 }
9e834013
NR
442 bundle.setString(id, value);
443 }
444
76b51de9
NR
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 */
8517b60c
NR
454 public void addSaveListener(Runnable listener) {
455 saveListeners.add(listener);
456 }
457
76b51de9
NR
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
0877d6f5
NR
477 @Override
478 public Iterator<MetaInfo<E>> iterator() {
479 return children.iterator();
480 }
481
9e834013
NR
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>>();
76b51de9 498 List<MetaInfo<E>> shadow = new ArrayList<MetaInfo<E>>();
9e834013 499 for (E id : type.getEnumConstants()) {
76b51de9
NR
500 MetaInfo<E> info = new MetaInfo<E>(type, bundle, id);
501 list.add(info);
502 shadow.add(info);
9e834013
NR
503 }
504
76b51de9
NR
505 for (int i = 0; i < list.size(); i++) {
506 MetaInfo<E> info = list.get(i);
8517b60c 507
76b51de9
NR
508 MetaInfo<E> parent = findParent(info, shadow);
509 if (parent != null) {
510 list.remove(i--);
511 parent.children.add(info);
d18e136e 512 info.name = idToName(info.id, parent.id);
8517b60c
NR
513 }
514 }
515
76b51de9 516 return list;
8517b60c
NR
517 }
518
76b51de9
NR
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 */
8517b60c 537 static private <E extends Enum<E>> MetaInfo<E> findParent(MetaInfo<E> info,
76b51de9
NR
538 List<MetaInfo<E>> candidates) {
539 String id = info.id.toString();
8517b60c
NR
540 MetaInfo<E> group = null;
541 for (MetaInfo<E> pcandidate : candidates) {
76b51de9
NR
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 }
8517b60c
NR
550 }
551 }
552 }
553
554 return group;
555 }
d18e136e
NR
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 }
9e834013 572}