ConfigItem: fix some errors, add jDoc
[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
66 if (description == null) {
67 description = meta.description();
68 if (description == null) {
69 description = "";
70 }
71 if (meta.info() != null && !meta.info().isEmpty()) {
72 if (!description.isEmpty()) {
73 description += "\n\n";
74 }
75 description += meta.info();
76 }
77 }
78
79 String name = id.toString();
80 if (name.length() > 1) {
81 name = name.substring(0, 1) + name.substring(1).toLowerCase();
82 name = name.replace("_", " ");
83 }
84
85 this.name = name;
86 this.description = description;
87
88 reload();
89 }
90
91 /**
92 * The name of this item, deduced from its ID.
93 * <p>
94 * In other words, it is the ID but presented in a displayable form.
95 *
96 * @return the name
97 */
98 public String getName() {
99 return name;
100 }
101
102 /**
103 * The description of this item (information to present to the user).
104 *
105 * @return the description
106 */
107 public String getDescription() {
108 return description;
109 }
110
111 /**
112 * The format this item is supposed to follow
113 *
114 * @return the format
115 */
116 public Format getFormat() {
117 return meta.format();
118 }
119
120 /**
121 * The allowed list of values that a {@link Format#FIXED_LIST} item is
122 * allowed to be, or a list of suggestions for {@link Format#COMBO_LIST}
123 * items.
124 *
125 * @return the list of values
126 */
127 public String[] getAllowedValues() {
128 return meta.list();
129 }
130
131 /**
132 * This item is a comma-separated list of values instead of a single value.
133 * <p>
134 * The list items are separated by a comma, each surrounded by
135 * double-quotes, with backslashes and double-quotes escaped by a backslash.
136 * <p>
137 * Example: <tt>"un", "deux"</tt>
138 *
139 * @return TRUE if it is
140 */
141 public boolean isArray() {
142 return meta.array();
143 }
144
145 /**
146 * This item is only used as a group, not as an option.
147 * <p>
148 * For instance, you could have LANGUAGE_CODE as a group for which you won't
149 * use the value in the program, and LANGUAGE_CODE_FR, LANGUAGE_CODE_EN
150 * inside for which the value must be set.
151 *
152 * @return TRUE if it is a group
153 */
154 public boolean isGroup() {
155 return meta.group();
156 }
157
158 /**
159 * The value stored by this item, as a {@link String}.
160 *
161 * @return the value
162 */
163 public String getString() {
164 return value;
165 }
166
167 /**
168 * The default value of this item, as a {@link String}.
169 *
170 * @return the default value
171 */
172 public String getDefaultString() {
173 return meta.def();
174 }
175
176 /**
177 * The value stored by this item, as a {@link Boolean}.
178 *
179 * @return the value
180 */
181 public Boolean getBoolean() {
182 return BundleHelper.parseBoolean(getString());
183 }
184
185 /**
186 * The default value of this item, as a {@link Boolean}.
187 *
188 * @return the default value
189 */
190 public Boolean getDefaultBoolean() {
191 return BundleHelper.parseBoolean(getDefaultString());
192 }
193
194 /**
195 * The value stored by this item, as a {@link Character}.
196 *
197 * @return the value
198 */
199 public Character getCharacter() {
200 return BundleHelper.parseCharacter(getString());
201 }
202
203 /**
204 * The default value of this item, as a {@link Character}.
205 *
206 * @return the default value
207 */
208 public Character getDefaultCharacter() {
209 return BundleHelper.parseCharacter(getDefaultString());
210 }
211
212 /**
213 * The value stored by this item, as an {@link Integer}.
214 *
215 * @return the value
216 */
217 public Integer getInteger() {
218 return BundleHelper.parseInteger(getString());
219 }
220
221 /**
222 * The default value of this item, as an {@link Integer}.
223 *
224 * @return the default value
225 */
226 public Integer getDefaultInteger() {
227 return BundleHelper.parseInteger(getDefaultString());
228 }
229
230 /**
231 * The value stored by this item, as a colour (represented here as an
232 * {@link Integer}) if it represents a colour, or NULL if it doesn't.
233 * <p>
234 * The returned colour value is an ARGB value.
235 *
236 * @return the value
237 */
238 public Integer getColor() {
239 return BundleHelper.parseColor(getString());
240 }
241
242 /**
243 * The default value stored by this item, as a colour (represented here as
244 * an {@link Integer}) if it represents a colour, or NULL if it doesn't.
245 * <p>
246 * The returned colour value is an ARGB value.
247 *
248 * @return the value
249 */
250 public Integer getDefaultColor() {
251 return BundleHelper.parseColor(getDefaultString());
252 }
253
254 /**
255 * A {@link String} representation of the list of values.
256 * <p>
257 * The list of values is comma-separated and each value is surrounded by
258 * double-quotes; backslashes and double-quotes are escaped by a backslash.
259 *
260 * @return the value
261 */
262 public List<String> getList() {
263 return BundleHelper.parseList(getString());
264 }
265
266 /**
267 * A {@link String} representation of the default list of values.
268 * <p>
269 * The list of values is comma-separated and each value is surrounded by
270 * double-quotes; backslashes and double-quotes are escaped by a backslash.
271 *
272 * @return the value
273 */
274 public List<String> getDefaultList() {
275 return BundleHelper.parseList(getDefaultString());
276 }
277
278 /**
279 * The value stored by this item, as a {@link String}.
280 *
281 * @param value
282 * the new value
283 */
284 public void setString(String value) {
285 this.value = value;
286 }
287
288 /**
289 * The value stored by this item, as a {@link Boolean}.
290 *
291 * @param value
292 * the new value
293 */
294 public void setBoolean(boolean value) {
295 setString(BundleHelper.fromBoolean(value));
296 }
297
298 /**
299 * The value stored by this item, as a {@link Character}.
300 *
301 * @param value
302 * the new value
303 */
304 public void setCharacter(char value) {
305 setString(BundleHelper.fromCharacter(value));
306 }
307
308 /**
309 * The value stored by this item, as an {@link Integer}.
310 *
311 * @param value
312 * the new value
313 */
314 public void setInteger(int value) {
315 setString(BundleHelper.fromInteger(value));
316 }
317
318 /**
319 * The value stored by this item, as a colour (represented here as an
320 * {@link Integer}) if it represents a colour, or NULL if it doesn't.
321 * <p>
322 * The returned colour value is an ARGB value.
323 *
324 * @param value
325 * the value
326 */
327 public void setColor(int value) {
328 setString(BundleHelper.fromColor(value));
329 }
330
331 /**
332 * A {@link String} representation of the default list of values.
333 * <p>
334 * The list of values is comma-separated and each value is surrounded by
335 * double-quotes; backslashes and double-quotes are escaped by a backslash.
336 *
337 * @param value
338 * the {@link String} representation
339 *
340 */
341 public void setList(List<String> value) {
342 setString(BundleHelper.fromList(value));
343 }
344
345 /**
346 * Reload the value from the {@link Bundle}, so the last value that was
347 * saved will be used.
348 */
349 public void reload() {
350 value = bundle.getString(id);
351 for (Runnable listener : reloadedListeners) {
352 try {
353 listener.run();
354 } catch (Exception e) {
355 // TODO: error management?
356 e.printStackTrace();
357 }
358 }
359 }
360
361 /**
362 * Add a listener that will be called <b>after</b> a reload operation.
363 * <p>
364 * You could use it to refresh the UI for instance.
365 *
366 * @param listener
367 * the listener
368 */
369 public void addReloadedListener(Runnable listener) {
370 reloadedListeners.add(listener);
371 }
372
373 /**
374 * Save the current value to the {@link Bundle}.
375 */
376 public void save() {
377 for (Runnable listener : saveListeners) {
378 try {
379 listener.run();
380 } catch (Exception e) {
381 // TODO: error management?
382 e.printStackTrace();
383 }
384 }
385 bundle.setString(id, value);
386 }
387
388 /**
389 * Add a listener that will be called <b>before</b> a save operation.
390 * <p>
391 * You could use it to make some modification to the stored value before it
392 * is saved.
393 *
394 * @param listener
395 * the listener
396 */
397 public void addSaveListener(Runnable listener) {
398 saveListeners.add(listener);
399 }
400
401 /**
402 * The sub-items if any (if no sub-items, will return an empty list).
403 * <p>
404 * Sub-items are declared when a {@link Meta} has an ID that starts with the
405 * ID of a {@link Meta#group()} {@link MetaInfo}.
406 * <p>
407 * For instance:
408 * <ul>
409 * <li>{@link Meta} <tt>MY_PREFIX</tt> is a {@link Meta#group()}</li>
410 * <li>{@link Meta} <tt>MY_PREFIX_DESCRIPTION</tt> is another {@link Meta}</li>
411 * <li><tt>MY_PREFIX_DESCRIPTION</tt> will be a child of <tt>MY_PREFIX</tt></li>
412 * </ul>
413 *
414 * @return the sub-items if any
415 */
416 public List<MetaInfo<E>> getChildren() {
417 return children;
418 }
419
420 @Override
421 public Iterator<MetaInfo<E>> iterator() {
422 return children.iterator();
423 }
424
425 /**
426 * Create a list of {@link MetaInfo}, one for each of the item in the given
427 * {@link Bundle}.
428 *
429 * @param <E>
430 * the type of {@link Bundle} to edit
431 * @param type
432 * a class instance of the item type to work on
433 * @param bundle
434 * the {@link Bundle} to sort through
435 *
436 * @return the list
437 */
438 static public <E extends Enum<E>> List<MetaInfo<E>> getItems(Class<E> type,
439 Bundle<E> bundle) {
440 List<MetaInfo<E>> list = new ArrayList<MetaInfo<E>>();
441 List<MetaInfo<E>> shadow = new ArrayList<MetaInfo<E>>();
442 for (E id : type.getEnumConstants()) {
443 MetaInfo<E> info = new MetaInfo<E>(type, bundle, id);
444 list.add(info);
445 shadow.add(info);
446 }
447
448 for (int i = 0; i < list.size(); i++) {
449 MetaInfo<E> info = list.get(i);
450
451 MetaInfo<E> parent = findParent(info, shadow);
452 if (parent != null) {
453 list.remove(i--);
454 parent.children.add(info);
455 }
456 }
457
458 return list;
459 }
460
461 /**
462 * Find the longest parent of the given {@link MetaInfo}, which means:
463 * <ul>
464 * <li>the parent is a {@link Meta#group()}</li>
465 * <li>the parent Id is a substring of the Id of the given {@link MetaInfo}</li>
466 * <li>there is no other parent sharing a substring for this
467 * {@link MetaInfo} with a longer Id</li>
468 * </ul>
469 *
470 * @param <E>
471 * the kind of enum
472 * @param info
473 * the info to look for a parent for
474 * @param candidates
475 * the list of potential parents
476 *
477 * @return the longest parent or NULL if no parent is found
478 */
479 static private <E extends Enum<E>> MetaInfo<E> findParent(MetaInfo<E> info,
480 List<MetaInfo<E>> candidates) {
481 String id = info.id.toString();
482 MetaInfo<E> group = null;
483 for (MetaInfo<E> pcandidate : candidates) {
484 if (pcandidate.isGroup()) {
485 String candidateId = pcandidate.id.toString();
486 if (!id.equals(candidateId) && id.startsWith(candidateId)) {
487 if (group == null
488 || group.id.toString().length() < candidateId
489 .length()) {
490 group = pcandidate;
491 }
492 }
493 }
494 }
495
496 return group;
497 }
498 }