package org.andork.swing;
/*******************************************************************************
* Breakout Cave Survey Visualizer
*
* Copyright (C) 2014 James Edwards
*
* jedwards8 at fastmail dot fm
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 2 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*******************************************************************************/
import java.math.BigDecimal;
import javax.swing.AbstractSpinnerModel;
import javax.swing.SpinnerModel;
import javax.swing.SpinnerNumberModel;
import org.andork.util.Java7.Objects;
/**
* A better version of {@link SpinnerNumberModel} that is generic and allows all
* values to be {@code null}.
*
* @author andy.edwards
*
* @param <N>
* the number type (doesn't necessarily have to be a {@link Number}).
*/
public class BetterSpinnerNumberModel<N> extends AbstractSpinnerModel {
public static interface Arithmetic<N> {
/**
* @return {@code n0 + n1 * n1f}.
*/
public N addMultiplied(N n0, N n1, int n1f);
}
public static class BigDecimalArithmetic implements Arithmetic<BigDecimal> {
public static final BigDecimalArithmetic instance = new BigDecimalArithmetic();
private BigDecimalArithmetic() {
}
@Override
public BigDecimal addMultiplied(BigDecimal n0, BigDecimal n1, int n1f) {
return n0.add(n1.multiply(new BigDecimal(n1f)));
}
}
public static class DoubleArithmetic implements Arithmetic<Double> {
public static final DoubleArithmetic instance = new DoubleArithmetic();
private DoubleArithmetic() {
}
@Override
public Double addMultiplied(Double n0, Double n1, int n1f) {
return n0 + n1 * n1f;
}
}
public static class IntegerArithmetic implements Arithmetic<Integer> {
public static final IntegerArithmetic instance = new IntegerArithmetic();
private IntegerArithmetic() {
}
@Override
public Integer addMultiplied(Integer n0, Integer n1, int n1f) {
return n0 + n1 * n1f;
}
}
/**
*
*/
private static final long serialVersionUID = -8212960889479949894L;
public static BetterSpinnerNumberModel<BigDecimal> newInstance(BigDecimal value, BigDecimal minimum,
BigDecimal maximum, BigDecimal stepSize) {
return new BetterSpinnerNumberModel<BigDecimal>(BigDecimal.class, value, minimum, maximum, stepSize,
BigDecimalArithmetic.instance);
}
public static BetterSpinnerNumberModel<Double> newInstance(Double value, Double minimum, Double maximum,
Double stepSize) {
return new BetterSpinnerNumberModel<Double>(Double.class, value, minimum, maximum, stepSize,
DoubleArithmetic.instance);
}
public static BetterSpinnerNumberModel<Integer> newInstance(Integer value, Integer minimum, Integer maximum,
Integer stepSize) {
return new BetterSpinnerNumberModel<Integer>(Integer.class, value, minimum, maximum, stepSize,
IntegerArithmetic.instance);
}
private Class<N> numberClass;
private N stepSize, value, defaultValue;
private Comparable<N> minimum, maximum;
private Arithmetic<N> arithmetic;
/**
* Constructs a <code>SpinnerModel</code> that represents a closed sequence
* of numbers from <code>minimum</code> to <code>maximum</code>. The
* <code>nextValue</code> and <code>previousValue</code> methods compute
* elements of the sequence by adding or subtracting <code>stepSize</code>
* respectively. All of the parameters must be mutually
* <code>Comparable</code>, <code>value</code> and <code>stepSize</code>
* must be instances of <code>Integer</code> <code>Long</code>,
* <code>Float</code>, or <code>Double</code>.
* <p>
* The <code>minimum</code> and <code>maximum</code> parameters can be
* <code>null</code> to indicate that the range doesn't have an upper or
* lower bound. If <code>value</code> or <code>stepSize</code> is
* <code>null</code>, or if both <code>minimum</code> and
* <code>maximum</code> are specified and <code>mininum > maximum</code>
* then an <code>IllegalArgumentException</code> is thrown. Similarly if
* <code>(minimum <= value <= maximum</code>) is false, an
* <code>IllegalArgumentException</code> is thrown.
*
* @param value
* the current (non <code>null</code>) value of the model
* @param minimum
* the first number in the sequence or <code>null</code>
* @param maximum
* the last number in the sequence or <code>null</code>
* @param stepSize
* the difference between elements of the sequence
*
* @throws IllegalArgumentException
* if stepSize or value is <code>null</code> or if the following
* expression is false:
* <code>minimum <= value <= maximum</code>
*/
public BetterSpinnerNumberModel(Class<N> numberClass, N value, Comparable<N> minimum, Comparable<N> maximum,
N stepSize, Arithmetic<N> arithmetic) {
if (value != null && !((minimum == null || minimum.compareTo(value) <= 0)
&& (maximum == null || maximum.compareTo(value) >= 0))) {
throw new IllegalArgumentException("(minimum <= value <= maximum) is false");
}
this.numberClass = numberClass;
this.value = value;
this.minimum = minimum;
this.maximum = maximum;
this.stepSize = stepSize;
this.arithmetic = arithmetic;
}
/**
* Returns the last number in the sequence.
*
* @return the value of the <code>maximum</code> property
* @see #setMaximum
*/
public Comparable<N> getMaximum() {
return maximum;
}
/**
* Returns the first number in this sequence.
*
* @return the value of the <code>minimum</code> property
* @see #setMinimum
*/
public Comparable<N> getMinimum() {
return minimum;
}
/**
* Returns the next number in the sequence.
*
* @return <code>value + stepSize</code> or <code>null</code> if the sum
* exceeds <code>maximum</code>.
*
* @see SpinnerModel#getNextValue
* @see #getPreviousValue
* @see #setStepSize
*/
@Override
public Object getNextValue() {
return incrValue(+1);
}
/**
* Returns the value of the current element of the sequence.
*
* @return the value property
* @see #setValue
*/
public N getNumber() {
return value;
}
/**
* Returns the previous number in the sequence.
*
* @return <code>value - stepSize</code>, or <code>null</code> if the sum is
* less than <code>minimum</code>.
*
* @see SpinnerModel#getPreviousValue
* @see #getNextValue
* @see #setStepSize
*/
@Override
public Object getPreviousValue() {
return incrValue(-1);
}
/**
* Returns the size of the value change computed by the
* <code>getNextValue</code> and <code>getPreviousValue</code> methods.
*
* @return the value of the <code>stepSize</code> property
* @see #setStepSize
*/
public N getStepSize() {
return stepSize;
}
/**
* Returns the value of the current element of the sequence.
*
* @return the value property
* @see #setValue
* @see #getNumber
*/
@Override
public Object getValue() {
return value;
}
private N incrValue(int dir) {
N newValue;
N value = this.value == null ? defaultValue : this.value;
if (value == null || stepSize == null) {
return null;
}
newValue = arithmetic.addMultiplied(value, stepSize, dir);
if (maximum != null && maximum.compareTo(newValue) < 0) {
return null;
}
if (minimum != null && minimum.compareTo(newValue) > 0) {
return null;
} else {
return newValue;
}
}
/**
* Changes the upper bound for numbers in this sequence. If
* <code>maximum</code> is <code>null</code>, then there is no upper bound.
* No bounds checking is done here; the new <code>maximum</code> value may
* invalidate the <code>(minimum <= value < maximum)</code> invariant
* enforced by the constructors. This is to simplify updating the model,
* naturally one should ensure that the invariant is true before calling the
* <code>next</code>, <code>previous</code>, or <code>setValue</code>
* methods.
* <p>
* Typically this property is a <code>Number</code> of the same type as the
* <code>value</code> however it's possible to use any
* <code>Comparable</code> with a <code>compareTo</code> method for a
* <code>Number</code> with the same type as the value. See
* <a href="#setMinimum(java.lang.Comparable)"> <code>setMinimum</code></a>
* for an example.
* <p>
* This method fires a <code>ChangeEvent</code> if the <code>maximum</code>
* has changed.
*
* @param maximum
* a <code>Comparable</code> that has a <code>compareTo</code>
* method for <code>Number</code>s with the same type as
* <code>value</code>
* @see #getMaximum
* @see #setMinimum
* @see SpinnerModel#addChangeListener
*/
public void setMaximum(Comparable<N> maximum) {
if (maximum == null ? this.maximum != null : !maximum.equals(this.maximum)) {
this.maximum = maximum;
fireStateChanged();
}
}
/**
* Changes the lower bound for numbers in this sequence. If
* <code>minimum</code> is <code>null</code>, then there is no lower bound.
* No bounds checking is done here; the new <code>minimum</code> value may
* invalidate the <code>(minimum <= value <= maximum)</code> invariant
* enforced by the constructors. This is to simplify updating the model,
* naturally one should ensure that the invariant is true before calling the
* <code>getNextValue</code>, <code>getPreviousValue</code>, or
* <code>setValue</code> methods.
* <p>
* Typically this property is a <code>Number</code> of the same type as the
* <code>value</code> however it's possible to use any
* <code>Comparable</code> with a <code>compareTo</code> method for a
* <code>Number</code> with the same type as the value. For example if value
* was a <code>Long</code>, <code>minimum</code> might be a Date subclass
* defined like this:
*
* <pre>
* MyDate extends Date { // Date already implements Comparable
* public int compareTo(Long o) {
* long t = getTime();
* return (t < o.longValue() ? -1 : (t == o.longValue() ? 0 : 1));
* }
* }
* </pre>
* <p>
* This method fires a <code>ChangeEvent</code> if the <code>minimum</code>
* has changed.
*
* @param minimum
* a <code>Comparable</code> that has a <code>compareTo</code>
* method for <code>Number</code>s with the same type as
* <code>value</code>
* @see #getMinimum
* @see #setMaximum
* @see SpinnerModel#addChangeListener
*/
public void setMinimum(Comparable<N> minimum) {
if (minimum == null ? this.minimum != null : !minimum.equals(this.minimum)) {
this.minimum = minimum;
fireStateChanged();
}
}
/**
* Changes the size of the value change computed by the
* <code>getNextValue</code> and <code>getPreviousValue</code> methods. An
* <code>IllegalArgumentException</code> is thrown if <code>stepSize</code>
* is <code>null</code>.
* <p>
* This method fires a <code>ChangeEvent</code> if the <code>stepSize</code>
* has changed.
*
* @param stepSize
* the size of the value change computed by the
* <code>getNextValue</code> and <code>getPreviousValue</code>
* methods
* @see #getNextValue
* @see #getPreviousValue
* @see #getStepSize
* @see SpinnerModel#addChangeListener
*/
public void setStepSize(N stepSize) {
if (Objects.equals(this.stepSize, stepSize)) {
this.stepSize = stepSize;
fireStateChanged();
}
}
/**
* Sets the current value for this sequence. If <code>value</code> is
* <code>null</code>, or not a <code>Number</code>, an
* <code>IllegalArgumentException</code> is thrown. No bounds checking is
* done here; the new value may invalidate the
* <code>(minimum <= value <= maximum)</code> invariant enforced by
* the constructors. It's also possible to set the value to be something
* that wouldn't naturally occur in the sequence, i.e. a value that's not
* modulo the <code>stepSize</code>. This is to simplify updating the model,
* and to accommodate spinners that don't want to restrict values that have
* been directly entered by the user. Naturally, one should ensure that the
* <code>(minimum <= value <= maximum)</code> invariant is true before
* calling the <code>next</code>, <code>previous</code>, or
* <code>setValue</code> methods.
* <p>
* This method fires a <code>ChangeEvent</code> if the value has changed.
*
* @param value
* the current (non <code>null</code>) <code>Number</code> for
* this sequence
* @throws IllegalArgumentException
* if <code>value</code> is <code>null</code> or not a
* <code>Number</code>
* @see #getNumber
* @see #getValue
* @see SpinnerModel#addChangeListener
*/
@Override
public void setValue(Object value) {
if (value != null && !numberClass.isInstance(value)) {
throw new IllegalArgumentException("illegal value");
}
if (!Objects.equals(this.value, value)) {
this.value = value == null ? null : numberClass.cast(value);
fireStateChanged();
}
}
}