release 0.9.6
[debian/openrocket] / src / net / sf / openrocket / gui / adaptors / DoubleModel.java
1 package net.sf.openrocket.gui.adaptors;
2
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
10 import javax.swing.AbstractAction;
11 import javax.swing.Action;
12 import javax.swing.BoundedRangeModel;
13 import javax.swing.SpinnerModel;
14 import javax.swing.SpinnerNumberModel;
15 import javax.swing.event.ChangeEvent;
16 import javax.swing.event.ChangeListener;
17
18 import net.sf.openrocket.unit.Unit;
19 import net.sf.openrocket.unit.UnitGroup;
20 import net.sf.openrocket.util.BugException;
21 import net.sf.openrocket.util.ChangeSource;
22 import net.sf.openrocket.util.MathUtil;
23 import net.sf.openrocket.util.Reflection;
24
25
26 /**
27  * A model connector that can read and modify any value of any ChangeSource that
28  * has the appropriate get/set methods defined.  
29  * 
30  * The variable is defined in the constructor by providing the variable name as a string
31  * (e.g. "Radius" -> getRadius()/setRadius()).  Additional scaling may be applied, e.g. a 
32  * DoubleModel for the diameter can be defined by the variable "Radius" and a multiplier of 2.
33  * 
34  * Sub-models suitable for JSpinners and other components are available from the appropriate
35  * methods.
36  * 
37  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
38  */
39
40 public class DoubleModel implements ChangeListener, ChangeSource {
41         private static final boolean DEBUG_LISTENERS = false;
42         
43         public static final DoubleModel ZERO = new DoubleModel(0);
44
45         //////////// JSpinner Model ////////////
46         
47         /**
48          * Model suitable for JSpinner using JSpinner.NumberEditor.  It extends SpinnerNumberModel
49          * to be compatible with the NumberEditor, but only has the necessary methods defined.
50          */
51         private class ValueSpinnerModel extends SpinnerNumberModel {
52                 
53                 @Override
54                 public Object getValue() {
55                         return currentUnit.toUnit(DoubleModel.this.getValue());
56 //                      return makeString(currentUnit.toUnit(DoubleModel.this.getValue()));
57                 }
58
59                 @Override
60                 public void setValue(Object value) {
61                         
62                         System.out.println("setValue("+value+") called, valueName="+valueName+
63                                         " firing="+firing);
64                         
65                         if (firing > 0)   // Ignore, if called when model is sending events
66                                 return;
67                         Number num = (Number)value;
68                         double newValue = num.doubleValue();
69                         DoubleModel.this.setValue(currentUnit.fromUnit(newValue));
70                         
71                         
72 //                      try {
73 //                              double newValue = Double.parseDouble((String)value);
74 //                              DoubleModel.this.setValue(currentUnit.fromUnit(newValue));
75 //                      } catch (NumberFormatException e) { 
76 //                              DoubleModel.this.fireStateChanged();
77 //                      };
78                 }
79                 
80                 @Override
81                 public Object getNextValue() {
82                         double d = currentUnit.toUnit(DoubleModel.this.getValue());
83                         double max = currentUnit.toUnit(maxValue);
84                         if (MathUtil.equals(d,max))
85                                 return null;
86                         d = currentUnit.getNextValue(d);
87                         if (d > max)
88                                 d = max;
89                         return d;
90 //                      return makeString(d);
91                 }
92
93                 @Override
94                 public Object getPreviousValue() {
95                         double d = currentUnit.toUnit(DoubleModel.this.getValue());
96                         double min = currentUnit.toUnit(minValue);
97                         if (MathUtil.equals(d,min))
98                                 return null;
99                         d = currentUnit.getPreviousValue(d);
100                         if (d < min)
101                                 d = min;
102                         return d;
103 //                      return makeString(d);
104                 }
105
106                 
107                 @Override
108                 public Comparable<Double> getMinimum() {
109                         return currentUnit.toUnit(minValue);
110                 }
111                 
112                 @Override
113                 public Comparable<Double> getMaximum() {
114                         return currentUnit.toUnit(maxValue);
115                 }
116                 
117                 
118                 @Override
119                 public void addChangeListener(ChangeListener l) {
120                         DoubleModel.this.addChangeListener(l);
121                 }
122
123                 @Override
124                 public void removeChangeListener(ChangeListener l) {
125                         DoubleModel.this.removeChangeListener(l);
126                 }
127         }
128         
129         /**
130          * Returns a new SpinnerModel with the same base as the DoubleModel.
131          * The values given to the JSpinner are in the currently selected units.
132          * 
133          * @return  A compatibility layer for a SpinnerModel.
134          */
135         public SpinnerModel getSpinnerModel() {
136                 return new ValueSpinnerModel();
137         }
138         
139         
140         
141         
142         
143         ////////////  JSlider model  ////////////
144         
145         private class ValueSliderModel implements BoundedRangeModel, ChangeListener {
146                 private static final int MAX = 1000;
147                 
148                 /*
149                  * Use linear scale  value = linear1 * x + linear0  when x < linearPosition
150                  * Use quadratic scale  value = quad2 * x^2 + quad1 * x + quad0  otherwise
151                  */
152                 
153                 // Linear in range x <= linearPosition
154                 private final double linearPosition;
155                 
156                 // May be changing DoubleModels when using linear model
157                 private final DoubleModel min, mid, max;
158                 
159                 // Linear multiplier and constant
160                 //private final double linear1;
161                 //private final double linear0;
162                 
163                 // Non-linear multiplier, exponent and constant
164                 private final double quad2,quad1,quad0;
165                 
166                 
167                 
168                 public ValueSliderModel(DoubleModel min, DoubleModel max) {
169                         linearPosition = 1.0;
170
171                         this.min = min;
172                         this.mid = max;  // Never use exponential scale
173                         this.max = max;
174                         
175                         min.addChangeListener(this);
176                         max.addChangeListener(this);
177
178                         quad2 = quad1 = quad0 = 0;  // Not used
179                 }
180                 
181                 
182                 
183                 /**
184                  * Generate a linear model from min to max.
185                  */
186                 public ValueSliderModel(double min, double max) {
187                         linearPosition = 1.0;
188
189                         this.min = new DoubleModel(min);
190                         this.mid = new DoubleModel(max);  // Never use exponential scale
191                         this.max = new DoubleModel(max);
192
193                         quad2 = quad1 = quad0 = 0;  // Not used
194                 }
195                 
196                 public ValueSliderModel(double min, double mid, double max) {
197                         this(min,0.5,mid,max);
198                 }
199                 
200                 /*
201                  * v(x)  = mul * x^exp + add
202                  * 
203                  * v(pos)  = mul * pos^exp + add = mid
204                  * v(1)    = mul + add = max
205                  * v'(pos) = mul*exp * pos^(exp-1) = linearMul
206                  */
207                 public ValueSliderModel(double min, double pos, double mid, double max) {
208                         this.min = new DoubleModel(min);
209                         this.mid = new DoubleModel(mid);
210                         this.max = new DoubleModel(max);
211
212                         
213                         linearPosition = pos;
214                         //linear0 = min;
215                         //linear1 = (mid-min)/pos;
216                         
217                         if (!(min < mid && mid <= max && 0 < pos && pos < 1)) {
218                                 throw new IllegalArgumentException("Bad arguments for ValueSliderModel "+
219                                                 "min="+min+" mid="+mid+" max="+max+" pos="+pos);
220                         }
221                         
222                         /*
223                          * quad2..0 are calculated such that
224                          *   f(pos)  = mid      - continuity
225                          *   f(1)    = max      - end point
226                          *   f'(pos) = linear1  - continuity of derivative
227                          */
228                         
229                         double delta = (mid-min)/pos;
230                         quad2 = (max - mid - delta + delta*pos) / pow2(pos-1);
231                         quad1 = (delta + 2*(mid-max)*pos - delta*pos*pos) / pow2(pos-1);
232                         quad0 = (mid - (2*mid+delta)*pos + (max+delta)*pos*pos) / pow2(pos-1);
233                         
234                 }
235                 
236                 private double pow2(double x) {
237                         return x*x;
238                 }
239                 
240                 public int getValue() {
241                         double value = DoubleModel.this.getValue();
242                         if (value <= min.getValue())
243                                 return 0;
244                         if (value >= max.getValue())
245                                 return MAX;
246                         
247                         double x;
248                         if (value <= mid.getValue()) {
249                                 // Use linear scale
250                                 //linear0 = min;
251                                 //linear1 = (mid-min)/pos;
252                                 
253                                 x = (value - min.getValue())*linearPosition/(mid.getValue()-min.getValue());
254                         } else {
255                                 // Use quadratic scale
256                                 // Further solution of the quadratic equation
257                                 //   a*x^2 + b*x + c-value == 0
258                                 x = (Math.sqrt(quad1*quad1 - 4*quad2*(quad0-value)) - quad1) / (2*quad2);
259                         }
260                         return (int)(x*MAX);
261                 }
262
263
264                 public void setValue(int newValue) {
265                         if (firing > 0)   // Ignore loops
266                                 return;
267                         
268                         double x = (double)newValue/MAX;
269                         double value;
270                         
271                         if (x <= linearPosition) {
272                                 // Use linear scale
273                                 //linear0 = min;
274                                 //linear1 = (mid-min)/pos;
275
276                                 value = (mid.getValue()-min.getValue())/linearPosition*x + min.getValue();
277                         } else {
278                                 // Use quadratic scale
279                                 value = quad2*x*x + quad1*x + quad0;
280                         }
281                         
282                         DoubleModel.this.setValue(currentUnit.fromUnit(
283                                         currentUnit.round(currentUnit.toUnit(value))));
284                 }
285
286                 
287                 // Static get-methods
288                 private boolean isAdjusting;
289                 public int getExtent() { return 0; }
290                 public int getMaximum() { return MAX; }
291                 public int getMinimum() { return 0; }
292                 public boolean getValueIsAdjusting() { return isAdjusting; }
293                 
294                 // Ignore set-values
295                 public void setExtent(int newExtent) { }
296                 public void setMaximum(int newMaximum) { }
297                 public void setMinimum(int newMinimum) { }
298                 public void setValueIsAdjusting(boolean b) { isAdjusting = b; }
299
300                 public void setRangeProperties(int value, int extent, int min, int max, boolean adjusting) {
301                         setValueIsAdjusting(adjusting);
302                         setValue(value);
303                 }
304
305                 // Pass change listeners to the underlying model
306                 public void addChangeListener(ChangeListener l) {
307                         DoubleModel.this.addChangeListener(l);
308                 }
309
310                 public void removeChangeListener(ChangeListener l) {
311                         DoubleModel.this.removeChangeListener(l);
312                 }
313
314
315
316                 public void stateChanged(ChangeEvent e) {
317                         // Min or max range has changed.
318                         // Fire if not already firing
319                         if (firing == 0)
320                                 fireStateChanged();
321                 }
322         }
323         
324         
325         public BoundedRangeModel getSliderModel(DoubleModel min, DoubleModel max) {
326                 return new ValueSliderModel(min,max);
327         }
328         
329         public BoundedRangeModel getSliderModel(double min, double max) {
330                 return new ValueSliderModel(min,max);
331         }
332         
333         public BoundedRangeModel getSliderModel(double min, double mid, double max) {
334                 return new ValueSliderModel(min,mid,max);
335         }
336         
337         public BoundedRangeModel getSliderModel(double min, double pos, double mid, double max) {
338                 return new ValueSliderModel(min,pos,mid,max);
339         }
340         
341         
342         
343         
344
345         ////////////  Action model  ////////////
346         
347         private class AutomaticActionModel extends AbstractAction implements ChangeListener {
348                 private boolean oldValue = false;
349                 
350                 public AutomaticActionModel() {
351                         oldValue = isAutomatic();
352                         addChangeListener(this);
353                 }
354                 
355
356                 @Override
357                 public boolean isEnabled() {
358                         // TODO: LOW: does not reflect if component is currently able to support automatic setting
359                         return isAutomaticAvailable();
360                 }
361                 
362                 @Override
363                 public Object getValue(String key) {
364                         if (key.equals(Action.SELECTED_KEY)) {
365                                 oldValue = isAutomatic();
366                                 return oldValue;
367                         }
368                         return super.getValue(key);
369                 }
370
371                 @Override
372                 public void putValue(String key, Object value) {
373                         if (firing > 0)
374                                 return;
375                         if (key.equals(Action.SELECTED_KEY) && (value instanceof Boolean)) {
376                                 oldValue = (Boolean)value;
377                                 setAutomatic((Boolean)value);
378                         } else {
379                                 super.putValue(key, value);
380                         }
381                 }
382
383                 // Implement a wrapper to the ChangeListeners
384                 ArrayList<PropertyChangeListener> propertyChangeListeners = 
385                         new ArrayList<PropertyChangeListener>();
386                 @Override
387                 public void addPropertyChangeListener(PropertyChangeListener listener) {
388                         propertyChangeListeners.add(listener);
389                         DoubleModel.this.addChangeListener(this);
390                 }
391                 @Override
392                 public void removePropertyChangeListener(PropertyChangeListener listener) {
393                         propertyChangeListeners.remove(listener);
394                         if (propertyChangeListeners.isEmpty())
395                                 DoubleModel.this.removeChangeListener(this);
396                 }
397                 // If the value has changed, generate an event to the listeners
398                 public void stateChanged(ChangeEvent e) {
399                         boolean newValue = isAutomatic();
400                         if (oldValue == newValue)
401                                 return;
402                         PropertyChangeEvent event = new PropertyChangeEvent(this,Action.SELECTED_KEY,
403                                         oldValue,newValue);
404                         oldValue = newValue;
405                         Object[] l = propertyChangeListeners.toArray();
406                         for (int i=0; i<l.length; i++) {
407                                 ((PropertyChangeListener)l[i]).propertyChange(event);
408                         }
409                 }
410
411                 public void actionPerformed(ActionEvent e) {
412                         // Setting performed in putValue
413                 }
414
415         }
416         
417         /**
418          * Returns a new Action corresponding to the changes of the automatic setting
419          * property of the value model.  This may be used directly with e.g. check buttons.
420          * 
421          * @return  A compatibility layer for an Action.
422          */
423         public Action getAutomaticAction() {
424                 return new AutomaticActionModel();
425         }
426         
427         
428         
429         
430
431
432         ////////////  Main model  /////////////
433
434         /*
435          * The main model handles all values in SI units, i.e. no conversion is made within the model.
436          */
437         
438         private final ChangeSource source;
439         private final String valueName;
440         private final double multiplier;
441         
442         private final Method getMethod;
443         private final Method setMethod;
444         
445         private final Method getAutoMethod;
446         private final Method setAutoMethod;
447         
448         private final ArrayList<ChangeListener> listeners = new ArrayList<ChangeListener>();
449         
450         private final UnitGroup units;
451         private Unit currentUnit;
452
453         private final double minValue;
454         private final double maxValue;
455
456         
457         private int firing = 0;  //  >0 when model itself is sending events
458         
459         
460         // Used to differentiate changes in valueName and other changes in the component:
461         private double lastValue = 0;
462         private boolean lastAutomatic = false;
463                 
464         
465         public DoubleModel(double value) {
466                 this(value, UnitGroup.UNITS_NONE,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
467         }
468         
469         public DoubleModel(double value, UnitGroup unit) {
470                 this(value,unit,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
471         }
472         
473         public DoubleModel(double value, UnitGroup unit, double min) {
474                 this(value,unit,min,Double.POSITIVE_INFINITY);
475         }
476         
477         public DoubleModel(double value, UnitGroup unit, double min, double max) {
478                 this.lastValue = value;
479                 this.minValue = min;
480                 this.maxValue = max;
481
482                 source = null;
483                 valueName = "Constant value";
484                 multiplier = 1;
485                 
486                 getMethod = setMethod = null;
487                 getAutoMethod = setAutoMethod = null;
488                 units = unit;
489                 currentUnit = units.getDefaultUnit();
490         }
491
492         
493         /**
494          * Generates a new DoubleModel that changes the values of the specified component.
495          * The double value is read and written using the methods "get"/"set" + valueName.
496          *  
497          * @param source Component whose parameter to use.
498          * @param valueName Name of metods used to get/set the parameter.
499          * @param multiplier Value shown by the model is the value from component.getXXX * multiplier
500          * @param min Minimum value allowed (in SI units)
501          * @param max Maximum value allowed (in SI units)
502          */
503         public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit,
504                         double min, double max) {
505                 this.source = source;
506                 this.valueName = valueName;
507                 this.multiplier = multiplier;
508
509                 this.units = unit;
510                 currentUnit = units.getDefaultUnit();
511                 
512                 this.minValue = min;
513                 this.maxValue = max;
514                 
515                 try {
516                         getMethod = source.getClass().getMethod("get" + valueName);
517                 } catch (NoSuchMethodException e) {
518                         throw new IllegalArgumentException("get method for value '"+valueName+
519                                         "' not present in class "+source.getClass().getCanonicalName());
520                 }
521
522                 Method s=null;
523                 try {
524                         s = source.getClass().getMethod("set" + valueName,double.class);
525                 } catch (NoSuchMethodException e1) { }  // Ignore
526                 setMethod = s;
527                 
528                 // Automatic selection methods
529                 
530                 Method set=null,get=null;
531                 
532                 try {
533                         get = source.getClass().getMethod("is" + valueName + "Automatic");
534                         set = source.getClass().getMethod("set" + valueName + "Automatic",boolean.class);
535                 } catch (NoSuchMethodException e) { } // ignore
536                 
537                 if (set!=null && get!=null) {
538                         getAutoMethod = get;
539                         setAutoMethod = set;
540                 } else {
541                         getAutoMethod = null;
542                         setAutoMethod = null;
543                 }
544                 
545         }
546
547         public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit,
548                         double min) {
549                 this(source,valueName,multiplier,unit,min,Double.POSITIVE_INFINITY);
550         }
551         
552         public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit) {
553                 this(source,valueName,multiplier,unit,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
554         }
555         
556         public DoubleModel(ChangeSource source, String valueName, UnitGroup unit, 
557                         double min, double max) {
558                 this(source,valueName,1.0,unit,min,max);
559         }
560         
561         public DoubleModel(ChangeSource source, String valueName, UnitGroup unit, double min) {
562                 this(source,valueName,1.0,unit,min,Double.POSITIVE_INFINITY);
563         }
564         
565         public DoubleModel(ChangeSource source, String valueName, UnitGroup unit) {
566                 this(source,valueName,1.0,unit,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
567         }
568
569         public DoubleModel(ChangeSource source, String valueName) {
570                 this(source,valueName,1.0,UnitGroup.UNITS_NONE,
571                                 Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
572         }
573
574         public DoubleModel(ChangeSource source, String valueName, double min) {
575                 this(source,valueName,1.0,UnitGroup.UNITS_NONE,min,Double.POSITIVE_INFINITY);
576         }
577         
578         public DoubleModel(ChangeSource source, String valueName, double min, double max) {
579                 this(source,valueName,1.0,UnitGroup.UNITS_NONE,min,max);
580         }
581         
582         
583         
584         /**
585          * Returns the value of the variable (in SI units).
586          */
587         public double getValue() {
588                 if (getMethod==null)  // Constant value
589                         return lastValue;
590
591                 try {
592                         return (Double)getMethod.invoke(source)*multiplier;
593                 } catch (IllegalArgumentException e) {
594                         throw new BugException("BUG: Unable to invoke getMethod of "+this, e);
595                 } catch (IllegalAccessException e) {
596                         throw new BugException("BUG: Unable to invoke getMethod of "+this, e);
597                 } catch (InvocationTargetException e) {
598                         throw Reflection.handleWrappedException(e);
599                 }
600         }
601         
602         /**
603          * Sets the value of the variable.
604          * @param v New value for parameter in SI units.
605          */
606         public void setValue(double v) {
607                 if (setMethod==null) {
608                         if (getMethod != null) {
609                                 throw new RuntimeException("setMethod not available for variable '"+valueName+
610                                                 "' in class "+source.getClass().getCanonicalName());
611                         }
612                         lastValue = v;
613                         fireStateChanged();
614                         return;
615                 }
616
617                 try {
618                         setMethod.invoke(source, v/multiplier);
619                 } catch (IllegalArgumentException e) {
620                         throw new BugException("BUG: Unable to invoke setMethod of "+this, e);
621                 } catch (IllegalAccessException e) {
622                         throw new BugException("BUG: Unable to invoke setMethod of "+this, e);
623                 } catch (InvocationTargetException e) {
624                         throw Reflection.handleWrappedException(e);
625                 }
626         }
627
628         
629         /**
630          * Returns whether setting the value automatically is available.
631          */
632         public boolean isAutomaticAvailable() {
633                 return (getAutoMethod != null) && (setAutoMethod != null);
634         }
635
636         /**
637          * Returns whether the value is currently being set automatically.
638          * Returns false if automatic setting is not available at all.
639          */
640         public boolean isAutomatic() {
641                 if (getAutoMethod == null)
642                         return false;
643                 
644                 try {
645                         return (Boolean)getAutoMethod.invoke(source);
646                 } catch (IllegalArgumentException e) {
647                         throw new BugException("Method call failed", e);
648                 } catch (IllegalAccessException e) {
649                         throw new BugException("Method call failed", e);
650                 } catch (InvocationTargetException e) {
651                         throw Reflection.handleWrappedException(e);
652                 }
653         }
654         
655         /**
656          * Sets whether the value should be set automatically.  Simply fires a
657          * state change event if automatic setting is not available.
658          */
659         public void setAutomatic(boolean auto) {
660                 if (setAutoMethod == null) {
661                         fireStateChanged();  // in case something is out-of-sync
662                         return;
663                 }
664                 
665                 lastAutomatic = auto;
666                 try {
667                         setAutoMethod.invoke(source, auto);
668                 } catch (IllegalArgumentException e) {
669                         throw new BugException(e);
670                 } catch (IllegalAccessException e) {
671                         throw new BugException(e);
672                 } catch (InvocationTargetException e) {
673                         throw Reflection.handleWrappedException(e);
674                 }
675         }
676         
677
678         /**
679          * Returns the current Unit.  At the beginning it is the default unit of the UnitGroup.
680          * @return The most recently set unit.
681          */
682         public Unit getCurrentUnit() {
683                 return currentUnit;
684         }
685         
686         /**
687          * Sets the current Unit.  The unit must be one of those included in the UnitGroup.
688          * @param u  The unit to set active.
689          */
690         public void setCurrentUnit(Unit u) {
691                 if (currentUnit == u)
692                         return;
693                 currentUnit = u;
694                 fireStateChanged();
695         }
696         
697         
698         /**
699          * Returns the UnitGroup associated with the parameter value.
700          *
701          * @return The UnitGroup given to the constructor.
702          */
703         public UnitGroup getUnitGroup() {
704                 return units;
705         }
706         
707         
708         
709         /**
710          * Add a listener to the model.  Adds the model as a listener to the value source if this
711          * is the first listener.
712          * @param l Listener to add.
713          */
714         public void addChangeListener(ChangeListener l) {
715                 if (listeners.isEmpty()) {
716                         if (source != null) {
717                                 source.addChangeListener(this);
718                                 lastValue = getValue();
719                                 lastAutomatic = isAutomatic();
720                         }
721                 }
722
723                 listeners.add(l);
724                 if (DEBUG_LISTENERS)
725                         System.out.println(this+" adding listener (total "+listeners.size()+"): "+l);
726         }
727
728         /**
729          * Remove a listener from the model.  Removes the model from being a listener to the Component
730          * if this was the last listener of the model.
731          * @param l Listener to remove.
732          */
733         public void removeChangeListener(ChangeListener l) {
734                 listeners.remove(l);
735                 if (listeners.isEmpty() && source != null) {
736                         source.removeChangeListener(this);
737                 }
738                 if (DEBUG_LISTENERS)
739                         System.out.println(this+" removing listener (total "+listeners.size()+"): "+l);
740         }
741         
742         /**
743          * Fire a ChangeEvent to all listeners.
744          */
745         protected void fireStateChanged() {
746                 Object[] l = listeners.toArray();
747                 ChangeEvent event = new ChangeEvent(this);
748                 firing++;
749                 for (int i=0; i<l.length; i++)
750                         ((ChangeListener)l[i]).stateChanged(event);
751                 firing--;
752         }
753
754         /**
755          * Called when the component changes.  Checks whether the modeled value has changed, and if
756          * it has, updates lastValue and generates ChangeEvents for all listeners of the model.
757          */
758         public void stateChanged(ChangeEvent e) {
759                 double v = getValue();
760                 boolean b = isAutomatic();
761                 if (lastValue == v && lastAutomatic == b)
762                         return;
763                 lastValue = v;
764                 lastAutomatic = b;
765                 fireStateChanged();
766         }
767
768         /**
769          * Explain the DoubleModel as a String.
770          */
771         @Override
772         public String toString() {
773                 if (source == null)
774                         return "DoubleModel[constant="+lastValue+"]";
775                 return "DoubleModel["+source.getClass().getCanonicalName()+":"+valueName+"]";
776         }
777 }