package be.nikiroo.utils.ui;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import be.nikiroo.utils.resources.Bundle;
import be.nikiroo.utils.resources.MetaInfo;
/**
* A graphical item that reflect a configuration option from the given
* {@link Bundle}.
*
* This graphical item can be edited, and the result will be saved back into the
* linked {@link MetaInfo}; you still have to save the {@link MetaInfo} should
* you wish to, of course.
*
* @author niki
*
* @param
* the graphical base type to use (i.e., T or TWidget)
* @param
* the type of {@link Bundle} to edit
*/
public abstract class ConfigItemBase> {
/** The original value before current changes. */
private Object orig;
private List origs = new ArrayList();
private List dirtyBits;
/** The fields (one for non-array, a list for arrays). */
private T field;
private List fields = new ArrayList();
/** The fields to panel map to get the actual item added to 'main'. */
private Map itemFields = new HashMap();
/** The {@link MetaInfo} linked to the field. */
private MetaInfo info;
/** The {@link MetaInfo} linked to the field. */
public MetaInfo getInfo() {
return info;
}
/**
* The number of fields, for arrays.
*
* @return
*/
public int getFieldsSize() {
return fields.size();
}
/**
* The number of fields to panel map to get the actual item added to 'main'.
*/
public int getItemFieldsSize() {
return itemFields.size();
}
/**
* Add a new item in an array-value {@link MetaInfo}.
*
* @param item
* the index of the new item
* @param panel
* a linked T, if we want to link it into the itemFields (can be
* NULL) -- that way, we can get it back later on
* {@link ConfigItemBase#removeItem(int)}
*
* @return the newly created graphical field
*/
public T addItem(final int item, T panel) {
if (panel != null) {
itemFields.put(item, panel);
}
return createField(item);
}
/**
* The counter-part to {@link ConfigItemBase#addItem(int, Object)}, to
* remove a specific item of an array-values {@link MetaInfo}; all the
* remaining items will be shifted as required (so, always the last
* graphical object will be removed).
*
* @param item
* the index of the item to remove
*
* @return the linked graphical T to remove if any (always the latest
* graphical object if any)
*/
protected T removeItem(int item) {
int last = itemFields.size() - 1;
for (int i = item; i <= last; i++) {
Object value = null;
if (i < last) {
value = getFromField(i + 1);
}
setToField(value, i);
setToInfo(value, i);
setDirtyItem(i);
}
return itemFields.remove(last);
}
/**
* Prepare a new {@link ConfigItemBase} instance, linked to the given
* {@link MetaInfo}.
*
* @param info
* the info
* @param autoDirtyHandling
* TRUE to automatically manage the setDirty/Save operations,
* FALSE if you want to do it yourself via
* {@link ConfigItemBase#setDirtyItem(int)}
*/
protected ConfigItemBase(MetaInfo info, boolean autoDirtyHandling) {
this.info = info;
if (!autoDirtyHandling) {
dirtyBits = new ArrayList();
}
}
/**
* Create an empty graphical component to be used later by
* {@link ConfigItemBase#createField(int)}.
*
* Note that {@link ConfigItemBase#reload(int)} will be called after it was
* created by {@link ConfigItemBase#createField(int)}.
*
* @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 graphical component
*/
abstract protected T createEmptyField(int item);
/**
* Get the information from the {@link MetaInfo} in the subclass preferred
* format.
*
* @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 information in the subclass preferred format
*/
abstract protected Object getFromInfo(int item);
/**
* Set the value to the {@link MetaInfo}.
*
* @param value
* the value in the subclass preferred format
* @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)
*/
abstract protected void setToInfo(Object value, int item);
/**
* The value present in the given item's related field in the subclass
* preferred format.
*
* @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 present in the given item's related field in the
* subclass preferred format
*/
abstract protected Object getFromField(int item);
/**
* Set the value (in the subclass preferred format) into the field.
*
* @param value
* the value in the subclass preferred format
* @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)
*/
abstract protected void setToField(Object value, int item);
/**
* Create a new field for the given graphical component at the given index
* (note that the component is usually created by
* {@link ConfigItemBase#createEmptyField(int)}).
*
* @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 field
* the graphical component
*/
private void setField(int item, T field) {
if (item < 0) {
this.field = field;
return;
}
for (int i = fields.size(); i <= item; i++) {
fields.add(null);
}
fields.set(item, field);
}
/**
* Retrieve the associated graphical component that was created with
* {@link ConfigItemBase#createEmptyField(int)}.
*
* @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 graphical component
*/
public T getField(int item) {
if (item < 0) {
return field;
}
if (item < fields.size()) {
return fields.get(item);
}
return null;
}
/**
* The original value (before any changes to the {@link MetaInfo}) for this
* item.
*
* @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 original value
*/
private Object getOrig(int item) {
if (item < 0) {
return orig;
}
if (item < origs.size()) {
return origs.get(item);
}
return null;
}
/**
* The original value (before any changes to the {@link MetaInfo}) for this
* item.
*
* @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 value
* the new original value
*/
private void setOrig(Object value, int item) {
if (item < 0) {
orig = value;
} else {
while (item >= origs.size()) {
origs.add(null);
}
origs.set(item, value);
}
}
/**
* Manually specify that the given item is "dirty" and thus should be saved
* when asked.
*
* Has no effect if the class is using automatic dirty handling (see
* {@link ConfigItemBase#ConfigItem(MetaInfo, 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)
*/
public void setDirtyItem(int item) {
if (dirtyBits != null) {
dirtyBits.add(item);
}
}
/**
* Check if the value changed since the last load/save into the linked
* {@link MetaInfo}.
*
* Note that we consider NULL and an Empty {@link String} to be equals.
*
* @param value
* the value to test
* @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 TRUE if it has
*/
public boolean hasValueChanged(Object value, int item) {
// We consider "" and NULL to be equals
Object orig = getOrig(item);
if (orig == null) {
orig = "";
}
return !orig.equals(value == null ? "" : value);
}
/**
* Reload the values to what they currently are in the {@link MetaInfo}.
*
* @return for arrays, the list of graphical T objects we don't need any
* more (never NULL, but can be empty)
*/
public List reload() {
List removed = new ArrayList();
if (info.isArray()) {
while (!itemFields.isEmpty()) {
removed.add(itemFields.remove(itemFields.size() - 1));
}
for (int item = 0; item < info.getListSize(false); item++) {
reload(item);
}
} else {
reload(-1);
}
return removed;
}
/**
* Reload the values to what they currently are in the {@link MetaInfo}.
*
* @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)
*/
private void reload(int item) {
if (item >= 0 && !itemFields.containsKey(item)) {
addItem(item, null);
}
Object value = getFromInfo(item);
setToField(value, item);
setOrig(value == null ? "" : value, item);
}
/**
* If the item has been modified, set the {@link MetaInfo} to dirty then
* modify it to, reflect the changes so it can be saved later.
*
* This method does not call {@link MetaInfo#save(boolean)}.
*/
private void save() {
if (info.isArray()) {
boolean dirty = itemFields.size() != info.getListSize(false);
for (int item = 0; item < itemFields.size(); item++) {
if (getDirtyBit(item)) {
dirty = true;
}
}
if (dirty) {
info.setDirty();
info.setString(null, -1);
for (int item = 0; item < itemFields.size(); item++) {
Object value = null;
if (getField(item) != null) {
value = getFromField(item);
if ("".equals(value)) {
value = null;
}
}
setToInfo(value, item);
setOrig(value, item);
}
}
} else {
if (getDirtyBit(-1)) {
Object value = getFromField(-1);
info.setDirty();
setToInfo(value, -1);
setOrig(value, -1);
}
}
}
/**
* Check if the item is dirty, and clear the dirty bit if set.
*
* @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 TRUE if it was dirty, FALSE if not
*/
private boolean getDirtyBit(int item) {
if (dirtyBits != null) {
return dirtyBits.remove((Integer) item);
}
Object value = null;
if (getField(item) != null) {
value = getFromField(item);
}
return hasValueChanged(value, item);
}
/**
* Create a new field for the given item.
*
* @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 newly created field
*/
public T createField(final int item) {
T field = createEmptyField(item);
setField(item, field);
reload(item);
info.addReloadedListener(new Runnable() {
@Override
public void run() {
reload();
}
});
info.addSaveListener(new Runnable() {
@Override
public void run() {
save();
}
});
return field;
}
}