Suggestion for Spinner control

After digging around in source for the Spinner control and how setFloatRange()/setIntegerRanger() works, I think this coud use some improvement.

With the current code, if I set a range say from 0 to 10000 (with an increment of 1), it would actually create 10000 String objects, one for each possible value. That is quite an overhead for a little component :slight_smile:

I believe a better approach may to be mimic to Swings SpinnerModel (or maybe just callbacks in a similar way in the base Spinner class). This returns the next, previous and current values and they can be of any type (so need for special support for floats, ints, etc). Only downside is it is an additional class. See here.

For people using addStepValue() methods so set strings, those could dealt with using a model implementation that just stores a string list. For people who want numeric ranges (possibly large), efficient FloatSpinnerModel and IntegerSpinnerModel could be used.

On a side note, it would also be pretty useful if the value was editable. I tried setting enabled to true, and that sort of lets you focus but nothing can be typed. Editing values is something I want to be able to do for my project, and If I can avoid having to customise this myself, that would be good.

Apologies if you already have something planned for this.

RR

1 Like

EDIT 1: Finally got the hang of editing
EDIT 2: Fixed bug with editing not firing event.

Also, it appears setStepFloatRange() has a bug anyway. I was trying to use the following :-

[java]spinner.setStepFloatRange(3.5f, 25.7f, 0.1f);[/java]

This however results in the following values as steps :-

3.5, 3.6, 3.6999998, 3.7999997, 3.8999996 … and so on.

A description of the problem can be found here.

<br/>
<br/>
Sooo, this is a modified version of Spinner that …

  • Uses a model for values. Float, Integer and String examples included. This avoids the mentioned object creation.
  • Is editable. The model supports setting current value from string, and a tweak to the key handling fixed this.
  • Doesn't suffer float rounding problem because BigDecimal is used internally.

How to use …

[java]
ModelledSpinner spinner = new ModelledSpinner(…) {
public void onChange(Object newValue) {
// In this case will be a Float
}
};
spinner.setSpinnerModel(new ModelledSpinner.FloatRangeModel(0f, 10.0f, 0.1f, 0.5f));
[/java]

or …

[java]
ModelledSpinner spinner = new ModelledSpinner(…) {
public void onChange(Object newValue) {
// In this case will be a String
}
};
spinner.setSpinnerModel(new ModelledSpinner.StringRangeModel(“val 1”, “val 2”, “val 3”));
[/java]

You can of course create your own model, just implement ModelledSpinner.SpinnerModel<SomeType>.
<br/><br/>
And the control itself …

[java]/*

  • To change this template, choose Tools | Templates
  • and open the template in the editor.
    */
    package tonegod.gui.controls.lists;

import com.jme3.input.KeyInput;
import com.jme3.input.event.KeyInputEvent;
import com.jme3.input.event.MouseButtonEvent;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector4f;
import java.math.BigDecimal;
import java.math.MathContext;
import java.util.ArrayList;
import java.util.Arrays;
import tonegod.gui.controls.buttons.ButtonAdapter;
import tonegod.gui.controls.text.TextField;
import tonegod.gui.core.ElementManager;
import tonegod.gui.core.utils.UIDUtil;

/**
*

  • @author t0neg0d
    */
    public abstract class ModelledSpinner extends TextField {

     /**
      * Use this interface to provide the values to use in the spinner. Implementations
      * must provide next, previous and current value so in general would keep a pointer
      * and a list or range of values.
      */
     public interface SpinnerModel&lt;T&gt; {
         /**
          * Get the next value in the sequence. If the end of the sequence has been
          * reached then <code>null</code> should be returned.
          */
         T getNextValue();
         
         /**
          * Get the previous value in sequence. If the pointer is currently at 
          * the first value in the sequence, the <code>null</code> should be returned.
          * 
          * @return 
          */
         T getPreviousValue();
         
         /**
          * Get the current value.
          * 
          * @return value
          */
         T getCurrentValue();
    
         /**
          * Wind the spinner to either the start or the end of sequence. To go forward,
          * supply <code>true</code> as the argument.
          * 
          * @param forward wind forward to end (or back to start if <code>false</code>)
          */
         void wind(boolean forward);
         
         /**
          * Set the current value from a string. This is to support editing.
          * 
          * @param stringValue
          */
         void setValueFromString(String stringValue);
     }
     
     /**
      * Spinner model that takes a list of strings (similar to {@link Spinner}.
      */
     public static class StringRangeModel extends ArrayList&lt;String&gt; implements SpinnerModel&lt;String&gt; {
         
         private int pointer = 0;
         
         public StringRangeModel() {
         }
         
         /**
          * Create spinner from list of strings.
          * 
          * @param values varargs of values
          */
         public StringRangeModel(String... values) {
             addAll(Arrays.asList(values));
         }
         
         public void setInitialValue(String value) {
             pointer = indexOf(value);
         }
    
         @Override
         public String getNextValue() {
             if(pointer + 1 &lt; size()) {
                 pointer++;
                 return get(pointer);
             }
             return null;
         }
    
         @Override
         public String getPreviousValue() {
             if(pointer &gt; 0) {
                 pointer--;
                 return get(pointer);
             }
             return null;
         }
    
         @Override
         public String getCurrentValue() {
             if(pointer &lt; size()) {
                 return get(pointer);
             }
             return null;
         }
    
         @Override
         public void wind(boolean forward) {
             if(forward &amp;&amp; size() &gt; 0) {
                 pointer = size() - 1;
             }
             else {
                 pointer = 0;
             }
         }
    
         @Override
         public void setValueFromString(String stringValue) {
             pointer = Math.max(0, indexOf(stringValue));
         }
     }
     
     /**
      * Spinner model that takes a range of floats. This uses {@link BigDecimal}
      * internally to avoid float rounding problems with small step sizes (e.g. 0.1)
      */
     public static class FloatRangeModel implements SpinnerModel&lt;Float&gt; {
    
         private BigDecimal min = new BigDecimal(0f);
         private BigDecimal max = new BigDecimal(100f);
         private BigDecimal incr = new BigDecimal(1f);            
         private BigDecimal value = new BigDecimal("0.0");
         private boolean started;
         
         /**
          * Default contructor, min = 0, max = 100, incr = 1, value = 0
          */
         public FloatRangeModel() {
         }
    
         public FloatRangeModel(float min, float max, float incr, float value) {
             this.min = new BigDecimal(String.valueOf(min));
             this.max = new BigDecimal(String.valueOf(max));
             this.incr =  new BigDecimal(String.valueOf(incr));
             this.value = new BigDecimal(String.valueOf(value));
         }
    
         public float getMin() {
             return min.floatValue();
         }
    
         public void setRange(float min, float max, float incr, float value) {
             this.min = new BigDecimal(String.valueOf(min));
             this.max = new BigDecimal(String.valueOf(max));
             this.incr =  new BigDecimal(String.valueOf(incr));
             this.value = new BigDecimal(String.valueOf(value));
             started = false;
         }
    
         @Override
         public Float getNextValue() {
             if(value.floatValue()  &gt;= max.floatValue()) {
                 return null;
             } 
             value = value.add(incr);
             return value.floatValue();
         }
    
         @Override
         public Float getPreviousValue() {
             if(value.floatValue() &lt;= min.floatValue()) {
                 return null;
             }
             value = value.subtract(incr);
             return value.round(MathContext.UNLIMITED).floatValue();
         }
    
         @Override
         public Float getCurrentValue() {
             return value.floatValue();
         }
    
         @Override
         public void wind(boolean forward) {
             if(forward) {
                 value = max;
             }
             else {
                 value = min;
             }
         }
    
         @Override
         public void setValueFromString(String stringValue) {
             value = new BigDecimal(stringValue);
         }
     }
    
     /**
      * Spinner model that takes a range of integers. This doesn't need to use
      * BigDecimal :)
      */
     public static class IntegerRangeModel implements SpinnerModel&lt;Integer&gt; {
    
         private int min = 0;
         private int max = 100;
         private int incr = 1;            
         private int value = 0;
         private boolean started;
    
         public float getMin() {
             return min;
         }
    
         public void setRange(int min, int max, int incr, int value) {
             this.min = min;
             this.max = max;
             this.incr = incr;
             this.value = value;
             started = false;
         }
    
         @Override
         public Integer getNextValue() {
             if(value &gt;= max) {
                 return null;
             } 
             value += incr;
             return value;
         }
    
         @Override
         public Integer getPreviousValue() {
             if(value &lt;= min) {
                 return null;
             }
             value -= incr;
             return value;
         }
    
         @Override
         public Integer getCurrentValue() {
             return value;
         }
    
         @Override
         public void wind(boolean forward) {
             if(forward) {
                 value = max;
             }
             else {
                 value = min;
             }
         }
    
         @Override
         public void setValueFromString(String stringValue) {
             value = Integer.parseInt(stringValue);
         }
     }
    

    public static enum Orientation {
    VERTICAL,
    HORIZONTAL
    }

     private SpinnerModel model = new IntegerRangeModel();
    

    private boolean cycle = false;
    private Orientation orientation;

    private float btnWidth;
    private float btnIncX, btnIncY, btnIncH, btnIncIconSize;
    private float btnDecX, btnDecY, btnDecH, btnDecIconSize;
    private String btnIncIcon, btnDecIcon;

    private ButtonAdapter btnInc, btnDec;

    /**

    • Creates a new instance of the Spinner control
    • @param screen The screen control the Element is to be added to
    • @param position A Vector2f containing the x/y position of the Element
    • @param orientation Spinner.Orientation used to establish Horizontal/Vertical layout during control configuration
    • @param cycle Boolean used to determine if the spinner should cycle back through values
      */
      public ModelledSpinner(ElementManager screen, Vector2f position, ModelledSpinner.Orientation orientation, boolean cycle) {
      this(screen, UIDUtil.getUID(), position,
      screen.getStyle(“TextField”).getVector2f(“defaultSize”),
      screen.getStyle(“TextField”).getVector4f(“resizeBorders”),
      screen.getStyle(“TextField”).getString(“defaultImg”),
      orientation,
      cycle
      );
      }

    /**

    • Creates a new instance of the Spinner control
    • @param screen The screen control the Element is to be added to
    • @param position A Vector2f containing the x/y position of the Element
    • @param dimensions A Vector2f containing the width/height dimensions of the Element
    • @param orientation Spinner.Orientation used to establish Horizontal/Vertical layout during control configuration
    • @param cycle Boolean used to determine if the spinner should cycle back through values
      */
      public ModelledSpinner(ElementManager screen, Vector2f position, Vector2f dimensions, ModelledSpinner.Orientation orientation, boolean cycle) {
      this(screen, UIDUtil.getUID(), position, dimensions,
      screen.getStyle(“TextField”).getVector4f(“resizeBorders”),
      screen.getStyle(“TextField”).getString(“defaultImg”),
      orientation,
      cycle
      );
      }

    /**

    • Creates a new instance of the Spinner control
    • @param screen The screen control the Element is to be added to
    • @param position A Vector2f containing the x/y position of the Element
    • @param dimensions A Vector2f containing the width/height dimensions of the Element
    • @param resizeBorders A Vector4f containg the border information used when resizing the default image (x = N, y = W, z = E, w = S)
    • @param defaultImg The default image to use for the Spinner
    • @param orientation Spinner.Orientation used to establish Horizontal/Vertical layout during control configuration
    • @param cycle Boolean used to determine if the spinner should cycle back through values
      */
      public ModelledSpinner(ElementManager screen, Vector2f position, Vector2f dimensions, Vector4f resizeBorders, String defaultImg, ModelledSpinner.Orientation orientation, boolean cycle) {
      this(screen, UIDUtil.getUID(), position, dimensions, resizeBorders, defaultImg, orientation, cycle);
      }

    /**

    • Creates a new instance of the Spinner control
    • @param screen The screen control the Element is to be added to
    • @param UID A unique String identifier for the Element
    • @param position A Vector2f containing the x/y position of the Element
    • @param orientation Spinner.Orientation used to establish Horizontal/Vertical layout during control configuration
    • @param cycle Boolean used to determine if the spinner should cycle back through values
      */
      public ModelledSpinner(ElementManager screen, String UID, Vector2f position, ModelledSpinner.Orientation orientation, boolean cycle) {
      this(screen, UID, position,
      screen.getStyle(“TextField”).getVector2f(“defaultSize”),
      screen.getStyle(“TextField”).getVector4f(“resizeBorders”),
      screen.getStyle(“TextField”).getString(“defaultImg”),
      orientation,
      cycle
      );
      }

    /**

    • Creates a new instance of the Spinner control
    • @param screen The screen control the Element is to be added to
    • @param UID A unique String identifier for the Element
    • @param position A Vector2f containing the x/y position of the Element
    • @param dimensions A Vector2f containing the width/height dimensions of the Element
    • @param orientation Spinner.Orientation used to establish Horizontal/Vertical layout during control configuration
    • @param cycle Boolean used to determine if the spinner should cycle back through values
      */
      public ModelledSpinner(ElementManager screen, String UID, Vector2f position, Vector2f dimensions, ModelledSpinner.Orientation orientation, boolean cycle) {
      this(screen, UID, position, dimensions,
      screen.getStyle(“TextField”).getVector4f(“resizeBorders”),
      screen.getStyle(“TextField”).getString(“defaultImg”),
      orientation,
      cycle
      );
      }

    /**

    • Creates a new instance of the Spinner control

    • @param screen The screen control the Element is to be added to

    • @param UID A unique String identifier for the Element

    • @param position A Vector2f containing the x/y position of the Element

    • @param dimensions A Vector2f containing the width/height dimensions of the Element

    • @param resizeBorders A Vector4f containg the border information used when resizing the default image (x = N, y = W, z = E, w = S)

    • @param defaultImg The default image to use for the Spinner

    • @param orientation Spinner.Orientation used to establish Horizontal/Vertical layout during control configuration

    • @param cycle Boolean used to determine if the spinner should cycle back through values
      */
      public ModelledSpinner(ElementManager screen, String UID, Vector2f position, Vector2f dimensions, Vector4f resizeBorders, String defaultImg, ModelledSpinner.Orientation orientation, boolean cycle) {
      super(screen, UID, position, dimensions, resizeBorders, defaultImg
      );

      this.orientation = orientation;
      this.cycle = cycle;
      setScaleEW(false);
      setScaleNS(false);
      setDockN(true);
      setDockW(true);

      btnWidth = getHeight();

      if (orientation == Orientation.HORIZONTAL) {
      setWidth(getWidth()-(btnWidth*2));
      setX(getX()+btnWidth);
      btnIncX = getWidth();
      btnIncY = 0;
      btnIncH = getHeight();
      btnIncIcon = screen.getStyle(“Common”).getString(“arrowRight”);
      btnDecX = -getHeight();
      btnDecY = 0;
      btnDecH = getHeight();
      btnDecIcon = screen.getStyle(“Common”).getString(“arrowLeft”);
      } else {
      setWidth(getWidth()-btnWidth);
      btnIncX = getWidth();
      btnIncY = 0;
      btnIncH = getHeight()/2;
      btnIncIcon = screen.getStyle(“Common”).getString(“arrowUp”);
      btnDecX = getWidth();
      btnDecY = getHeight()/2;
      btnDecH = getHeight()/2;
      btnDecIcon = screen.getStyle(“Common”).getString(“arrowDown”);
      }
      btnIncIconSize = getHeight()/2;
      btnDecIconSize = getHeight()/2;

      btnInc = new ButtonAdapter(
      screen,
      UID + “:btnInc”,
      new Vector2f(btnIncX, btnIncY),
      new Vector2f(getHeight(), btnIncH)
      ) {
      @Override
      public void onButtonMouseLeftDown(MouseButtonEvent evt, boolean toggled) {
      screen.setTabFocusElement((ModelledSpinner)getElementParent());
      ((ModelledSpinner)getElementParent()).incStep();
      }
      @Override
      public void onButtonStillPressedInterval() {
      ((ModelledSpinner)getElementParent()).incStep();
      }
      };
      btnInc.setButtonIcon(btnIncIconSize, btnIncIconSize, btnIncIcon);
      btnInc.setDockS(true);
      btnInc.setDockW(true);

      addChild(btnInc);

      btnDec = new ButtonAdapter(
      screen,
      UID + “:btnDec”,
      new Vector2f(btnDecX, btnDecY),
      new Vector2f(getHeight(), btnIncH)
      ) {
      @Override
      public void onButtonMouseLeftDown(MouseButtonEvent evt, boolean toggled) {
      screen.setTabFocusElement((ModelledSpinner)getElementParent());
      ((ModelledSpinner)getElementParent()).decStep();
      }
      @Override
      public void onButtonStillPressedInterval() {
      ((ModelledSpinner)getElementParent()).decStep();
      }
      };
      btnDec.setButtonIcon(btnDecIconSize, btnDecIconSize, btnDecIcon);
      btnDec.setDockS(true);
      btnDec.setDockW(true);

      addChild(btnDec);

    }

     /**
      * Set the spinner model to use. Setting this will update the current value.
      * 
      * @param model model
      */
     public void setSpinnerModel(SpinnerModel&lt;?&gt; model) {
         this.model = model;
         displaySelectedStep();
     }
     
     /**
      * Get the spinner model in use.
      * 
      * @return model
      */
     public SpinnerModel&lt;?&gt; getSpinnerModel() {
         return model;
     }
    

    /**

    • Sets the interval speed for the spinner

    • @param callsPerSecond float
      */
      public void setInterval(float callsPerSecond) {
      btnInc.setInterval(callsPerSecond);
      btnDec.setInterval(callsPerSecond);
      }

      @Override
      public void controlTextFieldResetTabFocusHook() {
      try {
      if(!String.valueOf(model.getCurrentValue()).equals(getText())) {
      model.setValueFromString(getText());
      onChange(model.getCurrentValue());
      }
      }
      catch(NumberFormatException nfe) {
      // Don’t care
      }
      }

    private void incStep() {
    Object newValue = model.getNextValue();
    if(newValue == null) {
    if (cycle) {
    model.wind(false);
    newValue = model.getCurrentValue();
    }
    }
    displaySelectedStep();
    onChange(newValue);
    }

    private void decStep() {
    Object newValue = model.getPreviousValue();
    if (newValue == null) {
    if (cycle) {
    model.wind(true);
    }
    }
    displaySelectedStep();
    onChange(newValue);
    }

    private void displaySelectedStep() {
    this.setText(String.valueOf(model.getCurrentValue()));
    }

    @Override
    public void controlKeyPressHook(KeyInputEvent evt, String text) {
    if (evt.getKeyCode() == KeyInput.KEY_LEFT || evt.getKeyCode() == KeyInput.KEY_DOWN) {
    decStep();
    } else if (evt.getKeyCode() == KeyInput.KEY_RIGHT || evt.getKeyCode() == KeyInput.KEY_UP) {
    incStep();
    } else {
    if(!getIsEnabled()) {
    // Only do this if not editable
    displaySelectedStep();
    }
    }
    }

    /**

    • The abstract event method that is called when the value changes
    • @param value The new value from the model
      */
      public abstract void onChange(Object value);
      }
      [/java]
1 Like
@rockfire said: EDIT 1: Finally got the hang of editing EDIT 2: Fixed bug with editing not firing event.

Also, it appears setStepFloatRange() has a bug anyway. I was trying to use the following :-

[java]spinner.setStepFloatRange(3.5f, 25.7f, 0.1f);[/java]

This however results in the following values as steps :-

3.5, 3.6, 3.6999998, 3.7999997, 3.8999996 … and so on.

A description of the problem can be found here.

<br/>
<br/>
Sooo, this is a modified version of Spinner that …

  • Uses a model for values. Float, Integer and String examples included. This avoids the mentioned object creation.
  • Is editable. The model supports setting current value from string, and a tweak to the key handling fixed this.
  • Doesn't suffer float rounding problem because BigDecimal is used internally.

How to use …

[java]
ModelledSpinner spinner = new ModelledSpinner(…) {
public void onChange(Object newValue) {
// In this case will be a Float
}
};
spinner.setSpinnerModel(new ModelledSpinner.FloatRangeModel(0f, 10.0f, 0.1f, 0.5f));
[/java]

or …

[java]
ModelledSpinner spinner = new ModelledSpinner(…) {
public void onChange(Object newValue) {
// In this case will be a String
}
};
spinner.setSpinnerModel(new ModelledSpinner.StringRangeModel(“val 1”, “val 2”, “val 3”));
[/java]

You can of course create your own model, just implement ModelledSpinner.SpinnerModel<SomeType>.
<br/><br/>
And the control itself …

[java]/*

  • To change this template, choose Tools | Templates
  • and open the template in the editor.
    */
    package tonegod.gui.controls.lists;

import com.jme3.input.KeyInput;
import com.jme3.input.event.KeyInputEvent;
import com.jme3.input.event.MouseButtonEvent;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector4f;
import java.math.BigDecimal;
import java.math.MathContext;
import java.util.ArrayList;
import java.util.Arrays;
import tonegod.gui.controls.buttons.ButtonAdapter;
import tonegod.gui.controls.text.TextField;
import tonegod.gui.core.ElementManager;
import tonegod.gui.core.utils.UIDUtil;

/**
*

  • @author t0neg0d
    */
    public abstract class ModelledSpinner extends TextField {

     /**
      * Use this interface to provide the values to use in the spinner. Implementations
      * must provide next, previous and current value so in general would keep a pointer
      * and a list or range of values.
      */
     public interface SpinnerModel&lt;T&gt; {
         /**
          * Get the next value in the sequence. If the end of the sequence has been
          * reached then <code>null</code> should be returned.
          */
         T getNextValue();
         
         /**
          * Get the previous value in sequence. If the pointer is currently at 
          * the first value in the sequence, the <code>null</code> should be returned.
          * 
          * @return 
          */
         T getPreviousValue();
         
         /**
          * Get the current value.
          * 
          * @return value
          */
         T getCurrentValue();
    
         /**
          * Wind the spinner to either the start or the end of sequence. To go forward,
          * supply <code>true</code> as the argument.
          * 
          * @param forward wind forward to end (or back to start if <code>false</code>)
          */
         void wind(boolean forward);
         
         /**
          * Set the current value from a string. This is to support editing.
          * 
          * @param stringValue
          */
         void setValueFromString(String stringValue);
     }
     
     /**
      * Spinner model that takes a list of strings (similar to {@link Spinner}.
      */
     public static class StringRangeModel extends ArrayList&lt;String&gt; implements SpinnerModel&lt;String&gt; {
         
         private int pointer = 0;
         
         public StringRangeModel() {
         }
         
         /**
          * Create spinner from list of strings.
          * 
          * @param values varargs of values
          */
         public StringRangeModel(String... values) {
             addAll(Arrays.asList(values));
         }
         
         public void setInitialValue(String value) {
             pointer = indexOf(value);
         }
    
         @Override
         public String getNextValue() {
             if(pointer + 1 &lt; size()) {
                 pointer++;
                 return get(pointer);
             }
             return null;
         }
    
         @Override
         public String getPreviousValue() {
             if(pointer &gt; 0) {
                 pointer--;
                 return get(pointer);
             }
             return null;
         }
    
         @Override
         public String getCurrentValue() {
             if(pointer &lt; size()) {
                 return get(pointer);
             }
             return null;
         }
    
         @Override
         public void wind(boolean forward) {
             if(forward &amp;&amp; size() &gt; 0) {
                 pointer = size() - 1;
             }
             else {
                 pointer = 0;
             }
         }
    
         @Override
         public void setValueFromString(String stringValue) {
             pointer = Math.max(0, indexOf(stringValue));
         }
     }
     
     /**
      * Spinner model that takes a range of floats. This uses {@link BigDecimal}
      * internally to avoid float rounding problems with small step sizes (e.g. 0.1)
      */
     public static class FloatRangeModel implements SpinnerModel&lt;Float&gt; {
    
         private BigDecimal min = new BigDecimal(0f);
         private BigDecimal max = new BigDecimal(100f);
         private BigDecimal incr = new BigDecimal(1f);            
         private BigDecimal value = new BigDecimal("0.0");
         private boolean started;
         
         /**
          * Default contructor, min = 0, max = 100, incr = 1, value = 0
          */
         public FloatRangeModel() {
         }
    
         public FloatRangeModel(float min, float max, float incr, float value) {
             this.min = new BigDecimal(String.valueOf(min));
             this.max = new BigDecimal(String.valueOf(max));
             this.incr =  new BigDecimal(String.valueOf(incr));
             this.value = new BigDecimal(String.valueOf(value));
         }
    
         public float getMin() {
             return min.floatValue();
         }
    
         public void setRange(float min, float max, float incr, float value) {
             this.min = new BigDecimal(String.valueOf(min));
             this.max = new BigDecimal(String.valueOf(max));
             this.incr =  new BigDecimal(String.valueOf(incr));
             this.value = new BigDecimal(String.valueOf(value));
             started = false;
         }
    
         @Override
         public Float getNextValue() {
             if(value.floatValue()  &gt;= max.floatValue()) {
                 return null;
             } 
             value = value.add(incr);
             return value.floatValue();
         }
    
         @Override
         public Float getPreviousValue() {
             if(value.floatValue() &lt;= min.floatValue()) {
                 return null;
             }
             value = value.subtract(incr);
             return value.round(MathContext.UNLIMITED).floatValue();
         }
    
         @Override
         public Float getCurrentValue() {
             return value.floatValue();
         }
    
         @Override
         public void wind(boolean forward) {
             if(forward) {
                 value = max;
             }
             else {
                 value = min;
             }
         }
    
         @Override
         public void setValueFromString(String stringValue) {
             value = new BigDecimal(stringValue);
         }
     }
    
     /**
      * Spinner model that takes a range of integers. This doesn't need to use
      * BigDecimal :)
      */
     public static class IntegerRangeModel implements SpinnerModel&lt;Integer&gt; {
    
         private int min = 0;
         private int max = 100;
         private int incr = 1;            
         private int value = 0;
         private boolean started;
    
         public float getMin() {
             return min;
         }
    
         public void setRange(int min, int max, int incr, int value) {
             this.min = min;
             this.max = max;
             this.incr = incr;
             this.value = value;
             started = false;
         }
    
         @Override
         public Integer getNextValue() {
             if(value &gt;= max) {
                 return null;
             } 
             value += incr;
             return value;
         }
    
         @Override
         public Integer getPreviousValue() {
             if(value &lt;= min) {
                 return null;
             }
             value -= incr;
             return value;
         }
    
         @Override
         public Integer getCurrentValue() {
             return value;
         }
    
         @Override
         public void wind(boolean forward) {
             if(forward) {
                 value = max;
             }
             else {
                 value = min;
             }
         }
    
         @Override
         public void setValueFromString(String stringValue) {
             value = Integer.parseInt(stringValue);
         }
     }
    

    public static enum Orientation {
    VERTICAL,
    HORIZONTAL
    }

     private SpinnerModel model = new IntegerRangeModel();
    

    private boolean cycle = false;
    private Orientation orientation;

    private float btnWidth;
    private float btnIncX, btnIncY, btnIncH, btnIncIconSize;
    private float btnDecX, btnDecY, btnDecH, btnDecIconSize;
    private String btnIncIcon, btnDecIcon;

    private ButtonAdapter btnInc, btnDec;

    /**

    • Creates a new instance of the Spinner control
    • @param screen The screen control the Element is to be added to
    • @param position A Vector2f containing the x/y position of the Element
    • @param orientation Spinner.Orientation used to establish Horizontal/Vertical layout during control configuration
    • @param cycle Boolean used to determine if the spinner should cycle back through values
      */
      public ModelledSpinner(ElementManager screen, Vector2f position, ModelledSpinner.Orientation orientation, boolean cycle) {
      this(screen, UIDUtil.getUID(), position,
      screen.getStyle(“TextField”).getVector2f(“defaultSize”),
      screen.getStyle(“TextField”).getVector4f(“resizeBorders”),
      screen.getStyle(“TextField”).getString(“defaultImg”),
      orientation,
      cycle
      );
      }

    /**

    • Creates a new instance of the Spinner control
    • @param screen The screen control the Element is to be added to
    • @param position A Vector2f containing the x/y position of the Element
    • @param dimensions A Vector2f containing the width/height dimensions of the Element
    • @param orientation Spinner.Orientation used to establish Horizontal/Vertical layout during control configuration
    • @param cycle Boolean used to determine if the spinner should cycle back through values
      */
      public ModelledSpinner(ElementManager screen, Vector2f position, Vector2f dimensions, ModelledSpinner.Orientation orientation, boolean cycle) {
      this(screen, UIDUtil.getUID(), position, dimensions,
      screen.getStyle(“TextField”).getVector4f(“resizeBorders”),
      screen.getStyle(“TextField”).getString(“defaultImg”),
      orientation,
      cycle
      );
      }

    /**

    • Creates a new instance of the Spinner control
    • @param screen The screen control the Element is to be added to
    • @param position A Vector2f containing the x/y position of the Element
    • @param dimensions A Vector2f containing the width/height dimensions of the Element
    • @param resizeBorders A Vector4f containg the border information used when resizing the default image (x = N, y = W, z = E, w = S)
    • @param defaultImg The default image to use for the Spinner
    • @param orientation Spinner.Orientation used to establish Horizontal/Vertical layout during control configuration
    • @param cycle Boolean used to determine if the spinner should cycle back through values
      */
      public ModelledSpinner(ElementManager screen, Vector2f position, Vector2f dimensions, Vector4f resizeBorders, String defaultImg, ModelledSpinner.Orientation orientation, boolean cycle) {
      this(screen, UIDUtil.getUID(), position, dimensions, resizeBorders, defaultImg, orientation, cycle);
      }

    /**

    • Creates a new instance of the Spinner control
    • @param screen The screen control the Element is to be added to
    • @param UID A unique String identifier for the Element
    • @param position A Vector2f containing the x/y position of the Element
    • @param orientation Spinner.Orientation used to establish Horizontal/Vertical layout during control configuration
    • @param cycle Boolean used to determine if the spinner should cycle back through values
      */
      public ModelledSpinner(ElementManager screen, String UID, Vector2f position, ModelledSpinner.Orientation orientation, boolean cycle) {
      this(screen, UID, position,
      screen.getStyle(“TextField”).getVector2f(“defaultSize”),
      screen.getStyle(“TextField”).getVector4f(“resizeBorders”),
      screen.getStyle(“TextField”).getString(“defaultImg”),
      orientation,
      cycle
      );
      }

    /**

    • Creates a new instance of the Spinner control
    • @param screen The screen control the Element is to be added to
    • @param UID A unique String identifier for the Element
    • @param position A Vector2f containing the x/y position of the Element
    • @param dimensions A Vector2f containing the width/height dimensions of the Element
    • @param orientation Spinner.Orientation used to establish Horizontal/Vertical layout during control configuration
    • @param cycle Boolean used to determine if the spinner should cycle back through values
      */
      public ModelledSpinner(ElementManager screen, String UID, Vector2f position, Vector2f dimensions, ModelledSpinner.Orientation orientation, boolean cycle) {
      this(screen, UID, position, dimensions,
      screen.getStyle(“TextField”).getVector4f(“resizeBorders”),
      screen.getStyle(“TextField”).getString(“defaultImg”),
      orientation,
      cycle
      );
      }

    /**

    • Creates a new instance of the Spinner control

    • @param screen The screen control the Element is to be added to

    • @param UID A unique String identifier for the Element

    • @param position A Vector2f containing the x/y position of the Element

    • @param dimensions A Vector2f containing the width/height dimensions of the Element

    • @param resizeBorders A Vector4f containg the border information used when resizing the default image (x = N, y = W, z = E, w = S)

    • @param defaultImg The default image to use for the Spinner

    • @param orientation Spinner.Orientation used to establish Horizontal/Vertical layout during control configuration

    • @param cycle Boolean used to determine if the spinner should cycle back through values
      */
      public ModelledSpinner(ElementManager screen, String UID, Vector2f position, Vector2f dimensions, Vector4f resizeBorders, String defaultImg, ModelledSpinner.Orientation orientation, boolean cycle) {
      super(screen, UID, position, dimensions, resizeBorders, defaultImg
      );

      this.orientation = orientation;
      this.cycle = cycle;
      setScaleEW(false);
      setScaleNS(false);
      setDockN(true);
      setDockW(true);

      btnWidth = getHeight();

      if (orientation == Orientation.HORIZONTAL) {
      setWidth(getWidth()-(btnWidth*2));
      setX(getX()+btnWidth);
      btnIncX = getWidth();
      btnIncY = 0;
      btnIncH = getHeight();
      btnIncIcon = screen.getStyle(“Common”).getString(“arrowRight”);
      btnDecX = -getHeight();
      btnDecY = 0;
      btnDecH = getHeight();
      btnDecIcon = screen.getStyle(“Common”).getString(“arrowLeft”);
      } else {
      setWidth(getWidth()-btnWidth);
      btnIncX = getWidth();
      btnIncY = 0;
      btnIncH = getHeight()/2;
      btnIncIcon = screen.getStyle(“Common”).getString(“arrowUp”);
      btnDecX = getWidth();
      btnDecY = getHeight()/2;
      btnDecH = getHeight()/2;
      btnDecIcon = screen.getStyle(“Common”).getString(“arrowDown”);
      }
      btnIncIconSize = getHeight()/2;
      btnDecIconSize = getHeight()/2;

      btnInc = new ButtonAdapter(
      screen,
      UID + “:btnInc”,
      new Vector2f(btnIncX, btnIncY),
      new Vector2f(getHeight(), btnIncH)
      ) {
      @Override
      public void onButtonMouseLeftDown(MouseButtonEvent evt, boolean toggled) {
      screen.setTabFocusElement((ModelledSpinner)getElementParent());
      ((ModelledSpinner)getElementParent()).incStep();
      }
      @Override
      public void onButtonStillPressedInterval() {
      ((ModelledSpinner)getElementParent()).incStep();
      }
      };
      btnInc.setButtonIcon(btnIncIconSize, btnIncIconSize, btnIncIcon);
      btnInc.setDockS(true);
      btnInc.setDockW(true);

      addChild(btnInc);

      btnDec = new ButtonAdapter(
      screen,
      UID + “:btnDec”,
      new Vector2f(btnDecX, btnDecY),
      new Vector2f(getHeight(), btnIncH)
      ) {
      @Override
      public void onButtonMouseLeftDown(MouseButtonEvent evt, boolean toggled) {
      screen.setTabFocusElement((ModelledSpinner)getElementParent());
      ((ModelledSpinner)getElementParent()).decStep();
      }
      @Override
      public void onButtonStillPressedInterval() {
      ((ModelledSpinner)getElementParent()).decStep();
      }
      };
      btnDec.setButtonIcon(btnDecIconSize, btnDecIconSize, btnDecIcon);
      btnDec.setDockS(true);
      btnDec.setDockW(true);

      addChild(btnDec);

    }

     /**
      * Set the spinner model to use. Setting this will update the current value.
      * 
      * @param model model
      */
     public void setSpinnerModel(SpinnerModel&lt;?&gt; model) {
         this.model = model;
         displaySelectedStep();
     }
     
     /**
      * Get the spinner model in use.
      * 
      * @return model
      */
     public SpinnerModel&lt;?&gt; getSpinnerModel() {
         return model;
     }
    

    /**

    • Sets the interval speed for the spinner

    • @param callsPerSecond float
      */
      public void setInterval(float callsPerSecond) {
      btnInc.setInterval(callsPerSecond);
      btnDec.setInterval(callsPerSecond);
      }

      @Override
      public void controlTextFieldResetTabFocusHook() {
      try {
      if(!String.valueOf(model.getCurrentValue()).equals(getText())) {
      model.setValueFromString(getText());
      onChange(model.getCurrentValue());
      }
      }
      catch(NumberFormatException nfe) {
      // Don’t care
      }
      }

    private void incStep() {
    Object newValue = model.getNextValue();
    if(newValue == null) {
    if (cycle) {
    model.wind(false);
    newValue = model.getCurrentValue();
    }
    }
    displaySelectedStep();
    onChange(newValue);
    }

    private void decStep() {
    Object newValue = model.getPreviousValue();
    if (newValue == null) {
    if (cycle) {
    model.wind(true);
    }
    }
    displaySelectedStep();
    onChange(newValue);
    }

    private void displaySelectedStep() {
    this.setText(String.valueOf(model.getCurrentValue()));
    }

    @Override
    public void controlKeyPressHook(KeyInputEvent evt, String text) {
    if (evt.getKeyCode() == KeyInput.KEY_LEFT || evt.getKeyCode() == KeyInput.KEY_DOWN) {
    decStep();
    } else if (evt.getKeyCode() == KeyInput.KEY_RIGHT || evt.getKeyCode() == KeyInput.KEY_UP) {
    incStep();
    } else {
    if(!getIsEnabled()) {
    // Only do this if not editable
    displaySelectedStep();
    }
    }
    }

    /**

    • The abstract event method that is called when the value changes
    • @param value The new value from the model
      */
      public abstract void onChange(Object value);
      }
      [/java]

Looks awesome =) Can’t thank you enough!

Gimme a day or so to get this in there.