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