package net.sf.openrocket.gui.adaptors;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeListener;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.EventObject;
import java.util.List;
import javax.swing.AbstractAction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.sf.openrocket.logging.Markers;
import net.sf.openrocket.util.BugException;
import net.sf.openrocket.util.ChangeSource;
import net.sf.openrocket.util.Invalidatable;
import net.sf.openrocket.util.Invalidator;
import net.sf.openrocket.util.MemoryManagement;
import net.sf.openrocket.util.Reflection;
import net.sf.openrocket.util.StateChangeListener;
/**
* A class that adapts an isXXX/setXXX boolean variable. It functions as an Action suitable
* for usage in JCheckBox or JToggleButton. You can create a suitable button with
* <code>
* check = new JCheckBox(new BooleanModel(component,"Value"))
* check.setText("Label");
* </code>
* This will produce a button that uses isValue() and setValue(boolean) of the corresponding
* component.
* <p>
* Additionally a number of component enabled states may be controlled by this class using
* the method {@link #addEnableComponent(Component, boolean)}.
*
* @author Sampo Niskanen <sampo.niskanen@iki.fi>
*/
public class BooleanModel extends AbstractAction implements StateChangeListener, Invalidatable {
private static final Logger log = LoggerFactory.getLogger(BooleanModel.class);
private final ChangeSource source;
private final String valueName;
/* Only used when referencing a ChangeSource! */
private final Method getMethod;
private final Method setMethod;
private final Method getEnabled;
/* Only used with internal boolean value! */
private boolean value;
private final List<Component> components = new ArrayList<Component>();
private final List<Boolean> componentEnableState = new ArrayList<Boolean>();
private String toString = null;
private int firing = 0;
private boolean oldValue;
private boolean oldEnabled;
private Invalidator invalidator = new Invalidator(this);
/**
* Construct a BooleanModel that holds the boolean value within itself.
*
* @param initialValue the initial value of the boolean
*/
public BooleanModel(boolean initialValue) {
this.valueName = null;
this.source = null;
this.getMethod = null;
this.setMethod = null;
this.getEnabled = null;
this.value = initialValue;
oldValue = getValue();
oldEnabled = getIsEnabled();
this.setEnabled(oldEnabled);
this.putValue(SELECTED_KEY, oldValue);
}
/**
* Construct a BooleanModel that references the boolean from a ChangeSource method.
*
* @param source the boolean source.
* @param valueName the name of the getter/setter method (without the get/is/set prefix)
*/
public BooleanModel(ChangeSource source, String valueName) {
this.source = source;
this.valueName = valueName;
Method getter = null, setter = null;
// Try get/is and set
try {
getter = source.getClass().getMethod("is" + valueName);
} catch (NoSuchMethodException ignore) {
}
if (getter == null) {
try {
getter = source.getClass().getMethod("get" + valueName);
} catch (NoSuchMethodException ignore) {
}
}
try {
setter = source.getClass().getMethod("set" + valueName, boolean.class);
} catch (NoSuchMethodException ignore) {
}
if (getter == null || setter == null) {
throw new IllegalArgumentException("get/is methods for boolean '" + valueName +
"' not present in class " + source.getClass().getCanonicalName());
}
getMethod = getter;
setMethod = setter;
Method e = null;
try {
e = source.getClass().getMethod("is" + valueName + "Enabled");
} catch (NoSuchMethodException ignore) {
}
getEnabled = e;
oldValue = getValue();
oldEnabled = getIsEnabled();
this.setEnabled(oldEnabled);
this.putValue(SELECTED_KEY, oldValue);
source.addChangeListener(this);
}
public boolean getValue() {
if (getMethod != null) {
try {
return (Boolean) getMethod.invoke(source);
} catch (IllegalAccessException e) {
throw new BugException("getMethod execution error for source " + source, e);
} catch (InvocationTargetException e) {
throw Reflection.handleWrappedException(e);
}
} else {
// Use internal value
return value;
}
}
public void setValue(boolean b) {
checkState(true);
log.debug("Setting value of " + this + " to " + b);
if (setMethod != null) {
try {
setMethod.invoke(source, new Object[] { b });
} catch (IllegalAccessException e) {
throw new BugException("setMethod execution error for source " + source, e);
} catch (InvocationTargetException e) {
throw Reflection.handleWrappedException(e);
}
} else {
// Manually fire state change - normally the ChangeSource fires it
value = b;
stateChanged(null);
}
}
/**
* Add a component the enabled status of which will be controlled by the value
* of this boolean. The <code>component</code> will be enabled exactly when
* the state of this model is equal to that of <code>enableState</code>.
*
* @param component the component to control.
* @param enableState the state in which the component should be enabled.
*/
public void addEnableComponent(Component component, boolean enableState) {
checkState(true);
components.add(component);
componentEnableState.add(enableState);
updateEnableStatus();
}
/**
* Add a component which will be enabled when this boolean is <code>true</code>.
* This is equivalent to <code>booleanModel.addEnableComponent(component, true)</code>.
*
* @param component the component to control.
* @see #addEnableComponent(Component, boolean)
*/
public void addEnableComponent(Component component) {
checkState(true);
addEnableComponent(component, true);
}
private void updateEnableStatus() {
boolean state = getValue();
for (int i = 0; i < components.size(); i++) {
Component c = components.get(i);
boolean b = componentEnableState.get(i);
c.setEnabled(state == b);
}
}
private boolean getIsEnabled() {
if (getEnabled == null)
return true;
try {
return (Boolean) getEnabled.invoke(source);
} catch (IllegalAccessException e) {
throw new BugException("getEnabled execution error for source " + source, e);
} catch (InvocationTargetException e) {
throw Reflection.handleWrappedException(e);
}
}
@Override
public void stateChanged(EventObject event) {
checkState(true);
if (firing > 0) {
log.debug("Ignoring stateChanged of " + this + ", currently firing events");
return;
}
boolean v = getValue();
boolean e = getIsEnabled();
if (oldValue != v) {
log.debug("Value of " + this + " has changed to " + v + " oldValue=" + oldValue);
oldValue = v;
firing++;
this.putValue(SELECTED_KEY, getValue());
// this.firePropertyChange(SELECTED_KEY, !v, v);
updateEnableStatus();
firing--;
}
if (oldEnabled != e) {
log.debug("Enabled status of " + this + " has changed to " + e + " oldEnabled=" + oldEnabled);
oldEnabled = e;
setEnabled(e);
}
}
@Override
public void actionPerformed(ActionEvent e) {
if (firing > 0) {
log.debug("Ignoring actionPerformed of " + this + ", currently firing events");
return;
}
boolean v = (Boolean) this.getValue(SELECTED_KEY);
log.info(Markers.USER_MARKER, "Value of " + this + " changed to " + v + " oldValue=" + oldValue);
if (v != oldValue) {
firing++;
setValue(v);
oldValue = getValue();
// Update all states
this.putValue(SELECTED_KEY, oldValue);
this.setEnabled(getIsEnabled());
updateEnableStatus();
firing--;
}
}
@Override
public void addPropertyChangeListener(PropertyChangeListener listener) {
checkState(true);
super.addPropertyChangeListener(listener);
}
/**
* Invalidates this model by removing all listeners and removing this from
* listening to the source. After invalidation no listeners can be added to this
* model and the value cannot be set.
*/
@Override
public void invalidate() {
invalidator.invalidate();
PropertyChangeListener[] listeners = this.getPropertyChangeListeners();
if (listeners.length > 0) {
log.warn("Invalidating " + this + " while still having listeners " + listeners);
for (PropertyChangeListener l : listeners) {
this.removePropertyChangeListener(l);
}
}
if (source != null) {
source.removeChangeListener(this);
}
MemoryManagement.collectable(this);
}
private void checkState(boolean error) {
invalidator.check(error);
}
@Override
public String toString() {
if (toString == null) {
if (source != null) {
toString = "BooleanModel[" + source.getClass().getSimpleName() + ":" + valueName + "]";
} else {
toString = "BooleanModel[internal value]";
}
}
return toString;
}
}