import java.util.EventObject;
import javax.swing.AbstractAction;
+import javax.swing.AbstractSpinnerModel;
import javax.swing.Action;
import javax.swing.BoundedRangeModel;
import javax.swing.SpinnerModel;
-import javax.swing.SpinnerNumberModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import net.sf.openrocket.unit.UnitGroup;
import net.sf.openrocket.util.BugException;
import net.sf.openrocket.util.ChangeSource;
+import net.sf.openrocket.util.ExpressionParser;
+import net.sf.openrocket.util.InvalidExpressionException;
import net.sf.openrocket.util.Invalidatable;
import net.sf.openrocket.util.Invalidator;
import net.sf.openrocket.util.MathUtil;
public class DoubleModel implements StateChangeListener, ChangeSource, Invalidatable {
private static final LogHelper log = Application.getLogger();
-
+
public static final DoubleModel ZERO = new DoubleModel(0);
//////////// JSpinner Model ////////////
/**
- * Model suitable for JSpinner using JSpinner.NumberEditor. It extends SpinnerNumberModel
+ * Model suitable for JSpinner.
+ * Note: Previously used using JSpinner.NumberEditor and extended SpinnerNumberModel
* to be compatible with the NumberEditor, but only has the necessary methods defined.
+ * This is still the design, but now extends AbstractSpinnerModel to allow other characters
+ * to be entered so that fractional units and expressions can be used.
*/
- private class ValueSpinnerModel extends SpinnerNumberModel implements Invalidatable {
+ public class ValueSpinnerModel extends AbstractSpinnerModel implements Invalidatable {
+
+ private ExpressionParser parser = new ExpressionParser();
@Override
public Object getValue() {
- return currentUnit.toUnit(DoubleModel.this.getValue());
+ return currentUnit.toString(DoubleModel.this.getValue());
}
@Override
" value=" + value + ", currently firing events");
return;
}
- Number num = (Number) value;
- double newValue = num.doubleValue();
- double converted = currentUnit.fromUnit(newValue);
- log.user("SpinnerModel setValue called for " + DoubleModel.this.toString() + " newValue=" + newValue +
- " converted=" + converted);
- DoubleModel.this.setValue(converted);
+ Number num = Double.NaN;
+
+ // Set num if possible
+ if (value instanceof Number) {
+ num = (Number) value;
+ }
+ else if (value instanceof String) {
+ try {
+ String newValString = (String) value;
+ num = parser.parse(newValString);
+ } catch (InvalidExpressionException e) {
+ // Ignore
+ }
+ }
+ // Update the doublemodel with the new number or return to the last number if not possible
+ if (((Double) num).isNaN()) {
+ DoubleModel.this.setValue(lastValue);
+ log.user("SpinnerModel could not set value for " + DoubleModel.this.toString() + ". Could not convert " + value.toString());
+ }
+ else {
+ double newValue = num.doubleValue();
+ double converted = currentUnit.fromUnit(newValue);
+
+ log.user("SpinnerModel setValue called for " + DoubleModel.this.toString() + " newValue=" + newValue +
+ " converted=" + converted);
+ DoubleModel.this.setValue(converted);
+ }
+
+ // Force a refresh if text doesn't match up exactly with the stored value
+ if (!((Double) lastValue).toString().equals(this.getValue().toString())) {
+ DoubleModel.this.fireStateChanged();
+ log.debug("SpinnerModel " + DoubleModel.this.toString() + " refresh forced because string did not match actual value.");
+ }
}
@Override
return d;
}
-
- @Override
- public Comparable<Double> getMinimum() {
- return currentUnit.toUnit(minValue);
- }
-
- @Override
- public Comparable<Double> getMaximum() {
- return currentUnit.toUnit(maxValue);
- }
-
-
@Override
public void addChangeListener(ChangeListener l) {
DoubleModel.this.addChangeListener(l);
return new ValueSpinnerModel();
}
-
-
-
-
//////////// JSlider model ////////////
private class ValueSliderModel implements BoundedRangeModel, StateChangeListener, Invalidatable {
* Use linear scale value = linear1 * x + linear0 when x < linearPosition
* Use quadratic scale value = quad2 * x^2 + quad1 * x + quad0 otherwise
*/
-
+ private final boolean islinear;
+
// Linear in range x <= linearPosition
private final double linearPosition;
//private final double linear0;
// Non-linear multiplier, exponent and constant
- private final double quad2, quad1, quad0;
+ private double quad2, quad1, quad0;
-
-
public ValueSliderModel(DoubleModel min, DoubleModel max) {
+ this.islinear = true;
linearPosition = 1.0;
this.min = min;
}
-
+
/**
* Generate a linear model from min to max.
*/
public ValueSliderModel(double min, double max) {
+ this.islinear = true;
linearPosition = 1.0;
this.min = new DoubleModel(min);
this(min, 0.5, mid, max);
}
+ public ValueSliderModel(double min, double mid, DoubleModel max) {
+ this(min, 0.5, mid, max);
+ }
+
/*
* v(x) = mul * x^exp + add
*
* v'(pos) = mul*exp * pos^(exp-1) = linearMul
*/
public ValueSliderModel(double min, double pos, double mid, double max) {
+ this(min, pos, mid, new DoubleModel(max));
+ }
+
+ public ValueSliderModel(double min, double pos, double mid, DoubleModel max) {
this.min = new DoubleModel(min);
this.mid = new DoubleModel(mid);
- this.max = new DoubleModel(max);
+ this.max = max;
+
+ this.islinear = false;
+
+ max.addChangeListener(this);
-
linearPosition = pos;
//linear0 = min;
//linear1 = (mid-min)/pos;
- if (!(min < mid && mid <= max && 0 < pos && pos < 1)) {
+ if (!(min < mid && mid <= max.getValue() && 0 < pos && pos < 1)) {
throw new IllegalArgumentException("Bad arguments for ValueSliderModel " +
"min=" + min + " mid=" + mid + " max=" + max + " pos=" + pos);
}
+ updateExponentialParameters();
+
+ }
+
+ private void updateExponentialParameters() {
+ double pos = this.linearPosition;
+ double minValue = this.min.getValue();
+ double midValue = this.mid.getValue();
+ double maxValue = this.max.getValue();
/*
* quad2..0 are calculated such that
* f(pos) = mid - continuity
* f(1) = max - end point
* f'(pos) = linear1 - continuity of derivative
*/
-
- double delta = (mid - min) / pos;
- quad2 = (max - mid - delta + delta * pos) / pow2(pos - 1);
- quad1 = (delta + 2 * (mid - max) * pos - delta * pos * pos) / pow2(pos - 1);
- quad0 = (mid - (2 * mid + delta) * pos + (max + delta) * pos * pos) / pow2(pos - 1);
-
+ double delta = (midValue - minValue) / pos;
+ quad2 = (maxValue - midValue - delta + delta * pos) / pow2(pos - 1);
+ quad1 = (delta + 2 * (midValue - maxValue) * pos - delta * pos * pos) / pow2(pos - 1);
+ quad0 = (midValue - (2 * midValue + delta) * pos + (maxValue + delta) * pos * pos) / pow2(pos - 1);
}
private double pow2(double x) {
@Override
public void stateChanged(EventObject e) {
// Min or max range has changed.
+ if (!islinear) {
+ double midValue = (max.getValue() - min.getValue()) / 3.0;
+ mid.setValue(midValue);
+ updateExponentialParameters();
+ }
// Fire if not already firing
if (firing == 0)
fireStateChanged();
return new ValueSliderModel(min, mid, max);
}
+ public BoundedRangeModel getSliderModel(double min, double mid, DoubleModel max) {
+ return new ValueSliderModel(min, mid, max);
+ }
+
public BoundedRangeModel getSliderModel(double min, double pos, double mid, double max) {
return new ValueSliderModel(min, pos, mid, max);
}
-
-
-
+
+
+
//////////// Action model ////////////
private class AutomaticActionModel extends AbstractAction implements StateChangeListener, Invalidatable {
}
-
-
-
+
+
+
//////////// Main model /////////////
/*
* The main model handles all values in SI units, i.e. no conversion is made within the model.
*/
-
+
private final ChangeSource source;
private final String valueName;
private final double multiplier;
private Unit currentUnit;
private final double minValue;
- private final double maxValue;
+ private double maxValue;
private String toString = null;
-
+
private int firing = 0; // >0 when model itself is sending events
-
+
// Used to differentiate changes in valueName and other changes in the component:
private double lastValue = 0;
private boolean lastAutomatic = false;
}
-
+
/**
* Returns the value of the variable (in SI units).
*/
}
}
-
/**
* Returns whether setting the value automatically is available.
*/
}
-
+
/**
* Add a listener to the model. Adds the model as a listener to the value source if this
* is the first listener.
// Copy the list before iterating to prevent concurrent modification exceptions.
EventListener[] ls = listeners.toArray(new EventListener[0]);
for (EventListener l : ls) {
- if ( l instanceof StateChangeListener ) {
- ((StateChangeListener)l).stateChanged(event);
- } else if ( l instanceof ChangeListener ) {
- ((ChangeListener)l).stateChanged(cevent);
+ if (l instanceof StateChangeListener) {
+ ((StateChangeListener) l).stateChanged(event);
+ } else if (l instanceof ChangeListener) {
+ ((ChangeListener) l).stateChanged(cevent);
}
}
firing--;