package be.nikiroo.utils.resources;
import java.util.ArrayList;
+import java.util.Iterator;
import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.TreeMap;
import be.nikiroo.utils.resources.Meta.Format;
* @param <E>
* the type of {@link Bundle} to edit
*/
-public class MetaInfo<E extends Enum<E>> {
+public class MetaInfo<E extends Enum<E>> implements Iterable<MetaInfo<E>> {
private final Bundle<E> bundle;
private final E id;
private Meta meta;
+ private List<MetaInfo<E>> children = new ArrayList<MetaInfo<E>>();
private String value;
private List<Runnable> reloadedListeners = new ArrayList<Runnable>();
private String name;
private String description;
+ private boolean dirty;
+
+ /**
+ * Create a new {@link MetaInfo} from a value (without children).
+ * <p>
+ * For instance, you can call
+ * <tt>new MetaInfo(Config.class, configBundle, Config.MY_VALUE)</tt>.
+ *
+ * @param type
+ * the type of enum the value is
+ * @param bundle
+ * the bundle this value belongs to
+ * @param id
+ * the value itself
+ */
public MetaInfo(Class<E> type, Bundle<E> bundle, E id) {
this.bundle = bundle;
this.id = id;
description = null;
}
}
-
if (description == null) {
description = meta.description();
- if (meta.info() != null && !meta.info().isEmpty()) {
- description += " (" + meta.info() + ")";
+ if (description == null) {
+ description = "";
}
}
- String name = id.toString();
- if (name.length() > 1) {
- name = name.substring(0, 1) + name.substring(1).toLowerCase();
- name = name.replace("_", " ");
+ String name = idToName(id, null);
+
+ // Special rules for groups:
+ if (meta.group()) {
+ String groupName = description.split("\n")[0];
+ description = description.substring(groupName.length()).trim();
+ if (!groupName.isEmpty()) {
+ name = groupName;
+ }
+ }
+
+ if (meta.def() != null && !meta.def().isEmpty()) {
+ if (!description.isEmpty()) {
+ description += "\n\n";
+ }
+ description += "(Default value: " + meta.def() + ")";
}
this.name = name;
}
/**
- * THe name of this item, deduced from its ID.
+ * For normal items, this is the name of this item, deduced from its ID (or
+ * in other words, it is the ID but presented in a displayable form).
* <p>
- * In other words, it is the ID but presented in a displayable form.
+ * For group items, this is the first line of the description if it is not
+ * empty (else, it is the ID in the same way as normal items).
+ * <p>
+ * Never NULL.
+ *
*
- * @return the name
+ * @return the name, never NULL
*/
public String getName() {
return name;
}
/**
- * The description of this item (information to present to the user).
+ * A description for this item: what it is or does, how to explain that item
+ * to the user including what can be used here (i.e., %s = file name, %d =
+ * file size...).
+ * <p>
+ * For group, the first line ('\\n'-separated) will be used as a title while
+ * the rest will be the description.
+ * <p>
+ * If a default value is known, it will be specified here, too.
+ * <p>
+ * Never NULL.
*
- * @return the description
+ * @return the description, not NULL
*/
public String getDescription() {
return description;
}
+ /**
+ * The format this item is supposed to follow
+ *
+ * @return the format
+ */
public Format getFormat() {
return meta.format();
}
+ /**
+ * The allowed list of values that a {@link Format#FIXED_LIST} item is
+ * allowed to be, or a list of suggestions for {@link Format#COMBO_LIST}
+ * items. Also works for {@link Format#LOCALE}.
+ * <p>
+ * Will always allow an empty string in addition to the rest.
+ *
+ * @return the list of values
+ */
+ public String[] getAllowedValues() {
+ String[] list = meta.list();
+
+ String[] withEmpty = new String[list.length + 1];
+ withEmpty[0] = "";
+ for (int i = 0; i < list.length; i++) {
+ withEmpty[i + 1] = list[i];
+ }
+
+ return withEmpty;
+ }
+
+ /**
+ * Return all the languages known by the program for this bundle.
+ * <p>
+ * This only works for {@link TransBundle}, and will return an empty list if
+ * this is not a {@link TransBundle}.
+ *
+ * @return the known language codes
+ */
+ public List<String> getKnownLanguages() {
+ if (bundle instanceof TransBundle) {
+ return ((TransBundle<E>) bundle).getKnownLanguages();
+ }
+
+ return new ArrayList<String>();
+ }
+
+ /**
+ * This item is a comma-separated list of values instead of a single value.
+ * <p>
+ * The list items are separated by a comma, each surrounded by
+ * double-quotes, with backslashes and double-quotes escaped by a backslash.
+ * <p>
+ * Example: <tt>"un", "deux"</tt>
+ *
+ * @return TRUE if it is
+ */
+ public boolean isArray() {
+ return meta.array();
+ }
+
+ /**
+ * A manual flag to specify if the data has been changed or not, which can
+ * be used by {@link MetaInfo#save(boolean)}.
+ *
+ * @return TRUE if it is dirty (if it has changed)
+ */
+ public boolean isDirty() {
+ return dirty;
+ }
+
+ /**
+ * A manual flag to specify that the data has been changed, which can be
+ * used by {@link MetaInfo#save(boolean)}.
+ */
+ public void setDirty() {
+ this.dirty = true;
+ }
+
+ /**
+ * The number of items in this item if it {@link MetaInfo#isArray()}, or -1
+ * if not.
+ *
+ * @param useDefaultIfEmpty
+ * check the size of the default list instead if the list is
+ * empty
+ *
+ * @return -1 or the number of items
+ */
+ public int getListSize(boolean useDefaultIfEmpty) {
+ if (!isArray()) {
+ return -1;
+ }
+
+ return BundleHelper.getListSize(getString(-1, useDefaultIfEmpty));
+ }
+
+ /**
+ * This item is only used as a group, not as an option.
+ * <p>
+ * For instance, you could have LANGUAGE_CODE as a group for which you won't
+ * use the value in the program, and LANGUAGE_CODE_FR, LANGUAGE_CODE_EN
+ * inside for which the value must be set.
+ *
+ * @return TRUE if it is a group
+ */
+ public boolean isGroup() {
+ return meta.group();
+ }
+
/**
* The value stored by this item, as a {@link String}.
*
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ * @param useDefaultIfEmpty
+ * use the default value instead of NULL if the setting is not
+ * set
+ *
* @return the value
*/
- public String getString() {
+ public String getString(int item, boolean useDefaultIfEmpty) {
+ if (isArray() && item >= 0) {
+ List<String> values = BundleHelper.parseList(value, -1);
+ if (values != null && item < values.size()) {
+ return values.get(item);
+ }
+
+ if (useDefaultIfEmpty) {
+ return getDefaultString(item);
+ }
+
+ return null;
+ }
+
+ if (value == null && useDefaultIfEmpty) {
+ return getDefaultString(item);
+ }
+
return value;
}
- public String getDefaultString() {
+ /**
+ * The default value of this item, as a {@link String}.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ *
+ * @return the default value
+ */
+ public String getDefaultString(int item) {
+ if (isArray() && item >= 0) {
+ List<String> values = BundleHelper.parseList(meta.def(), item);
+ if (values != null && item < values.size()) {
+ return values.get(item);
+ }
+
+ return null;
+ }
+
return meta.def();
}
- public Boolean getBoolean() {
- return BundleHelper.parseBoolean(getString());
+ /**
+ * The value stored by this item, as a {@link Boolean}.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ * @param useDefaultIfEmpty
+ * use the default value instead of NULL if the setting is not
+ * set
+ *
+ * @return the value
+ */
+ public Boolean getBoolean(int item, boolean useDefaultIfEmpty) {
+ return BundleHelper
+ .parseBoolean(getString(item, useDefaultIfEmpty), -1);
}
- public Boolean getDefaultBoolean() {
- return BundleHelper.parseBoolean(getDefaultString());
+ /**
+ * The default value of this item, as a {@link Boolean}.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ *
+ * @return the default value
+ */
+ public Boolean getDefaultBoolean(int item) {
+ return BundleHelper.parseBoolean(getDefaultString(item), -1);
}
- public Character getCharacter() {
- return BundleHelper.parseCharacter(getString());
+ /**
+ * The value stored by this item, as a {@link Character}.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ * @param useDefaultIfEmpty
+ * use the default value instead of NULL if the setting is not
+ * set
+ *
+ * @return the value
+ */
+ public Character getCharacter(int item, boolean useDefaultIfEmpty) {
+ return BundleHelper.parseCharacter(getString(item, useDefaultIfEmpty),
+ -1);
}
- public Character getDefaultCharacter() {
- return BundleHelper.parseCharacter(getDefaultString());
+ /**
+ * The default value of this item, as a {@link Character}.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ *
+ * @return the default value
+ */
+ public Character getDefaultCharacter(int item) {
+ return BundleHelper.parseCharacter(getDefaultString(item), -1);
}
- public Integer getInteger() {
- return BundleHelper.parseInteger(getString());
+ /**
+ * The value stored by this item, as an {@link Integer}.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ * @param useDefaultIfEmpty
+ * use the default value instead of NULL if the setting is not
+ * set
+ *
+ * @return the value
+ */
+ public Integer getInteger(int item, boolean useDefaultIfEmpty) {
+ return BundleHelper
+ .parseInteger(getString(item, useDefaultIfEmpty), -1);
}
- public Integer getDefaultInteger() {
- return BundleHelper.parseInteger(getDefaultString());
+ /**
+ * The default value of this item, as an {@link Integer}.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ *
+ * @return the default value
+ */
+ public Integer getDefaultInteger(int item) {
+ return BundleHelper.parseInteger(getDefaultString(item), -1);
}
- public Integer getColor() {
- return BundleHelper.parseColor(getString());
+ /**
+ * The value stored by this item, as a colour (represented here as an
+ * {@link Integer}) if it represents a colour, or NULL if it doesn't.
+ * <p>
+ * The returned colour value is an ARGB value.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ * @param useDefaultIfEmpty
+ * use the default value instead of NULL if the setting is not
+ * set
+ *
+ * @return the value
+ */
+ public Integer getColor(int item, boolean useDefaultIfEmpty) {
+ return BundleHelper.parseColor(getString(item, useDefaultIfEmpty), -1);
}
- public Integer getDefaultColor() {
- return BundleHelper.parseColor(getDefaultString());
+ /**
+ * The default value stored by this item, as a colour (represented here as
+ * an {@link Integer}) if it represents a colour, or NULL if it doesn't.
+ * <p>
+ * The returned colour value is an ARGB value.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ *
+ * @return the value
+ */
+ public Integer getDefaultColor(int item) {
+ return BundleHelper.parseColor(getDefaultString(item), -1);
}
- public List<String> getList() {
- return BundleHelper.parseList(getString());
+ /**
+ * A {@link String} representation of the list of values.
+ * <p>
+ * The list of values is comma-separated and each value is surrounded by
+ * double-quotes; backslashes and double-quotes are escaped by a backslash.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ * @param useDefaultIfEmpty
+ * use the default value instead of NULL if the setting is not
+ * set
+ *
+ * @return the value
+ */
+ public List<String> getList(int item, boolean useDefaultIfEmpty) {
+ return BundleHelper.parseList(getString(item, useDefaultIfEmpty), -1);
}
- public List<String> getDefaultList() {
- return BundleHelper.parseList(getDefaultString());
+ /**
+ * A {@link String} representation of the default list of values.
+ * <p>
+ * The list of values is comma-separated and each value is surrounded by
+ * double-quotes; backslashes and double-quotes are escaped by a backslash.
+ *
+ * @param item
+ * the item number to get for an array of values, or -1 to get
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ *
+ * @return the value
+ */
+ public List<String> getDefaultList(int item) {
+ return BundleHelper.parseList(getDefaultString(item), -1);
}
/**
*
* @param value
* the new value
+ * @param item
+ * the item number to set for an array of values, or -1 to set
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
*/
- public void setString(String value) {
- this.value = value;
+ public void setString(String value, int item) {
+ if (isArray() && item >= 0) {
+ this.value = BundleHelper.fromList(this.value, value, item);
+ } else {
+ this.value = value;
+ }
}
- public void setBoolean(boolean value) {
- setString(BundleHelper.fromBoolean(value));
+ /**
+ * The value stored by this item, as a {@link Boolean}.
+ *
+ * @param value
+ * the new value
+ * @param item
+ * the item number to set for an array of values, or -1 to set
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ */
+ public void setBoolean(boolean value, int item) {
+ setString(BundleHelper.fromBoolean(value), item);
}
- public void setCharacter(char value) {
- setString(BundleHelper.fromCharacter(value));
+ /**
+ * The value stored by this item, as a {@link Character}.
+ *
+ * @param value
+ * the new value
+ * @param item
+ * the item number to set for an array of values, or -1 to set
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ */
+ public void setCharacter(char value, int item) {
+ setString(BundleHelper.fromCharacter(value), item);
}
- public void setInteger(int value) {
- setString(BundleHelper.fromInteger(value));
+ /**
+ * The value stored by this item, as an {@link Integer}.
+ *
+ * @param value
+ * the new value
+ * @param item
+ * the item number to set for an array of values, or -1 to set
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ */
+ public void setInteger(int value, int item) {
+ setString(BundleHelper.fromInteger(value), item);
}
- public void setColor(int value) {
- setString(BundleHelper.fromColor(value));
+ /**
+ * The value stored by this item, as a colour (represented here as an
+ * {@link Integer}) if it represents a colour, or NULL if it doesn't.
+ * <p>
+ * The colour value is an ARGB value.
+ *
+ * @param value
+ * the value
+ * @param item
+ * the item number to set for an array of values, or -1 to set
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ */
+ public void setColor(int value, int item) {
+ setString(BundleHelper.fromColor(value), item);
}
- public void setList(List<String> value) {
- setString(BundleHelper.fromList(value));
+ /**
+ * A {@link String} representation of the default list of values.
+ * <p>
+ * The list of values is comma-separated and each value is surrounded by
+ * double-quotes; backslashes and double-quotes are escaped by a backslash.
+ *
+ * @param value
+ * the {@link String} representation
+ * @param item
+ * the item number to set for an array of values, or -1 to set
+ * the whole value (has no effect if {@link MetaInfo#isArray()}
+ * is FALSE)
+ */
+ public void setList(List<String> value, int item) {
+ setString(BundleHelper.fromList(value), item);
}
/**
- * Reload the value from the {@link Bundle}.
+ * Reload the value from the {@link Bundle}, so the last value that was
+ * saved will be used.
*/
public void reload() {
- value = bundle.getString(id);
- for (Runnable listener : reloadedListeners) {
+ if (bundle.isSet(id, false)) {
+ value = bundle.getString(id);
+ } else {
+ value = null;
+ }
+
+ // Copy the list so we can create new listener in a listener
+ for (Runnable listener : new ArrayList<Runnable>(reloadedListeners)) {
try {
listener.run();
} catch (Exception e) {
- // TODO: error management?
e.printStackTrace();
}
}
}
- // listeners will be called AFTER reload
+ /**
+ * Add a listener that will be called <b>after</b> a reload operation.
+ * <p>
+ * You could use it to refresh the UI for instance.
+ *
+ * @param listener
+ * the listener
+ */
public void addReloadedListener(Runnable listener) {
reloadedListeners.add(listener);
}
/**
* Save the current value to the {@link Bundle}.
+ * <p>
+ * Note that listeners will be called <b>before</b> the dirty check and
+ * <b>before</b> saving the value.
+ *
+ * @param onlyIfDirty
+ * only save the data if the dirty flag is set (will reset the
+ * dirty flag)
*/
- public void save() {
- for (Runnable listener : saveListeners) {
+ public void save(boolean onlyIfDirty) {
+ // Copy the list so we can create new listener in a listener
+ for (Runnable listener : new ArrayList<Runnable>(saveListeners)) {
try {
listener.run();
} catch (Exception e) {
- // TODO: error management?
e.printStackTrace();
}
}
- bundle.setString(id, value);
+
+ if (!onlyIfDirty || isDirty()) {
+ bundle.setString(id, value);
+ }
}
- // listeners will be called BEFORE save
+ /**
+ * Add a listener that will be called <b>before</b> a save operation.
+ * <p>
+ * You could use it to make some modification to the stored value before it
+ * is saved.
+ *
+ * @param listener
+ * the listener
+ */
public void addSaveListener(Runnable listener) {
saveListeners.add(listener);
}
+ /**
+ * The sub-items if any (if no sub-items, will return an empty list).
+ * <p>
+ * Sub-items are declared when a {@link Meta} has an ID that starts with the
+ * ID of a {@link Meta#group()} {@link MetaInfo}.
+ * <p>
+ * For instance:
+ * <ul>
+ * <li>{@link Meta} <tt>MY_PREFIX</tt> is a {@link Meta#group()}</li>
+ * <li>{@link Meta} <tt>MY_PREFIX_DESCRIPTION</tt> is another {@link Meta}</li>
+ * <li><tt>MY_PREFIX_DESCRIPTION</tt> will be a child of <tt>MY_PREFIX</tt></li>
+ * </ul>
+ *
+ * @return the sub-items if any
+ */
+ public List<MetaInfo<E>> getChildren() {
+ return children;
+ }
+
+ /**
+ * The number of sub-items, if any.
+ *
+ * @return the number or 0
+ */
+ public int size() {
+ return children.size();
+ }
+
+ @Override
+ public Iterator<MetaInfo<E>> iterator() {
+ return children.iterator();
+ }
+
/**
* Create a list of {@link MetaInfo}, one for each of the item in the given
* {@link Bundle}.
static public <E extends Enum<E>> List<MetaInfo<E>> getItems(Class<E> type,
Bundle<E> bundle) {
List<MetaInfo<E>> list = new ArrayList<MetaInfo<E>>();
+ List<MetaInfo<E>> shadow = new ArrayList<MetaInfo<E>>();
for (E id : type.getEnumConstants()) {
- list.add(new MetaInfo<E>(type, bundle, id));
+ MetaInfo<E> info = new MetaInfo<E>(type, bundle, id);
+ list.add(info);
+ shadow.add(info);
}
- return list;
- }
+ for (int i = 0; i < list.size(); i++) {
+ MetaInfo<E> info = list.get(i);
- // TODO: multiple levels?
- static public <E extends Enum<E>> Map<MetaInfo<E>, List<MetaInfo<E>>> getGroupedItems(
- Class<E> type, Bundle<E> bundle) {
- Map<MetaInfo<E>, List<MetaInfo<E>>> map = new TreeMap<MetaInfo<E>, List<MetaInfo<E>>>();
- Map<MetaInfo<E>, List<MetaInfo<E>>> map1 = new TreeMap<MetaInfo<E>, List<MetaInfo<E>>>();
-
- List<MetaInfo<E>> ungrouped = new ArrayList<MetaInfo<E>>();
- for (MetaInfo<E> info : getItems(type, bundle)) {
- if (info.meta.group()) {
- List<MetaInfo<E>> list = new ArrayList<MetaInfo<E>>();
- map.put(info, list);
- map1.put(info, list);
- } else {
- ungrouped.add(info);
+ MetaInfo<E> parent = findParent(info, shadow);
+ if (parent != null) {
+ list.remove(i--);
+ parent.children.add(info);
+ info.name = idToName(info.id, parent.id);
}
}
- for (int i = 0; i < ungrouped.size(); i++) {
- MetaInfo<E> info = ungrouped.get(i);
- MetaInfo<E> group = findParent(info, map.keySet());
- if (group != null) {
- map.get(group).add(info);
- ungrouped.remove(i--);
- }
- }
-
- if (ungrouped.size() > 0) {
- map.put(null, ungrouped);
- }
-
- return map;
+ return list;
}
+ /**
+ * Find the longest parent of the given {@link MetaInfo}, which means:
+ * <ul>
+ * <li>the parent is a {@link Meta#group()}</li>
+ * <li>the parent Id is a substring of the Id of the given {@link MetaInfo}</li>
+ * <li>there is no other parent sharing a substring for this
+ * {@link MetaInfo} with a longer Id</li>
+ * </ul>
+ *
+ * @param <E>
+ * the kind of enum
+ * @param info
+ * the info to look for a parent for
+ * @param candidates
+ * the list of potential parents
+ *
+ * @return the longest parent or NULL if no parent is found
+ */
static private <E extends Enum<E>> MetaInfo<E> findParent(MetaInfo<E> info,
- Set<MetaInfo<E>> candidates) {
+ List<MetaInfo<E>> candidates) {
+ String id = info.id.toString();
MetaInfo<E> group = null;
for (MetaInfo<E> pcandidate : candidates) {
- if (info.id.toString().startsWith(pcandidate.id.toString())) {
- if (group == null
- || group.id.toString().length() < pcandidate.id
- .toString().length()) {
- group = pcandidate;
+ if (pcandidate.isGroup()) {
+ String candidateId = pcandidate.id.toString();
+ if (!id.equals(candidateId) && id.startsWith(candidateId)) {
+ if (group == null
+ || group.id.toString().length() < candidateId
+ .length()) {
+ group = pcandidate;
+ }
}
}
}
return group;
}
+
+ static private <E extends Enum<E>> String idToName(E id, E prefix) {
+ String name = id.toString();
+ if (prefix != null && name.startsWith(prefix.toString())) {
+ name = name.substring(prefix.toString().length());
+ }
+
+ if (name.length() > 0) {
+ name = name.substring(0, 1).toUpperCase()
+ + name.substring(1).toLowerCase();
+ }
+
+ name = name.replace("_", " ");
+
+ return name.trim();
+ }
}