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