1 package net.sf.openrocket.gui.adaptors;
3 import java.awt.event.ActionEvent;
4 import java.beans.PropertyChangeEvent;
5 import java.beans.PropertyChangeListener;
6 import java.lang.reflect.InvocationTargetException;
7 import java.lang.reflect.Method;
8 import java.util.ArrayList;
9 import java.util.EventListener;
10 import java.util.EventObject;
12 import javax.swing.AbstractAction;
13 import javax.swing.AbstractSpinnerModel;
14 import javax.swing.Action;
15 import javax.swing.BoundedRangeModel;
16 import javax.swing.SpinnerModel;
17 import javax.swing.event.ChangeEvent;
18 import javax.swing.event.ChangeListener;
20 import net.sf.openrocket.logging.LogHelper;
21 import net.sf.openrocket.startup.Application;
22 import net.sf.openrocket.unit.Unit;
23 import net.sf.openrocket.unit.UnitGroup;
24 import net.sf.openrocket.util.BugException;
25 import net.sf.openrocket.util.ChangeSource;
26 import net.sf.openrocket.util.Invalidatable;
27 import net.sf.openrocket.util.Invalidator;
28 import net.sf.openrocket.util.MathUtil;
29 import net.sf.openrocket.util.MemoryManagement;
30 import net.sf.openrocket.util.Reflection;
31 import net.sf.openrocket.util.StateChangeListener;
32 import net.sf.openrocket.util.exp4j.Calculable;
33 import net.sf.openrocket.util.exp4j.ExpressionBuilder;
34 import net.sf.openrocket.util.exp4j.UnknownFunctionException;
35 import net.sf.openrocket.util.exp4j.UnparsableExpressionException;
39 * A model connector that can read and modify any value of any ChangeSource that
40 * has the appropriate get/set methods defined.
42 * The variable is defined in the constructor by providing the variable name as a string
43 * (e.g. "Radius" -> getRadius()/setRadius()). Additional scaling may be applied, e.g. a
44 * DoubleModel for the diameter can be defined by the variable "Radius" and a multiplier of 2.
46 * Sub-models suitable for JSpinners and other components are available from the appropriate
49 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
52 public class DoubleModel implements StateChangeListener, ChangeSource, Invalidatable {
53 private static final LogHelper log = Application.getLogger();
56 public static final DoubleModel ZERO = new DoubleModel(0);
58 //////////// JSpinner Model ////////////
61 * Model suitable for JSpinner.
62 * Note: Previously used using JSpinner.NumberEditor and extended SpinnerNumberModel
63 * to be compatible with the NumberEditor, but only has the necessary methods defined.
64 * This is still the design, but now extends AbstractSpinnerModel to allow other characters
65 * to be entered so that fractional units and expressions can be used.
67 public class ValueSpinnerModel extends AbstractSpinnerModel implements Invalidatable {
70 public Object getValue() {
71 return currentUnit.toString(DoubleModel.this.getValue());
75 public void setValue(Object value) {
77 // Ignore, if called when model is sending events
78 log.verbose("Ignoring call to SpinnerModel setValue for " + DoubleModel.this.toString() +
79 " value=" + value + ", currently firing events");
83 Number num = Double.NaN;
85 // Set num if possible
86 if ( value instanceof Number ) {
89 else if ( value instanceof String ) {
91 String newValString = (String)value;
92 ExpressionBuilder builder=new ExpressionBuilder(newValString);
93 Calculable calc=builder.build();
94 num = calc.calculate();
96 catch ( java.lang.NumberFormatException e ) {
97 } catch (UnknownFunctionException e) {
98 } catch (UnparsableExpressionException e) {
99 } catch (java.util.EmptyStackException e) {
103 // Update the doublemodel with the new number or return to the last number if not possible
104 if ( ((Double)num).isNaN() ) {
105 DoubleModel.this.setValue( lastValue );
106 log.user("SpinnerModel could not set value for " + DoubleModel.this.toString() + ". Could not convert " + value.toString());
109 double newValue = num.doubleValue();
110 double converted = currentUnit.fromUnit(newValue);
112 log.user("SpinnerModel setValue called for " + DoubleModel.this.toString() + " newValue=" + newValue +
113 " converted=" + converted);
114 DoubleModel.this.setValue(converted);
117 // Force a refresh if text doesn't match up exactly with the stored value
118 if ( ! ((Double)lastValue).toString().equals( this.getValue().toString() ) ) {
119 DoubleModel.this.fireStateChanged();
120 log.debug("SpinnerModel "+DoubleModel.this.toString()+" refresh forced because string did not match actual value.");
125 public Object getNextValue() {
126 double d = currentUnit.toUnit(DoubleModel.this.getValue());
127 double max = currentUnit.toUnit(maxValue);
128 if (MathUtil.equals(d, max))
130 d = currentUnit.getNextValue(d);
137 public Object getPreviousValue() {
138 double d = currentUnit.toUnit(DoubleModel.this.getValue());
139 double min = currentUnit.toUnit(minValue);
140 if (MathUtil.equals(d, min))
142 d = currentUnit.getPreviousValue(d);
149 public void addChangeListener(ChangeListener l) {
150 DoubleModel.this.addChangeListener(l);
154 public void removeChangeListener(ChangeListener l) {
155 DoubleModel.this.removeChangeListener(l);
159 public void invalidate() {
160 DoubleModel.this.invalidate();
165 * Returns a new SpinnerModel with the same base as the DoubleModel.
166 * The values given to the JSpinner are in the currently selected units.
168 * @return A compatibility layer for a SpinnerModel.
170 public SpinnerModel getSpinnerModel() {
171 return new ValueSpinnerModel();
174 //////////// JSlider model ////////////
176 private class ValueSliderModel implements BoundedRangeModel, StateChangeListener, Invalidatable {
177 private static final int MAX = 1000;
180 * Use linear scale value = linear1 * x + linear0 when x < linearPosition
181 * Use quadratic scale value = quad2 * x^2 + quad1 * x + quad0 otherwise
183 private final boolean islinear;
185 // Linear in range x <= linearPosition
186 private final double linearPosition;
188 // May be changing DoubleModels when using linear model
189 private final DoubleModel min, mid, max;
191 // Linear multiplier and constant
192 //private final double linear1;
193 //private final double linear0;
195 // Non-linear multiplier, exponent and constant
196 private double quad2, quad1, quad0;
198 public ValueSliderModel(DoubleModel min, DoubleModel max) {
199 this.islinear = true;
200 linearPosition = 1.0;
203 this.mid = max; // Never use exponential scale
206 min.addChangeListener(this);
207 max.addChangeListener(this);
209 quad2 = quad1 = quad0 = 0; // Not used
215 * Generate a linear model from min to max.
217 public ValueSliderModel(double min, double max) {
218 this.islinear = true;
219 linearPosition = 1.0;
221 this.min = new DoubleModel(min);
222 this.mid = new DoubleModel(max); // Never use exponential scale
223 this.max = new DoubleModel(max);
225 quad2 = quad1 = quad0 = 0; // Not used
228 public ValueSliderModel(double min, double mid, double max) {
229 this(min, 0.5, mid, max);
232 public ValueSliderModel(double min, double mid, DoubleModel max) {
233 this(min, 0.5, mid, max);
237 * v(x) = mul * x^exp + add
239 * v(pos) = mul * pos^exp + add = mid
240 * v(1) = mul + add = max
241 * v'(pos) = mul*exp * pos^(exp-1) = linearMul
243 public ValueSliderModel(double min, double pos, double mid, double max ) {
244 this(min, pos, mid, new DoubleModel(max));
246 public ValueSliderModel(double min, double pos, double mid, DoubleModel max) {
247 this.min = new DoubleModel(min);
248 this.mid = new DoubleModel(mid);
251 this.islinear = false;
253 max.addChangeListener(this);
255 linearPosition = pos;
257 //linear1 = (mid-min)/pos;
259 if (!(min < mid && mid <= max.getValue() && 0 < pos && pos < 1)) {
260 throw new IllegalArgumentException("Bad arguments for ValueSliderModel " +
261 "min=" + min + " mid=" + mid + " max=" + max + " pos=" + pos);
264 updateExponentialParameters();
268 private void updateExponentialParameters() {
269 double pos = this.linearPosition;
270 double minValue = this.min.getValue();
271 double midValue = this.mid.getValue();
272 double maxValue = this.max.getValue();
274 * quad2..0 are calculated such that
275 * f(pos) = mid - continuity
276 * f(1) = max - end point
277 * f'(pos) = linear1 - continuity of derivative
279 double delta = (midValue - minValue) / pos;
280 quad2 = (maxValue - midValue - delta + delta * pos) / pow2(pos - 1);
281 quad1 = (delta + 2 * (midValue - maxValue) * pos - delta * pos * pos) / pow2(pos - 1);
282 quad0 = (midValue - (2 * midValue + delta) * pos + (maxValue + delta) * pos * pos) / pow2(pos - 1);
285 private double pow2(double x) {
290 public int getValue() {
291 double value = DoubleModel.this.getValue();
292 if (value <= min.getValue())
294 if (value >= max.getValue())
298 if (value <= mid.getValue()) {
301 //linear1 = (mid-min)/pos;
303 x = (value - min.getValue()) * linearPosition / (mid.getValue() - min.getValue());
305 // Use quadratic scale
306 // Further solution of the quadratic equation
307 // a*x^2 + b*x + c-value == 0
308 x = (MathUtil.safeSqrt(quad1 * quad1 - 4 * quad2 * (quad0 - value)) - quad1) / (2 * quad2);
310 return (int) (x * MAX);
315 public void setValue(int newValue) {
318 log.verbose("Ignoring call to SliderModel setValue for " + DoubleModel.this.toString() +
319 " value=" + newValue + ", currently firing events");
323 double x = (double) newValue / MAX;
326 if (x <= linearPosition) {
329 //linear1 = (mid-min)/pos;
331 scaledValue = (mid.getValue() - min.getValue()) / linearPosition * x + min.getValue();
333 // Use quadratic scale
334 scaledValue = quad2 * x * x + quad1 * x + quad0;
337 double converted = currentUnit.fromUnit(currentUnit.round(currentUnit.toUnit(scaledValue)));
338 log.user("SliderModel setValue called for " + DoubleModel.this.toString() + " newValue=" + newValue +
339 " scaledValue=" + scaledValue + " converted=" + converted);
340 DoubleModel.this.setValue(converted);
344 // Static get-methods
345 private boolean isAdjusting;
348 public int getExtent() {
353 public int getMaximum() {
358 public int getMinimum() {
363 public boolean getValueIsAdjusting() {
369 public void setExtent(int newExtent) {
373 public void setMaximum(int newMaximum) {
377 public void setMinimum(int newMinimum) {
381 public void setValueIsAdjusting(boolean b) {
386 public void setRangeProperties(int value, int extent, int min, int max, boolean adjusting) {
387 setValueIsAdjusting(adjusting);
391 // Pass change listeners to the underlying model
393 public void addChangeListener(ChangeListener l) {
394 DoubleModel.this.addChangeListener(l);
398 public void removeChangeListener(ChangeListener l) {
399 DoubleModel.this.removeChangeListener(l);
403 public void invalidate() {
404 DoubleModel.this.invalidate();
408 public void stateChanged(EventObject e) {
409 // Min or max range has changed.
411 double midValue = (max.getValue() - min.getValue()) /3.0;
412 mid.setValue(midValue);
413 updateExponentialParameters();
415 // Fire if not already firing
422 public BoundedRangeModel getSliderModel(DoubleModel min, DoubleModel max) {
423 return new ValueSliderModel(min, max);
426 public BoundedRangeModel getSliderModel(double min, double max) {
427 return new ValueSliderModel(min, max);
430 public BoundedRangeModel getSliderModel(double min, double mid, double max) {
431 return new ValueSliderModel(min, mid, max);
434 public BoundedRangeModel getSliderModel(double min, double mid, DoubleModel max) {
435 return new ValueSliderModel(min, mid, max);
438 public BoundedRangeModel getSliderModel(double min, double pos, double mid, double max) {
439 return new ValueSliderModel(min, pos, mid, max);
446 //////////// Action model ////////////
448 private class AutomaticActionModel extends AbstractAction implements StateChangeListener, Invalidatable {
449 private boolean oldValue = false;
451 public AutomaticActionModel() {
452 oldValue = isAutomatic();
453 addChangeListener(this);
458 public boolean isEnabled() {
459 return isAutomaticAvailable();
463 public Object getValue(String key) {
464 if (key.equals(Action.SELECTED_KEY)) {
465 oldValue = isAutomatic();
468 return super.getValue(key);
472 public void putValue(String key, Object value) {
474 log.verbose("Ignoring call to ActionModel putValue for " + DoubleModel.this.toString() +
475 " key=" + key + " value=" + value + ", currently firing events");
478 if (key.equals(Action.SELECTED_KEY) && (value instanceof Boolean)) {
479 log.user("ActionModel putValue called for " + DoubleModel.this.toString() +
480 " key=" + key + " value=" + value);
481 oldValue = (Boolean) value;
482 setAutomatic((Boolean) value);
484 log.debug("Passing ActionModel putValue call to supermethod for " + DoubleModel.this.toString() +
485 " key=" + key + " value=" + value);
486 super.putValue(key, value);
490 // Implement a wrapper to the ChangeListeners
491 ArrayList<PropertyChangeListener> propertyChangeListeners =
492 new ArrayList<PropertyChangeListener>();
495 public void addPropertyChangeListener(PropertyChangeListener listener) {
496 propertyChangeListeners.add(listener);
497 DoubleModel.this.addChangeListener(this);
501 public void removePropertyChangeListener(PropertyChangeListener listener) {
502 propertyChangeListeners.remove(listener);
503 if (propertyChangeListeners.isEmpty())
504 DoubleModel.this.removeChangeListener(this);
507 // If the value has changed, generate an event to the listeners
509 public void stateChanged(EventObject e) {
510 boolean newValue = isAutomatic();
511 if (oldValue == newValue)
513 PropertyChangeEvent event = new PropertyChangeEvent(this, Action.SELECTED_KEY,
516 Object[] l = propertyChangeListeners.toArray();
517 for (int i = 0; i < l.length; i++) {
518 ((PropertyChangeListener) l[i]).propertyChange(event);
523 public void actionPerformed(ActionEvent e) {
524 // Setting performed in putValue
528 public void invalidate() {
529 DoubleModel.this.invalidate();
534 * Returns a new Action corresponding to the changes of the automatic setting
535 * property of the value model. This may be used directly with e.g. check buttons.
537 * @return A compatibility layer for an Action.
539 public Action getAutomaticAction() {
540 return new AutomaticActionModel();
547 //////////// Main model /////////////
550 * The main model handles all values in SI units, i.e. no conversion is made within the model.
553 private final ChangeSource source;
554 private final String valueName;
555 private final double multiplier;
557 private final Method getMethod;
558 private final Method setMethod;
560 private final Method getAutoMethod;
561 private final Method setAutoMethod;
563 private final ArrayList<EventListener> listeners = new ArrayList<EventListener>();
565 private final UnitGroup units;
566 private Unit currentUnit;
568 private final double minValue;
569 private double maxValue;
571 private String toString = null;
574 private int firing = 0; // >0 when model itself is sending events
577 // Used to differentiate changes in valueName and other changes in the component:
578 private double lastValue = 0;
579 private boolean lastAutomatic = false;
581 private Invalidator invalidator = new Invalidator(this);
585 * Generate a DoubleModel that contains an internal double value.
587 * @param value the initial value.
589 public DoubleModel(double value) {
590 this(value, UnitGroup.UNITS_NONE, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
594 * Generate a DoubleModel that contains an internal double value.
596 * @param value the initial value.
597 * @param unit the unit for the value.
599 public DoubleModel(double value, UnitGroup unit) {
600 this(value, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
604 * Generate a DoubleModel that contains an internal double value.
606 * @param value the initial value.
607 * @param unit the unit for the value.
608 * @param min minimum value.
610 public DoubleModel(double value, UnitGroup unit, double min) {
611 this(value, unit, min, Double.POSITIVE_INFINITY);
615 * Generate a DoubleModel that contains an internal double value.
617 * @param value the initial value.
618 * @param unit the unit for the value.
619 * @param min minimum value.
620 * @param max maximum value.
622 public DoubleModel(double value, UnitGroup unit, double min, double max) {
623 this.lastValue = value;
628 valueName = "Constant value";
631 getMethod = setMethod = null;
632 getAutoMethod = setAutoMethod = null;
634 currentUnit = units.getDefaultUnit();
639 * Generates a new DoubleModel that changes the values of the specified component.
640 * The double value is read and written using the methods "get"/"set" + valueName.
642 * @param source Component whose parameter to use.
643 * @param valueName Name of methods used to get/set the parameter.
644 * @param multiplier Value shown by the model is the value from component.getXXX * multiplier
645 * @param min Minimum value allowed (in SI units)
646 * @param max Maximum value allowed (in SI units)
648 public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit,
649 double min, double max) {
650 this.source = source;
651 this.valueName = valueName;
652 this.multiplier = multiplier;
655 currentUnit = units.getDefaultUnit();
661 getMethod = source.getClass().getMethod("get" + valueName);
662 } catch (NoSuchMethodException e) {
663 throw new IllegalArgumentException("get method for value '" + valueName +
664 "' not present in class " + source.getClass().getCanonicalName());
669 s = source.getClass().getMethod("set" + valueName, double.class);
670 } catch (NoSuchMethodException e1) {
674 // Automatic selection methods
676 Method set = null, get = null;
679 get = source.getClass().getMethod("is" + valueName + "Automatic");
680 set = source.getClass().getMethod("set" + valueName + "Automatic", boolean.class);
681 } catch (NoSuchMethodException e) {
684 if (set != null && get != null) {
688 getAutoMethod = null;
689 setAutoMethod = null;
694 public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit,
696 this(source, valueName, multiplier, unit, min, Double.POSITIVE_INFINITY);
699 public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit) {
700 this(source, valueName, multiplier, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
703 public DoubleModel(ChangeSource source, String valueName, UnitGroup unit,
704 double min, double max) {
705 this(source, valueName, 1.0, unit, min, max);
708 public DoubleModel(ChangeSource source, String valueName, UnitGroup unit, double min) {
709 this(source, valueName, 1.0, unit, min, Double.POSITIVE_INFINITY);
712 public DoubleModel(ChangeSource source, String valueName, UnitGroup unit) {
713 this(source, valueName, 1.0, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
716 public DoubleModel(ChangeSource source, String valueName) {
717 this(source, valueName, 1.0, UnitGroup.UNITS_NONE,
718 Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
721 public DoubleModel(ChangeSource source, String valueName, double min) {
722 this(source, valueName, 1.0, UnitGroup.UNITS_NONE, min, Double.POSITIVE_INFINITY);
725 public DoubleModel(ChangeSource source, String valueName, double min, double max) {
726 this(source, valueName, 1.0, UnitGroup.UNITS_NONE, min, max);
732 * Returns the value of the variable (in SI units).
734 public double getValue() {
735 if (getMethod == null) // Constant value
739 return (Double) getMethod.invoke(source) * multiplier;
740 } catch (IllegalArgumentException e) {
741 throw new BugException("Unable to invoke getMethod of " + this, e);
742 } catch (IllegalAccessException e) {
743 throw new BugException("Unable to invoke getMethod of " + this, e);
744 } catch (InvocationTargetException e) {
745 throw Reflection.handleWrappedException(e);
750 * Sets the value of the variable.
751 * @param v New value for parameter in SI units.
753 public void setValue(double v) {
756 log.debug("Setting value " + v + " for " + this);
757 if (setMethod == null) {
758 if (getMethod != null) {
759 throw new BugException("setMethod not available for variable '" + valueName +
760 "' in class " + source.getClass().getCanonicalName());
768 setMethod.invoke(source, v / multiplier);
769 } catch (IllegalArgumentException e) {
770 throw new BugException("Unable to invoke setMethod of " + this, e);
771 } catch (IllegalAccessException e) {
772 throw new BugException("Unable to invoke setMethod of " + this, e);
773 } catch (InvocationTargetException e) {
774 throw Reflection.handleWrappedException(e);
779 * Returns whether setting the value automatically is available.
781 public boolean isAutomaticAvailable() {
782 return (getAutoMethod != null) && (setAutoMethod != null);
786 * Returns whether the value is currently being set automatically.
787 * Returns false if automatic setting is not available at all.
789 public boolean isAutomatic() {
790 if (getAutoMethod == null)
794 return (Boolean) getAutoMethod.invoke(source);
795 } catch (IllegalArgumentException e) {
796 throw new BugException("Method call failed", e);
797 } catch (IllegalAccessException e) {
798 throw new BugException("Method call failed", e);
799 } catch (InvocationTargetException e) {
800 throw Reflection.handleWrappedException(e);
805 * Sets whether the value should be set automatically. Simply fires a
806 * state change event if automatic setting is not available.
808 public void setAutomatic(boolean auto) {
811 if (setAutoMethod == null) {
812 log.debug("Setting automatic to " + auto + " for " + this + ", automatic not available");
813 fireStateChanged(); // in case something is out-of-sync
817 log.debug("Setting automatic to " + auto + " for " + this);
818 lastAutomatic = auto;
820 setAutoMethod.invoke(source, auto);
821 } catch (IllegalArgumentException e) {
822 throw new BugException(e);
823 } catch (IllegalAccessException e) {
824 throw new BugException(e);
825 } catch (InvocationTargetException e) {
826 throw Reflection.handleWrappedException(e);
832 * Returns the current Unit. At the beginning it is the default unit of the UnitGroup.
833 * @return The most recently set unit.
835 public Unit getCurrentUnit() {
840 * Sets the current Unit. The unit must be one of those included in the UnitGroup.
841 * @param u The unit to set active.
843 public void setCurrentUnit(Unit u) {
845 if (currentUnit == u)
847 log.debug("Setting unit for " + this + " to '" + u + "'");
854 * Returns the UnitGroup associated with the parameter value.
856 * @return The UnitGroup given to the constructor.
858 public UnitGroup getUnitGroup() {
865 * Add a listener to the model. Adds the model as a listener to the value source if this
866 * is the first listener.
867 * @param l Listener to add.
870 public void addChangeListener(EventListener l) {
873 if (listeners.isEmpty()) {
874 if (source != null) {
875 source.addChangeListener(this);
876 lastValue = getValue();
877 lastAutomatic = isAutomatic();
882 log.verbose(this + " adding listener (total " + listeners.size() + "): " + l);
886 * Remove a listener from the model. Removes the model from being a listener to the Component
887 * if this was the last listener of the model.
888 * @param l Listener to remove.
891 public void removeChangeListener(EventListener l) {
895 if (listeners.isEmpty() && source != null) {
896 source.removeChangeListener(this);
898 log.verbose(this + " removing listener (total " + listeners.size() + "): " + l);
903 * Invalidates this model by removing all listeners and removing this from
904 * listening to the source. After invalidation no listeners can be added to this
905 * model and the value cannot be set.
908 public void invalidate() {
909 log.verbose("Invalidating " + this);
910 invalidator.invalidate();
912 if (!listeners.isEmpty()) {
913 log.warn("Invalidating " + this + " while still having listeners " + listeners);
916 if (source != null) {
917 source.removeChangeListener(this);
919 MemoryManagement.collectable(this);
923 private void checkState(boolean error) {
924 invalidator.check(error);
929 protected void finalize() throws Throwable {
931 if (!listeners.isEmpty()) {
932 log.warn(this + " being garbage-collected while having listeners " + listeners);
938 * Fire a ChangeEvent to all listeners.
940 protected void fireStateChanged() {
943 EventObject event = new EventObject(this);
944 ChangeEvent cevent = new ChangeEvent(this);
946 // Copy the list before iterating to prevent concurrent modification exceptions.
947 EventListener[] ls = listeners.toArray(new EventListener[0]);
948 for (EventListener l : ls) {
949 if ( l instanceof StateChangeListener ) {
950 ((StateChangeListener)l).stateChanged(event);
951 } else if ( l instanceof ChangeListener ) {
952 ((ChangeListener)l).stateChanged(cevent);
959 * Called when the component changes. Checks whether the modeled value has changed, and if
960 * it has, updates lastValue and generates ChangeEvents for all listeners of the model.
963 public void stateChanged(EventObject e) {
966 double v = getValue();
967 boolean b = isAutomatic();
968 if (lastValue == v && lastAutomatic == b)
977 * Explain the DoubleModel as a String.
980 public String toString() {
981 if (toString == null) {
982 if (source == null) {
983 toString = "DoubleModel[constant=" + lastValue + "]";
985 toString = "DoubleModel[" + source.getClass().getSimpleName() + ":" + valueName + "]";