HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML
 

It's ironic how sometimes the simplest ideas can turn out to be the most development-intensive. This month's Widget Factory participant is the seemingly modest JSpinner control, which lets you constrain user interface selections by using arrow buttons or up/down keystrokes to increment or decrement values, typically in a field. JSpinner comes with a whole family of siblings to handle numbers, currency, percentage, date, time, lists and custom values. It supports multiple field elements, custom renderers and a compound model to make it all possible.

The table at right shows the various spinner controls we'll be implementing. JSpinner and JSpinnerField are the basic classes. The others stand as good examples of what can be done with a well-designed premise. You'll rarely tend to use JSpinner directly. You'll usually reach for JSpinnerField or one of its subclasses to do specific work. Figure 1 shows the JSpinner family at work.

Figure 1
Figure 1:

It's worth noting that this installment of the Widget Factory has a large number of listings, but most of the classes involve only a small amount of code. The main classes, like JSpinnerField, DefaultSpinModel, DefaultSpinRangeModel and DefaultRenderer, do most of the work. The SpinTime-Model and SpinDateModel are relatively uncomplicated, for example, as are various JSpinnerField extensions. Several of the listings are merely interfaces that maintain flexibility in our design, supporting reuse and extensibility.

Architecture
The JSpinner architecture uses several interfaces to maximize flexibility. The SpinModel interface defines the methods required to access the model, which represents values in the JSpinner architecture. The SpinModel contains one or more instances of a class that implements the SpinRangeModel. Listing 1 shows the SpinModel interface. The SpinModel contains ranges that can be accessed by a field ID. These identifiers map directly onto the field identifiers provided by the text Format classes in the Java API.

The SpinModel always has an active field and can get and set a list of field IDs. This is important when you need to switch between locales because the subfield order is not always the same. We also support the ChangeListener interface so that views can be updated when the model changes.

The SpinRangeModel is very much like the BoundedRangeModel provided by the Swing API, but it supports the use of decimal values. Listing 2 shows how a SpinRangeModel allows you to set and get the currently selected value, a minimum and maximum value and an increment (extent), and whether the model wraps or not when it hits a boundary. In contrast, the Bounded-RangeModel doesn't permit the maximum value ever to be selected and is restricted to integer values. It also knows nothing about wrapping values, so it was necessary to invent a new model for the JSpinner controls. The BoundedRangeModel also supports the ChangeListener interface, which is used by the SpinModel to detect changes.

The SpinRenderer interface is designed to support custom renderers in JSpinner controls. The only required method is called getSpinCellRendererComponent and returns the rendering component. Listing 3 shows the SpinRenderer interface. We expect a reference to a JSpinnerField, the current value object, a flag indicating whether we have the focus, a Format instance and the field identifier for the currently selected field. We'll cover this in more detail when we implement the DefaultSpinRenderer.

Figure 2 shows the basic relationship between JSpinner, JSpinnerField and the simplest model configuration.

Figure 2
Figure 2:

The JSpinnerField class is the parent of all the other widgets implemented in this article. The JSpinner class handles the buttons and up/down activity. It operates directly on the model and can be used for other purposes requiring up/down activities. That's why it was given the big "J" prefix. You can create an arbitrary SpinModel if you like, regardless of whether you use a view to watch the results. Notice that change events from the SpinRangeModel are sent to the SpinModel. This happens automatically and you can watch for the SpinModel events, comfortable in the certainty that you'll never miss any other change events.

The JSpinner Family of Classes
Spinner
Provides a pair of up- and down-arrow buttons and operates on a SpinModel to increment or decrement values. It handles keyboard events as well. It's up to other components to watch the model and update their views when they receive a ChangeEvent.
JSpinnerField
A basic numerical spinner that uses a DefaultSpinRenderer and DefaultSpinModel to manage a single range of values. A SpinModel may contain more than one SpinRangeModel, but the JSpinnerField requires only one. This is the base class for all the other family members. It takes responsibility for certain mouse events, listening for model changes and focus handling.
JSpinnerPercent
The percentage spinner uses the NumberFormat.getPercentInstance to format a locale-dependent percentage field.
JSpinnerList
The string list spinner uses a ChoiceFormat to format a list selection. This is a simple field, useful for handling small lists of selected options.
JSpinnerCurrency
The currency spinner uses the NumberFormat.getCurrencyInstance to format a locale-dependent currency field. This is a compound field that supports incrementing and decrementing the integer and decimal values independently of each other.
JSpinnerTime
The time spinner uses the DateFormat.getTimeInstance to format a locale-dependent time field. This implementation uses a SpinTimeModel to map the Calendar object onto a SpinModel. It is a compound field with three elements.
JSpinnerDate
The date spinner uses the DateFormat.getDateInstance to format a locale-dependent date field. This implementation uses a SpinDateModel to map the Calendar object onto a SpinModel. It is a compound field with three elements.
JSpinnerColor
This is a color selection spinner example of using a custom SpinRenderer.

Modeling
The SpinModel and SpinRangeModel interfaces need concrete implementations to provide the functionality they expose. The DefaultSpinModel and DefaultSpinRangeModel provide a generic set of capabilities that most of the controls in this article use. Listing 4 shows the DefaultSpinModel class. The internal list of SpinRangeModel instances is maintained by a hashtable that is accessed by a fieldID Integer. A separate, ordered list of fieldIDs is held in a Vector object, as are the registered change listeners. We also keep an activeField value to indicate the currently active SpinRangeModel.

To make life easier, we expose three constructors. The first simply creates an empty model. The second assumes we will use a single SpinRangeModel and takes the same set of arguments, creating the SpinRangeModel automatically. The third constructor assumes that we plan to use two SpinRangeModels and does the same thing, automatically creating both for us. More than two subfields would make the constructor too complicated, so we assume it's just as easy to add fields outside the constructor.

Listing 5 shows the DefaultSpinRangeModel class. The basics are pretty simple, with the constructor accepting each of the arguments and get/set accessors provided for each of the attributes. Worth noting, however, is that the stateChange event is not fired unless the setValueIsAdjusting method is called with a false argument. This is intended to defer the change event to avoid inconsistent states. My implementation is less robust than the Swing BoundedRangeModel, but it works well enough in practice.

The JSpinner class is provided in Listing 6. The constructor expects a SpinModel instance and creates the up and down buttons using the Swing BasicArrowButton class. We register JSpinner as both an ActionListener and a KeyListener to handle increment and decrement operations on the model.

Most of the code that acts on the model is in the increment and decrement methods that handle boundary conditions, deciding whether or not to wrap. The JSpinner class also handles right- and left-arrow keystrokes and changes the active field in the SpinModel. To make this work, you have to register JSpinner as a KeyListener from elsewhere, since the buttons never really get the focus.

JSpinnerField
The JSpinnerField (see Listing 7) is the simplest instance of the JSpinner family of controls, but because it's the parent of all the other family members, it's designed to handle general circumstances. As such, it contains more code than most of the other classes in this article. There are three constructors to let us create an empty JSpinnerField, one with a single SpinRangeModel, or one with an arbitrary model, renderer and field Format class. Because we need to refresh the view after construction, we delegate subcomponent creation to an init method. This allows subclasses to control the call to refreshSpinView, which tends to be specific to selected implementations.

The setLocale method sets a localized instance of the Format class associated with the control. This is also overridden by child implementations. The updateField-Order method is required to determine what the correct field order is for a given locale. This is handled in a separate utility class called LocaleUtil (see Listing 8), which effectively sorts the fields based on their starting location in the Format object. It also implements a findMouseInField method that lets us determine which field is active when the mouse is clicked on a JSpinnerField.

The JSpinnerField does the rendering through a SpinField class that expects a reference to the JSpinnerField object. As you can see in Listing 9, this class extends JPanel and uses a Swing CellRendererPane to do the actual rendering. The paintComponent call gets the current SpinRenderer and calls its getSpinCellRendererComponent method. We also override the getPreferredSize and getMinimumSize to return the renderer's preferred and minimum sizes.

Listing 10 shows the DefaultSpinRenderer, which extends JTextField and sets the editable flag to false. The getSpinCellRendererComponent method returns the current instance after calling setText with the current value object formatted by the Format instance. It also uses the LocaleUtil.getFieldPosition method to determine what the current selection range is for display if we have the focus.

Simple Extensions
Having just covered the DefaultSpinRenderer, let's take a look at the JSpinnerColor class, which uses a custom renderer to spin through a short set of colors. The ColorSpinRender, shown in Listing 11, is pretty uncomplicated; it ignores most of the getSpinCellRendererComponent arguments and expects to see a Color object as the value. We use a white border to indicate the focus.

Listing 12 shows how simple the JSpinnerColor class is to implement. We extend the JSpinnerField class with a custom spin renderer and a simple model that ranges from zero to the length of our list. We store our list of Color objects in a Vector for convenience. To avoid formatting issues, we declare an empty updateFieldOrder method. We override the refreshSpinView to update the view when the selection changes. All we have to do is get the active model field and range, and set the current list selection based on the active model value. Calling setValue on the SpinField gives the renderer access to the active object.

The JSpinnerList class (see Listing 13) does the same kind of thing without the extra complication of a custom renderer, given that it handles a string list. It overrides the setLocale method because explicit strings are not localizable. The JSpinnerList and JSpinnerColor widgets demonstrate how easy it is to subclass JSpinnerField to create customized behavior. There is no restriction in the data you choose to represent, since both the model and renderer are under your control. Of course, these implementations are not internationalizable unless you use resource bundles or account for it directly in your code.

Internationalizable Spinners
The last four variations on our theme capitalize on the JSpinnerField infrastructure to handle internationalization through the Java Format class. Listing 14 shows the JSpinnerPercent class, which extends JSpinnerField and implements very little code. The constructor creates a model that uses 0.01 as an increment and sets NumberFormat.getPercentInstance() as the format class. We override setLocale to reset the Format class if necessary.

Listing 15 shows the JSpinnerCurrency control, which is similar but extends the model to use two ranges. One handles the integer portion of our currency field and the other handles the decimal value. We set the NumberFormat.getCurrencyInstance() format in both the constructor and the setLocale methods. The only other thing we need to do is override the refreshSpinView method to properly set the value from the two model ranges. Figure 3 shows the relationship between classes in the JSpinnerCurrency control.

Figure 3
Figure 3:

The last two widget variations are only slightly more complicated because they use custom models. Listings 16 and 17 show the TimeSpinModel and DateSpinModel, respectively. Both extend the DefaultSpinModel but override the setRange and getRange methods to control where the values come from. They map the Calendar class values onto the range models as they are being retrieved and manage the Calendar instance that represents the time or date with a couple of accessor methods. You can do something similar to manage your own variations on a SpinModel.

The JSpinnerTime and JSpinnerDate classes are in Listings 18 and 19, respectively. They both override the constructor, setLocale and refreshSpinView methods, but are otherwise unencumbered. Listing 20 shows the JSpinnerTest harness used to test the controls. Figure 4 shows the way it looks when the French locale is selected.

Figure 4
Figure 4:

Summary
As you've seen, despite all the listings, JSpinner controls are actually quite simple to use. By providing customizable model and renderer interfaces, the variations you can implement are wide open, as they should be with any open architecture. All the internationalization issues are essentially transparent, thanks to the Format classes provided in the Java API. It would have been easy to write this article around a simpler implementation, but the strength of these widgets is largely in their customizability and in the lessons learned from effective design. I hope you'll agree that even this simple widget turned out to be instructive and worth the investment.

About the Author
Claude Duguay has been programming since 1980. In 1988 he founded LogiCraft Corporation, and he currently leads the development team at Atrieva Corp. You can reach him at [email protected]

	

Listing 1.
 
public interface SpinModel 
{ 
  public int getFieldCount(); 
  public int getActiveField(); 
  public void setActiveField(int fieldID); 
  public void setNextField(); 
  public void setPrevField(); 
  public int[] getFieldIDs(); 
  public void setFieldIDs(int[] fields); 
  public void setRange(int fieldID, SpinRangeModel range); 
  public SpinRangeModel getRange(int fieldID); 
  public void addChangeListener(ChangeListener listener); 
  public void removeChangeListener(ChangeListener listener); 
} 

Listing 2.
 
public interface SpinRangeModel 
{ 
  public double getValue(); 
  public double getExtent(); 
  public double getMinimum(); 
  public double getMaximum(); 
  public boolean getWrap(); 
  public void setValue(double value); 
  public void setExtent(double extent); 
  public void setMinimum(double min); 
  public void setMaximum(double max); 
  public void setWrap(boolean wrap); 
  public void setValueIsAdjusting(boolean adusting); 
  public boolean getValueIsAdjusting(); 
  public void addChangeListener(ChangeListener listener); 
  public void removeChangeListener(ChangeListener listener); 
} 

Listing 3.
 
public interface SpinRenderer 
{ 
  public Component getSpinCellRendererComponent( 
    JSpinnerField spin, Object value, boolean hasFocus, 
    Format formatter, int selectedFieldID); 
} 

Listing 4.
 
public class DefaultSpinModel 
  implements SpinModel, ChangeListener 
{ 
  protected int activeField; 
  protected Vector fieldIDs = new Vector(); 
  protected Hashtable table = new Hashtable(); 
  protected Vector listeners = new Vector(); 

  public DefaultSpinModel() {} 

  public DefaultSpinModel(double value, 
    double extent, double min, double max, boolean wrap) 
  { 
    SpinRangeModel model = new DefaultSpinRangeModel( 
      value, extent, min, max, wrap); 
    setRange(NumberFormat.INTEGER_FIELD, model); 
    setActiveField(NumberFormat.INTEGER_FIELD); 
  } 

  public DefaultSpinModel( 
    double iValue, double iExtent, double iMin, double 
iMax, boolean iWrap, 
    double fValue, double fExtent, double fMin, double 
fMax, boolean fWrap) 
  { 
    SpinRangeModel iModel = new DefaultSpinRangeModel( 
      iValue, iExtent, iMin, iMax, iWrap); 
    setRange(NumberFormat.INTEGER_FIELD, iModel); 

    SpinRangeModel fModel = new DefaultSpinRangeModel( 
      fValue, fExtent, fMin, fMax, fWrap); 
    setRange(NumberFormat.FRACTION_FIELD, fModel); 

    setActiveField(NumberFormat.INTEGER_FIELD); 
  } 

  public int getFieldCount() 
  { 
    return fieldIDs.size(); 
  } 

  public int getActiveField() 
  { 
    return activeField; 
  } 

  public void setActiveField(int fieldID) 
  { 
    activeField = fieldID; 
    fireStateChanged(); 
  } 

  public void setNextField() 
  { 
    int[] array = getFieldIDs(); 
    for (int i = 0; i < array.length; i++) 
    { 
      if (array[i] == activeField)// && i != 
array.length 
- 1) 
      { 
        setActiveField(array[Math.min(i + 1, 
array.length - 1)]); 
        return; 
      } 
    } 
  } 

  public void setPrevField() 
  { 
    int[] array = getFieldIDs(); 
    for (int i = 0; i < array.length; i++) 
    { 
      if (array[i] == activeField) 
      { 
        setActiveField(array[Math.max(i - 1, 0)]); 
        return; 
      } 
    } 
  } 

  public void setRange(int id, SpinRangeModel range) 
  { 
    Integer key = new Integer(id); 
    if (!table.containsKey(key)) 
    { 
      fieldIDs.addElement(key); 
      range.addChangeListener(this); 
    } 
    table.put(key, range); 
  } 

  public SpinRangeModel getRange(int id) 
  { 
    Integer key = new Integer(id); 
    return (SpinRangeModel)table.get(key); 
  } 

  public int[] getFieldIDs() 
  { 
    int size = fieldIDs.size(); 
    int[] array = new int[size]; 
    for (int i = 0; i < size; i++) 
    { 
      array[i] = 
((Integer)fieldIDs.elementAt(i)).intValue(); 
    } 
    return array; 
  } 

  public void setFieldIDs(int[] fields) 
  { 
    fieldIDs.removeAllElements(); 
    for (int i = 0; i < fields.length; i++) 
    { 
      fieldIDs.addElement(new Integer(fields[i])); 
    } 
  } 

  public void stateChanged(ChangeEvent event) 
  { 
    fireStateChanged(); 
  } 

  public void addChangeListener(ChangeListener 
listener) 
  { 
    listeners.addElement(listener); 
  } 

  public void removeChangeListener(ChangeListener 
listener) 
  { 
    listeners.removeElement(listener); 
  } 

  public void fireStateChanged() 
  { 
    ChangeListener listener; 
    Vector list = (Vector)listeners.clone(); 
    ChangeEvent event = new ChangeEvent(this); 
    for (int i = 0; i < list.size(); i++) 
    { 
      listener = ((ChangeListener)list.elementAt(i)); 
      listener.stateChanged(event); 
    } 
  } 
} 

Listing 5.
 
public class DefaultSpinRangeModel 
  implements SpinRangeModel 
{ 
    protected Vector listeners = new Vector(); 

    private double value = 0; 
    private double extent = 1; 
    private double min = 0; 
    private double max = 100; 
    private boolean wrap = true; 
    private boolean isAdjusting = false; 

  public DefaultSpinRangeModel() {} 

  public DefaultSpinRangeModel(double value, 
double extent, 
    double min, double max, boolean wrap) 
  { 
    this.value = value; 
    this.extent = extent; 
    this.min = min; 
    this.max = max; 
  } 

  public double getValue() 
  { 
    return value; 
  } 

  public double getExtent() 
  { 
    return extent; 
  } 

  public double getMinimum() 
  { 
    return min; 
  } 

  public double getMaximum() 
  { 
    return max; 
  } 

  public boolean getWrap() 
  { 
    return wrap; 
  } 

  public void setValue(double value) 
  { 
    this.value = value; 
  } 

  public void setExtent(double extent) 
  { 
    this.extent = extent; 
  } 

  public void setMinimum(double min) 
  { 
    this.min = min; 
  } 

  public void setMaximum(double max) 
  { 
    this.max = max; 
  } 

  public void setWrap(boolean wrap) 
  { 
    this.wrap = wrap; 
  } 

  public void setValueIsAdjusting(boolean isAdusting) 
  { 
    this.isAdjusting = isAdjusting; 
    if (!isAdjusting) fireStateChanged(); 
  } 

  public boolean getValueIsAdjusting() 
  { 
    return isAdjusting; 
  } 

  public void addChangeListener(ChangeListener 
listener) 
  { 
    listeners.addElement(listener); 
  } 

  public void removeChangeListener(ChangeListener 
listener) 
  { 
    listeners.removeElement(listener); 
  } 

  public void fireStateChanged() 
  { 
    ChangeListener listener; 
    Vector list = (Vector)listeners.clone(); 
    ChangeEvent event = new ChangeEvent(this); 
    for (int i = 0; i < list.size(); i++) 
    { 
      listener = ((ChangeListener)list.elementAt(i)); 
      listener.stateChanged(event); 
    } 
  } 

  public String toString() 
  { 
    String modelString = 
      "value=" + getValue() + ", " + 
      "extent=" + getExtent() + ", " + 
      "min=" + getMinimum() + ", " + 
      "max=" + getMaximum() + ", " + 
      "adj=" + getValueIsAdjusting(); 
    return getClass().getName() + "[" + modelString + "]"; 
  } 
} 

Listing 6.
 
public class JSpinner extends JPanel 
  implements ActionListener, KeyListener, SwingConstants 
{ 
  protected Vector listeners = new Vector(); 
  protected BasicArrowButton north, south; 
  SpinModel model; 

  public JSpinner(SpinModel model) 
  { 
    this.model = model; 
    setLayout(new GridLayout(2, 1)); 
    setPreferredSize(new Dimension(16, 16)); 

    north = new BasicArrowButton(BasicArrowButton.NORTH); 
    north.addActionListener(this); 
    add(north); 

    south = new BasicArrowButton(BasicArrowButton.SOUTH); 
    south.addActionListener(this); 
    add(south); 
  } 

  public void actionPerformed(ActionEvent event) 
  { 
    if (event.getSource() == north) 
    { 
      increment(); 
    } 
    if (event.getSource() == south) 
    { 
      decrement(); 
    } 
  } 

  public void keyTyped(KeyEvent event) {} 
  public void keyReleased(KeyEvent event) {} 
  public void keyPressed(KeyEvent event) 
  { 
    int code = event.getKeyCode(); 
    if (code == KeyEvent.VK_UP) 
    { 
      increment(); 
    } 
    if (code == KeyEvent.VK_DOWN) 
    { 
      decrement(); 
    } 
    if (code == KeyEvent.VK_LEFT) 
    { 
      model.setPrevField(); 
    } 
    if (code == KeyEvent.VK_RIGHT) 
    { 
      model.setNextField(); 
    } 
  } 

  protected void increment() 
  { 
    int fieldID = model.getActiveField(); 
    SpinRangeModel range = model.getRange(fieldID); 
    range.setValueIsAdjusting(true); 
    double value = range.getValue() + range.getExtent(); 
    if (value > range.getMaximum()) 
      value = range.getWrap() ? 
        range.getMinimum() : range.getMaximum(); 
    range.setValue(value); 
    model.setRange(fieldID, range); 
    range.setValueIsAdjusting(false); 
  } 

  public void decrement() 
  { 
    int fieldID = model.getActiveField(); 
    SpinRangeModel range = model.getRange(fieldID); 
    range.setValueIsAdjusting(true); 
    double value = range.getValue() - range.getExtent(); 
    if (value < range.getMinimum()) 
      value = range.getWrap() ? 
        range.getMaximum() : range.getMinimum(); 
    range.setValue(value); 
    model.setRange(fieldID, range); 
    range.setValueIsAdjusting(false); 
  } 
} 

Listing 7.
 
public class JSpinnerField extends JPanel 
  implements ChangeListener, MouseListener, 
FocusListener 
{ 
  protected SpinModel model; 
  protected SpinField spinField; 
  protected SpinRenderer renderer; 
  protected Format formatter; 
  protected boolean wrap = true; 
  protected boolean hasFocus = false; 

  public JSpinnerField() {} 

  public JSpinnerField(int value, 
    int extent, int min, int max, boolean wrap) 
  { 
    init(new DefaultSpinModel(value, extent, min, 
max, wrap), 
      new DefaultSpinRenderer(), 
      NumberFormat.getInstance(), wrap); 
    refreshSpinView(); 
  } 

  public JSpinnerField(SpinModel model, 
    SpinRenderer renderer, Format formatter, 
    boolean wrap) 
  { 
    init(model, renderer, formatter, wrap); 
    refreshSpinView(); 
  } 

  protected void init(SpinModel model, 
    SpinRenderer renderer, Format formatter, 
    boolean wrap) 
  { 
    this.model = model; 
    this.renderer = renderer; 
    this.formatter = formatter; 
    this.wrap = wrap; 
    spinField = new SpinField(this); 
    setLayout(new BorderLayout()); 
    add(BorderLayout.CENTER, spinField); 
    setBorder(spinField.getBorder()); 
    spinField.setBorder(null); 
    JSpinner spinner = new JSpinner(model); 
    addKeyListener(spinner); 
    addMouseListener(this); 
    addFocusListener(this); 
    model.addChangeListener(this); 
    add(BorderLayout.EAST, spinner); 
  } 

  public void setLocale(Locale locale) 
  { 
    formatter = NumberFormat.getInstance(locale); 
    updateFieldOrder(); 
  } 

  public void updateFieldOrder() 
  { 
    if (spinField.getValue() == null) return; 
    int[] fieldIDs = model.getFieldIDs(); 
    LocaleUtil.sortFieldOrder(formatter, 
spinField.getValue(), fieldIDs); 
    model.setFieldIDs(fieldIDs); 
  } 

  public SpinRenderer getRenderer() 
  { 
    return renderer; 
  } 

  protected void refreshSpinView() 
  { 
    int fieldID = model.getActiveField(); 
    SpinRangeModel range = model.getRange(fieldID); 
    spinField.setValue(new Double(range.getValue())); 
  } 

  public void stateChanged(ChangeEvent event) 
  { 
    requestFocus(); 
    refreshSpinView(); 
    repaint(); 
  } 

  public void mouseClicked(MouseEvent event) 
  { 
    int fieldID = LocaleUtil.findMouseInField( 
      getGraphics().getFontMetrics(), event.getX(), 
      formatter, spinField.getValue(), 
model.getFieldIDs()); 
    model.setActiveField(fieldID); 
    requestFocus(); 
    refreshSpinView(); 
  } 
  public void mousePressed(MouseEvent event) {} 
  public void mouseReleased(MouseEvent event) {} 
  public void mouseEntered(MouseEvent event) {} 
  public void mouseExited(MouseEvent event) {} 

  public void focusGained(FocusEvent event) 
  { 
    hasFocus = true; 
    repaint(); 
  } 

  public void focusLost(FocusEvent event) 
  { 
    hasFocus = false; 
    repaint(); 
  } 

  public boolean isFocusTraversable() 
  { 
    return true; 
  } 
} 

Listing 8.
 
public class LocaleUtil 
{ 
  public static void sortFieldOrder( 
    Format formatter, Object obj, int[] fieldIDs) 
  { 
    int size = fieldIDs.length; 
    int[] order = new int[size]; 
    for (int i = 0; i < size; i++) 
    { 
      order[i] = getFieldPosition( 
        formatter, obj, fieldIDs[i]).getBeginIndex(); 
    } 
    sort(fieldIDs, order); 
  } 

  public static int findMouseInField(FontMetrics 
metrics, 
int x, 
    Format formatter, Object obj, int[] fieldIDs) 
  { 
    String text = formatter.format(obj); 
    int size = fieldIDs.length; 
    FieldPosition pos; 
    for (int i = 0; i < size; i++) 
    { 
      pos = getFieldPosition(formatter, obj, 
fieldIDs[i]); 
      int left = metrics.stringWidth( 
        text.substring(0, pos.getBeginIndex())); 
      int right = metrics.stringWidth( 
        text.substring(0, pos.getEndIndex())); 
      if (x >= left && x <= right) 
      { 
        return fieldIDs[i]; 
      } 
    } 
    return fieldIDs[0]; 
  } 

  public static FieldPosition getFieldPosition( 
    Format formatter, Object obj, int field) 
  { 
    FieldPosition pos = new FieldPosition(field); 
    StringBuffer buffer = new StringBuffer(); 
    formatter.format(obj, buffer, pos); 
    return pos; 
  } 

  private static void sort(int[] fieldIDs, int[] 
order) 
  { 
    sort(fieldIDs, order, 0, fieldIDs.length - 1); 
    } 

  private static void sort(int[] fieldIDs, int[] 
order, 
int first, int last) 
  { 
    if (first >= last) return; 
    int lo = first, hi = last; 
    int mid = order[(first + last) / 2]; 
    int tmp, temp; 
    do 
    { 
      while (mid > order[lo]) lo++; 
      while (mid < order[hi]) hi--; 
      if (lo <= hi) 
      { 
        tmp = order[lo]; 
        temp = fieldIDs[lo]; 
        order[lo] = order[hi]; 
        fieldIDs[lo] = fieldIDs[hi]; 
        lo++; 
        order[hi] = tmp; 
        fieldIDs[hi] = temp; 
        hi--; 
      } 
    } 
    while (lo <= hi) ; 
    sort(fieldIDs, order, first, hi); 
    sort(fieldIDs, order, lo, last); 
  } 

  public static void main(String[] args) 
  { 
    int[] order = {2, 8, 12, 6, 10, 4}; 
    int[] fieldIDs = {1, 4, 6, 3, 5, 2}; 
    sort(fieldIDs, order); 
    for (int i = 0; i < fieldIDs.length; i++) 
    { 
      System.out.print(fieldIDs[i] + "; "); 
    } 
  } 
} 

Listing 9.
 
public class SpinField extends JPanel 
{ 
  protected CellRendererPane pane; 
  protected JSpinnerField field; 
  protected Object value; 

  public SpinField(JSpinnerField field) 
  { 
    this.field = field; 
    setLayout(new BorderLayout()); 
    add(BorderLayout.CENTER, pane = new 
CellRendererPane()); 
    JComponent renderer = (JComponent) 
field.getRenderer(); 
    setBorder(renderer.getBorder()); 
    renderer.setBorder(null); 
  } 

  public void setValue(Object value) 
  { 
    this.value = value; 
    repaint(); 
  } 

  public Object getValue() 
  { 
    return value; 
  } 

  public void paintComponent(Graphics g) 
  { 
    int w = getSize().width; 
    int h = getSize().height; 
    Component comp = field.getRenderer(). 
      getSpinCellRendererComponent(field, value, 
        field.hasFocus, field.formatter, 
        field.model.getActiveField()); 
    pane.paintComponent(g, comp, this, 0, 0, w, h); 
  } 

  public Dimension getPreferredSize() 
  { 
    return ((JComponent)field.getRenderer()) 
.getPreferredSize(); 
  } 

  public Dimension getMinimumSize() 
  { 
    return ((JComponent)field.getRenderer()) 
.getMinimumSize(); 
  } 
} 

Listing 10.
 
public class DefaultSpinRenderer extends JTextField 
  implements SpinRenderer 
{ 
  private static Color focusColor = new Color(0, 0, 128); 

  public DefaultSpinRenderer() 
  { 
    setOpaque(true); 
    setEditable(false); 
  } 

  public Component getSpinCellRendererComponent( 
    JSpinnerField spin, Object value, boolean hasFocus, 
    Format formatter, int selectedFieldID) 
  { 
    String text = formatter.format(value); 
    setText(text); 
    FieldPosition pos = LocaleUtil.getFieldPosition( 
      formatter, value, selectedFieldID); 
    // Make non-selections expand to full selections 
    if (pos.getBeginIndex() == pos.getEndIndex()) 
    { 
      pos.setBeginIndex(0); 
      pos.setEndIndex(text.length()); 
    } 
    if (hasFocus) 
      select(pos.getBeginIndex(), pos.getEndIndex()); 
    else select(0, 0); 
    return this; 
  } 
} 

Listing 11.
 
public class ColorSpinRenderer extends JTextField 
  implements SpinRenderer 
{ 
  private static Color focusColor = Color.white; 
  private static Border focus = new 
LineBorder(focusColor, 1); 

  public ColorSpinRenderer() 
  { 
    setOpaque(true); 
    setEditable(false); 
  } 

  public Component getSpinCellRendererComponent( 
    JSpinnerField spin, Object value, boolean hasFocus, 
    Format formatter, int selectedFieldID) 
  { 
    if (value instanceof Color) 
    { 
      Color color = (Color)value; 
      setBackground(color); 
      if (hasFocus) setBorder(focus); 
      else setBorder(null); 
    } 
    return this; 
  } 
} 

Listing 12.
 
public class JSpinnerColor extends JSpinnerField 
{ 
  protected Vector list = new Vector(); 

  public JSpinnerColor(Color[] items, int index, 
boolean wrap) 
  { 
    super( 
      new DefaultSpinModel(index, 1, 0, items.length 
- 1, wrap), 
      new ColorSpinRenderer(), null, wrap); 
    for (int i = 0; i < items.length; i++) 
    { 
      list.addElement(items[i]); 
    } 
    refreshSpinView(); 
  } 

  protected void refreshSpinView() 
  { 
    if (list == null) return; 
    int fieldID = model.getActiveField(); 
    SpinRangeModel range = model.getRange(fieldID); 
    spinField.setValue(list.elementAt((int) 
range.getValue())); 
  } 

  public void updateFieldOrder() {} 
} 

Listing 13.
 
public class JSpinnerList extends JSpinnerField 
{ 
  public JSpinnerList(String[] items, int index, 
boolean wrap) 
  { 
    double[] limits = new double[items.length]; 
    for (int i = 0; i < items.length; i++) 
    { 
      limits[i] = i; 
    } 
    init(new DefaultSpinModel(index, 1, 0, 
items.length - 1, wrap), 
      new DefaultSpinRenderer(), 
      new ChoiceFormat(limits, items), wrap); 
    refreshSpinView(); 
  } 

  public void setLocale(Locale locale) {} 
} 

Listing 14.
 
public class JSpinnerPercent extends JSpinnerField 
{ 
  public JSpinnerPercent() 
  { 
    init(new DefaultSpinModel(0, 0.01, 0, 1, true), 
      new DefaultSpinRenderer(), 
      NumberFormat.getPercentInstance(), 
      true); 
    refreshSpinView(); 
  } 

  public void setLocale(Locale locale) 
  { 
    formatter = NumberFormat.getPercentInstance(locale); 
    updateFieldOrder(); 
  } 
} 

Listing 15.
 
public class JSpinnerCurrency extends JSpinnerField 
{ 
  public JSpinnerCurrency() 
  { 
    init(new DefaultSpinModel( 
        0, 1, 0, 10, true, 
        0, 0.01, 0, 1, true), 
      new DefaultSpinRenderer(), 
      NumberFormat.getCurrencyInstance(), 
      true); 
    refreshSpinView(); 
  } 

  public void setLocale(Locale locale) 
  { 
    formatter = NumberFormat.getCurrencyInstance(locale); 
    updateFieldOrder(); 
  } 

  protected void refreshSpinView() 
  { 
    double integer = model.getRange( 
      NumberFormat.INTEGER_FIELD).getValue(); 
    double fraction = model.getRange( 
      NumberFormat.FRACTION_FIELD).getValue(); 
    spinField.setValue(new Double(integer + fraction)); 
  } 
} 

Listing 16.
 
public class TimeSpinModel extends DefaultSpinModel 
{ 
  protected Calendar time = Calendar.getInstance(); 

  public TimeSpinModel() 
  { 
    setRange(DateFormat.HOUR1_FIELD, 
      new DefaultSpinRangeModel()); 
    setRange(DateFormat.MINUTE_FIELD, 
      new DefaultSpinRangeModel()); 
    setRange(DateFormat.AM_PM_FIELD, 
      new DefaultSpinRangeModel()); 
    setActiveField(DateFormat.HOUR1_FIELD); 
    setTime(time); 
  } 

  public void setRange(int fieldID, SpinRangeModel range) 
  { 
    super.setRange(fieldID, range); 
    if (fieldID == DateFormat.HOUR1_FIELD) 
    { 
      time.set(Calendar.HOUR, (int)range.getValue()); 
    } 
    if (fieldID == DateFormat.MINUTE_FIELD) 
    { 
      time.set(Calendar.MINUTE, (int)range.getValue()); 
    } 
    if (fieldID == DateFormat.AM_PM_FIELD) 
    { 
      time.set(Calendar.HOUR_OF_DAY, 
        time.get(Calendar.HOUR) + 12 * (int) 
range.getValue()); 
    } 
  } 

  public SpinRangeModel getRange(int fieldID) 
  { 
    SpinRangeModel range = super.getRange(fieldID); 
    if (fieldID == DateFormat.HOUR1_FIELD) 
    { 
      range.setExtent(1.0); 
      range.setValue(time.get(Calendar.HOUR)); 
      range.setMinimum(time.getActualMinimum 
(Calendar.HOUR)); 
      range.setMaximum(time.getActualMaximum 
(Calendar.HOUR)); 
    } 
    if (fieldID == DateFormat.MINUTE_FIELD) 
    { 
      range.setExtent(1.0); 
      range.setValue(time.get(Calendar.MINUTE)); 
      range.setMinimum(time.getActualMinimum 
(Calendar.MINUTE)); 
      range.setMaximum(time.getActualMaximum 
(Calendar.MINUTE)); 
    } 
    if (fieldID == DateFormat.AM_PM_FIELD) 
    { 
      range.setExtent(1.0); 
      range.setValue(time.get(Calendar.AM_PM)); 
      range.setMinimum(time.getActualMinimum 
(Calendar.AM_PM)); 
      range.setMaximum(time.getActualMaximum 
(Calendar.AM_PM)); 
    } 
    return range; 
  } 

  public void setTime(Calendar time) 
  { 
    this.time = time; 
    getRange(DateFormat.HOUR1_FIELD); 
    getRange(DateFormat.MINUTE_FIELD); 
    getRange(DateFormat.AM_PM_FIELD); 
    fireStateChanged(); 
  } 

  public Calendar getTime() 
  { 
    return time; 
  } 

  public static void main(String[] args) 
  { 
    TimeSpinModel model = new TimeSpinModel(); 
    int activeField = model.getActiveField(); 
    System.out.println(model.getRange(activeField)); 
    model.setNextField(); 
    activeField = model.getActiveField(); 
    System.out.println(model.getRange(activeField)); 
    model.setNextField(); 
    activeField = model.getActiveField(); 
    System.out.println(model.getRange(activeField)); 
    model.setNextField(); 
    activeField = model.getActiveField(); 
    System.out.println(model.getRange(activeField)); 
  } 
} 

Listing 17.
 
public class DateSpinModel extends DefaultSpinModel 
{ 
  protected Calendar date = Calendar.getInstance(); 

  public DateSpinModel() 
  { 
    setRange(DateFormat.MONTH_FIELD, 
      new DefaultSpinRangeModel()); 
    setRange(DateFormat.DATE_FIELD, 
      new DefaultSpinRangeModel()); 
    setRange(DateFormat.YEAR_FIELD, 
      new DefaultSpinRangeModel()); 
    setActiveField(DateFormat.MONTH_FIELD); 
  } 

  public void setRange(int fieldID, SpinRangeModel 
range) 
  { 
    super.setRange(fieldID, range); 
    if (fieldID == DateFormat.DATE_FIELD) 
    { 
      date.set(Calendar.DATE, (int)range.getValue()); 
    } 
    if (fieldID == DateFormat.MONTH_FIELD) 
    { 
      date.set(Calendar.MONTH, (int)range.getValue()); 
    } 
    if (fieldID == DateFormat.YEAR_FIELD) 
    { 
      date.set(Calendar.YEAR, (int)range.getValue()); 
    } 
  } 

  public SpinRangeModel getRange(int fieldID) 
  { 
    SpinRangeModel range = super.getRange(fieldID); 
    if (fieldID == DateFormat.DATE_FIELD) 
    { 
      range.setExtent(1); 
      range.setValue(date.get(Calendar.DAY_OF_MONTH)); 
      range.setMinimum(date.getActualMinimum 
(Calendar.DAY_OF_MONTH)); 
      range.setMaximum(date.getActualMaximum 
(Calendar.DAY_OF_MONTH)); 
    } 
    if (fieldID == DateFormat.MONTH_FIELD) 
    { 
      range.setExtent(1); 
      range.setValue(date.get(Calendar.MONTH)); 
      range.setMinimum(date.getActualMinimum 
(Calendar.MONTH)); 
      range.setMaximum(date.getActualMaximum 
(Calendar.MONTH)); 
    } 
    if (fieldID == DateFormat.YEAR_FIELD) 
    { 
      range.setExtent(1); 
      range.setValue(date.get(Calendar.YEAR)); 
      range.setMinimum(date.getActualMinimum(Calendar.YEAR)); 
      range.setMaximum(date.getActualMaximum(Calendar.YEAR)); 
    } 
    return range; 
  } 

  public void setDate(Calendar date) 
  { 
    this.date = date; 
    getRange(DateFormat.DATE_FIELD); 
    getRange(DateFormat.MONTH_FIELD); 
    getRange(DateFormat.YEAR_FIELD); 
  } 

  public Calendar getDate() 
  { 
    return date; 
  } 
} 

Listing 18.
 
public class JSpinnerTime extends JSpinnerField 
{ 
  public JSpinnerTime() 
  { 
    this(Calendar.getInstance()); 
  } 

  public JSpinnerTime(Calendar time) 
  { 
    super(new TimeSpinModel(), 
      new DefaultSpinRenderer(), 
      DateFormat.getTimeInstance(DateFormat.SHORT), 
      true); 
    getTimeModel().setTime(time); 
    refreshSpinView(); 
  } 

  public void setLocale(Locale locale) 
  { 
    formatter = DateFormat.getTimeInstance(DateFormat.SHORT, 
locale); 
    updateFieldOrder(); 
  } 

  private TimeSpinModel getTimeModel() 
  { 
    return (TimeSpinModel)model; 
  } 

  protected void refreshSpinView() 
  { 
    spinField.setValue(getTimeModel().getTime().getTime()); 
  } 
} 

Listing 19.
 
public class JSpinnerDate extends JSpinnerField 
{ 
  public JSpinnerDate() 
  { 
    this(Calendar.getInstance()); 
  } 

  public JSpinnerDate(Calendar date) 
  { 
    super(new DateSpinModel(), 
      new DefaultSpinRenderer(), 
      DateFormat.getDateInstance(DateFormat.MEDIUM), 
      true); 
    getDateModel().setDate(date); 
    refreshSpinView(); 
  } 

  public void setLocale(Locale locale) 
  { 
    formatter = DateFormat.getDateInstance(DateFormat.MEDIUM, 
locale); 
    updateFieldOrder(); 
  } 

  private DateSpinModel getDateModel() 
  { 
    return (DateSpinModel)model; 
  } 

  protected void refreshSpinView() 
  { 
    spinField.setValue(getDateModel().getDate().getTime()); 
  } 
} 

Listing 20.
 
public class JSpinnerTest extends JPanel 
  implements ActionListener 
{ 
  protected JComboBox localeChoice; 
  protected JSpinnerField date, time, 
    currency, percent, number, skip, list, color; 
  protected String[] localeNames = 
  { 
    "U.S.", "English", "French", "German" 
  }; 
  protected Locale[] localeTypes = 
  { 
    Locale.US, Locale.ENGLISH, Locale.FRENCH, Locale.GERMAN 
  }; 

  public JSpinnerTest() 
  { 
    setLayout(new FieldLayout(4, 4)); 
    add(new JLabel(" Locale: ")); 
    add(localeChoice = new JComboBox(localeNames)); 
    localeChoice.addActionListener(this); 
    add(new JLabel(" JSpinnerField (1-10/1): ")); 
    add(number = new JSpinnerField(1, 1, 1, 10, true)); 
    add(new JLabel(" JSpinnerField (0-8/2): ")); 
    add(skip = new JSpinnerField(0, 2, 0, 8, true)); 
    add(new JLabel(" JSpinnerDate (today): ")); 
    add(date = new JSpinnerDate()); 
    add(new JLabel(" JSpinnerTime (now): ")); 
    add(time = new JSpinnerTime()); 
    add(new JLabel(" JSpinnerPercent: ")); 
    add(percent = new JSpinnerPercent()); 
    add(new JLabel(" JSpinnerCurrency: ")); 
    add(currency = new JSpinnerCurrency()); 
    add(new JLabel(" JSpinnerList (a,b,c,d,e): ")); 
    add(list = new JSpinnerList(new String[] { 
      "alpha", "beta", "gamma", "delta", "epsilon"}, 0, true)); 
    add(new JLabel(" JSpinnerColor (r,g,b): ")); 
    add(color = new JSpinnerColor(new Color[] { 
      Color.red, Color.green, Color.blue}, 0, true)); 
  } 

  public void actionPerformed(ActionEvent event) 
  { 
    Locale locale = localeTypes 
[localeChoice.getSelectedIndex()]; 
    date.setLocale(locale); 
    time.setLocale(locale); 
    percent.setLocale(locale); 
    currency.setLocale(locale); 
    number.setLocale(locale); 
    skip.setLocale(locale); 
    list.setLocale(locale); 
    color.setLocale(locale); 
    repaint(); 
  } 

  public static void main(String[] args) 
  { 
    PLAF.setNativeLookAndFeel(true); 
    JFrame frame = new JFrame("JSpinner* Test"); 
    frame.getContentPane().add(new JSpinnerTest()); 
    frame.setBounds(100, 100, 250, 250); 
    frame.show(); 
  } 
} 
  

Download Assoicated Source Files (Zip format - 55.7 KB)
 

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.
  E-mail: [email protected]

Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.