blob: 79ea3f236c93bf0ce166a37f162f2ebeae648604 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2004, 2014 1&1 Internet AG, Germany, http://www.1und1.de,
* EclipseSource and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* 1&1 Internet AG and others - original API and implementation
* EclipseSource - adaptation for the Eclipse Remote Application Platform
******************************************************************************/
/**
* A *spinner* is a control that allows you to adjust a numerical value,
* typically within an allowed range. An obvious example would be to specify the
* month of a year as a number in the range 1 - 12.
*
* To do so, a spinner encompasses a field to display the current value (a
* textfield) and controls such as up and down buttons to change that value. The
* current value can also be changed by editing the display field directly, or
* using mouse wheel and cursor keys.
*
* To implement the range of a spinner's value, a {@link rwt.util.Range
* Range} object is deployed as the {@link #manager} object. Here you can define the
* boundaries of the range (*min* and *max* properties), the *default* value,
* the *precision* and whether the range should *wrap* when stepping beyond a
* border (see the Range documentation for more information). An optional {@link
* #numberFormat} property allows you to control the format of how a value can
* be entered and will be displayed.
*
* A brief, but non-trivial example:
*
* <pre>
* var s = new rwt.widgets.base.Spinner;
* s.set({
* max: 3000,
* min: -3000
* });
* var nf = new qx.util.format.NumberFormat();
* nf.setMaximumFractionDigits(2);
* s.setNumberFormat(nf);
* s.getManager().setPrecision(2);
* </pre>
*
* A spinner instance without any further properties specified in the
* constructor or a subsequent *set* command will appear with default
* values and behaviour.
*
* @appearance spinner
*
* @appearance spinner-field {rwt.widgets.base.BasicText}
*
* @appearance spinner-button-up {rwt.widgets.base.Image}
* @state pressed {spinner-button-up}
*
* @appearance spinner-button-down {rwt.widgets.base.Image}
* @state pressed {spinner-button-down}
*/
rwt.qx.Class.define("rwt.widgets.base.Spinner",
{
extend : rwt.widgets.base.HorizontalBoxLayout,
/*
*****************************************************************************
CONSTRUCTOR
*****************************************************************************
*/
construct : function(vMin, vValue, vMax)
{
this.base(arguments);
// ************************************************************************
// TEXTFIELD
// ************************************************************************
this._textfield = new rwt.widgets.base.BasicText();
this._textfield.setBorder(null);
this._textfield.setWidth("1*");
this._textfield.setAllowStretchY(true);
this._textfield.setHeight(null);
this._textfield.setVerticalAlign("middle");
this._textfield.setAppearance("spinner-text-field");
this.add(this._textfield);
// ************************************************************************
// BUTTON LAYOUT
// ************************************************************************
this._buttonlayout = new rwt.widgets.base.VerticalBoxLayout();
this._buttonlayout.setWidth("auto");
this.add(this._buttonlayout);
// ************************************************************************
// UP-BUTTON
// ************************************************************************
this._upbutton = new rwt.widgets.base.BasicButton( "push", true );
this._upbutton.setAppearance("spinner-button-up");
this._upbutton.setTabIndex(null);
this._upbutton.setHeight("1*");
this._buttonlayout.add(this._upbutton);
// ************************************************************************
// DOWN-BUTTON
// ************************************************************************
this._downbutton = new rwt.widgets.base.BasicButton( "push", true );
this._downbutton.setAppearance("spinner-button-down");
this._downbutton.setTabIndex(null);
this._downbutton.setHeight("1*");
this._buttonlayout.add(this._downbutton);
// ************************************************************************
// TIMER
// ************************************************************************
this._timer = new rwt.client.Timer(this.getInterval());
// ************************************************************************
// MANAGER
// ************************************************************************
this.setManager(new rwt.util.Range());
this.initWrap();
// ************************************************************************
// EVENTS
// ************************************************************************
this.addEventListener("keypress", this._onkeypress, this);
this.addEventListener("keydown", this._onkeydown, this);
this.addEventListener("keyup", this._onkeyup, this);
this.addEventListener("mousewheel", this._onmousewheel, this);
this._textfield.addEventListener("changeValue", this._ontextchange, this);
this._textfield.addEventListener("input", this._oninput, this);
this._textfield.addEventListener("blur", this._onblur, this);
this._textfield.addEventListener("keypress", this._onkeypress, this);
this._upbutton.addEventListener("mousedown", this._onmousedown, this);
this._downbutton.addEventListener("mousedown", this._onmousedown, this);
this._timer.addEventListener("interval", this._oninterval, this);
// ************************************************************************
// INITIALIZATION
// ************************************************************************
if (vMin != null) {
this.setMin(vMin);
}
if (vMax != null) {
this.setMax(vMax);
}
if (vValue != null) {
this.setValue(vValue);
}
this._checkValue = this.__checkValue;
this.initWidth();
this.initHeight();
this._last_value = "";
},
/*
*****************************************************************************
EVENTS
*****************************************************************************
*/
events: {
/**
* Fired each time the value of the spinner changes.
* The "data" property of the event is set to the new value
* of the spinner.
*/
"change" : "rwt.event.DataEvent"
},
/*
*****************************************************************************
PROPERTIES
*****************************************************************************
*/
properties :
{
/*
---------------------------------------------------------------------------
PROPERTIES
---------------------------------------------------------------------------
*/
appearance :
{
refine : true,
init : "spinner"
},
width :
{
refine : true,
init : 60
},
height :
{
refine : true,
init : 22
},
/** The amount to increment on each event (keypress or mousedown). */
incrementAmount :
{
check : "Number",
init : 1,
apply : "_applyIncrementAmount"
},
/** The amount to increment on each event (keypress or mousedown). */
wheelIncrementAmount :
{
check : "Number",
init : 1
},
/** The amount to increment on each pageup / pagedown keypress */
pageIncrementAmount :
{
check : "Number",
init : 10
},
/** The current value of the interval (this should be used internally only). */
interval :
{
check : "Integer",
init : 100
},
/** The first interval on event based shrink/growth of the value. */
firstInterval :
{
check : "Integer",
init : 500
},
/** This configures the minimum value for the timer interval. */
minTimer :
{
check : "Integer",
init : 20
},
/** Decrease of the timer on each interval (for the next interval) until minTimer reached. */
timerDecrease :
{
check : "Integer",
init : 2
},
/** If minTimer was reached, how much the amount of each interval should grow (in relation to the previous interval). */
amountGrowth :
{
check : "Number",
init : 1.01
},
/** whether the value should wrap around */
wrap :
{
check : "Boolean",
init : false,
apply : "_applyWrap"
},
/** Controls whether the textfield of the spinner is editable or not */
editable :
{
check : "Boolean",
init : true,
apply : "_applyEditable"
},
/** Range manager */
manager :
{
check : "rwt.util.Range",
apply : "_applyManager",
dispose : true
},
/** Holding a reference to the protected {@link _checkValue} method */
checkValueFunction :
{
apply : "_applyCheckValueFunction"
},
/** */
selectTextOnInteract :
{
check : "Boolean",
init : true
}
},
/*
*****************************************************************************
MEMBERS
*****************************************************************************
*/
members :
{
_applyIncrementAmount : function(value) {
this._computedIncrementAmount = value;
},
_applyEditable : function(value)
{
if (this._textfield) {
this._textfield.setReadOnly(! value);
}
},
_applyWrap : function(value)
{
this.getManager().setWrap(value);
this._onchange();
},
_applyManager : function(value, old)
{
if (old)
{
old.removeEventListener("change", this._onchange, this);
}
if (value)
{
value.addEventListener("change", this._onchange, this);
}
// apply initital value
this._onchange();
},
_applyCheckValueFunction : function(value) {
this._checkValue = value;
},
/*
---------------------------------------------------------------------------
PREFERRED DIMENSIONS
---------------------------------------------------------------------------
*/
/**
* Returns the prefered inner width for the spinner widget. Currently this
* method returns 50.
*
* @type member
* @return {Integer} prefered inner width for the spinner widget
*/
_computePreferredInnerWidth : function() {
return 50;
},
/**
* Return the prefered inner height for the spinner widget. Currently this
* method returns 14
*
* @type member
* @return {Integer} prefered inner height for the spinner widget
*/
_computePreferredInnerHeight : function() {
return 14;
},
/*
---------------------------------------------------------------------------
KEY EVENT-HANDLING
---------------------------------------------------------------------------
*/
/**
* Callback for the "keyPress" event.<br/>
* Perform action when "Enter" (without "Alt"), control keys
* and numeric (0-9) keys are pressed. Suppress all key events for
* events without modifiers.
*
* @type member
* @param e {rwt.event.KeyEvent} keyPress event
* @return {void}
*/
_onkeypress : function(e)
{
var vIdentifier = e.getKeyIdentifier();
if (vIdentifier == "Enter" && !e.isAltPressed())
{
this._checkValue(true, false);
if (this.getSelectTextOnInteract()) {
this._textfield.selectAll();
}
}
else
{
switch(vIdentifier)
{
case "Up":
case "Down":
case "Left":
case "Right":
case "Shift":
case "Control":
case "Alt":
case "Escape":
case "Delete":
case "Backspace":
case "Insert":
case "Home":
case "End":
case "PageUp":
case "PageDown":
case "NumLock":
case "Tab":
break;
default:
if( !this._isMinus( vIdentifier ) && !this._isNumber( vIdentifier ) ) {
var modifiers = e.getModifiers();
if( modifiers === 0 || modifiers === rwt.event.DomEvent.SHIFT_MASK ) {
e.preventDefault();
}
}
}
}
},
_isMinus : function( identifier ) {
var caretPosition = this._textfield._getSelectionStart();
var text = this._textfield.getValue();
return identifier === "-" && caretPosition === 0 && !/-/.test( text );
},
_isNumber : function( identifier ) {
return identifier >= "0" && identifier <= "9";
},
/**
* Callback for "keyDown" event.<br/>
* Controls the interval mode ("single" or "page")
* and the interval increase by detecting "Up"/"Down"
* and "PageUp"/"PageDown" keys.
* Starting a timer to control the incrementing of the
* spinner value.
*
* @type member
* @param e {rwt.event.KeyEvent} keyDown event
* @return {void}
*/
_onkeydown : function(e)
{
var vIdentifier = e.getKeyIdentifier();
if (this._intervalIncrease == null)
{
switch(vIdentifier)
{
case "Up":
case "Down":
this._intervalIncrease = vIdentifier == "Up";
this._intervalMode = "single";
this._resetIncrements();
this._checkValue(true, false);
this._increment();
this._timer.startWith(this.getFirstInterval());
break;
case "PageUp":
case "PageDown":
this._intervalIncrease = vIdentifier == "PageUp";
this._intervalMode = "page";
this._resetIncrements();
this._checkValue(true, false);
this._pageIncrement();
this._timer.startWith(this.getFirstInterval());
break;
}
}
},
/**
* Callback for "keyUp" event.<br/>
* Detecting "Up"/"Down" and "PageUp"/"PageDown" keys.
* If detected the interval mode and interval increase get resetted
* and the timer for the control of the increase of the spinner value
* gets stopped.
*
* @type member
* @param e {rwt.event.KeyEvent} keyUp event
* @return {void}
*/
_onkeyup : function(e)
{
if (this._intervalIncrease != null)
{
switch(e.getKeyIdentifier())
{
case "Up":
case "Down":
case "PageUp":
case "PageDown":
this._timer.stop();
this._intervalIncrease = null;
this._intervalMode = null;
}
}
},
/*
---------------------------------------------------------------------------
MOUSE EVENT-HANDLING
---------------------------------------------------------------------------
*/
/**
* Callback method for the "mouseDown" event of the spinner buttons.<br/>
* State handling, registering event listeners at the spinner button and
* invoking the increment management (resets increments, setup and start timer etc.).
*
* @type member
* @param e {rwt.event.MouseEvent} mouseDown event
* @return {void}
*/
_onmousedown : function(e)
{
if (!e.isLeftButtonPressed()) {
return;
}
this._checkValue(true);
var vButton = e.getCurrentTarget();
vButton.addState("pressed");
vButton.addEventListener("mouseup", this._onmouseup, this);
vButton.addEventListener("mouseout", this._onmouseup, this);
this._intervalIncrease = vButton == this._upbutton;
this._resetIncrements();
this._increment();
if (this.getSelectTextOnInteract()) {
this._textfield.selectAll();
}
this._timer.setInterval(this.getFirstInterval());
this._timer.start();
},
/**
* Callback method for the "mouseUp" event of the spinner buttons.<br/>
* State handling, removing event listeners at the spinner button, focusing
* the text field and resetting the interval management (stopping timer,
* resetting interval increase).
*
* @type member
* @param e {rwt.event.MouseEvent} mouseUp event
* @return {void}
*/
_onmouseup : function(e)
{
var vButton = e.getCurrentTarget();
vButton.removeState("pressed");
vButton.removeEventListener("mouseup", this._onmouseup, this);
vButton.removeEventListener("mouseout", this._onmouseup, this);
if (this.getSelectTextOnInteract()) {
this._textfield.selectAll();
}
this._textfield.setFocused(true);
this._timer.stop();
this._intervalIncrease = null;
},
/**
* Callback method for the "mouseWheel" event.<br/>
* Delegates the in-/decrementing to the manager and
* selects the text field.
*
* @type member
* @param e {rwt.event.MouseEvent} mouseWheel event
* @return {void}
*/
_onmousewheel : function(e)
{
this._checkValue(true);
if (this.getManager().incrementValue)
{
this.getManager().incrementValue(this.getWheelIncrementAmount() *
e.getWheelDelta());
}
else
{
var value = this.getManager().getValue() +
(this.getWheelIncrementAmount() *
e.getWheelDelta());
value = this.getManager().limit(value);
this.getManager().setValue(value);
}
if( this.getSelectTextOnInteract() ) {
this._textfield.selectAll();
}
// RAP [rst] See https://bugs.eclipse.org/bugs/show_bug.cgi?id=283546
e.preventDefault();
e.stopPropagation();
// END RAP
},
/*
---------------------------------------------------------------------------
OTHER EVENT-HANDLING
---------------------------------------------------------------------------
*/
/**
* Event handler method for text changes
*
* @type member
* @param e {rwt.event.ChangeEvent} change event
* @return {void}
*/
_ontextchange : function(e) {
this._last_value = e.getOldValue();
},
/**
* Callback method for the "input" event.<br/>
* Delegates the further processing to the method
* hold by the "checkValue" property.
*
* @type member
* @param e {rwt.event.DataEvent} input event
* @return {void}
*/
_oninput : function() {
this._checkValue(true, true);
},
/**
* Callback method for the "change" event.<br/>
* Sets the value of the text field and enables/disables
* the up-/down-buttons of the min-/max-value are reached
* (additionally stops the timer of the min-/max-boundaries are reached)
* Dispatched the "change" event.
*
* @type member
* @param e {rwt.event.ChangeEvent} change event
* @return {void}
*/
_onchange : function()
{
var vValue = this.getManager().getValue();
this._textfield.setValue(String(vValue));
if (vValue == this.getMin() && !this.getWrap())
{
this._downbutton.removeState("pressed");
this._downbutton.setEnabled(false);
this._timer.stop();
}
else
{
this._downbutton.resetEnabled();
}
if (vValue == this.getMax() && !this.getWrap())
{
this._upbutton.removeState("pressed");
this._upbutton.setEnabled(false);
this._timer.stop();
}
else
{
this._upbutton.resetEnabled();
}
this.createDispatchDataEvent("change", vValue);
},
/**
* Callback method for the "blur" event.<br/>
* Calls the method of the "checkValueFunction" property
*
* @type member
* @param e {rwt.event.FocusEvent} blur event
* @return {void}
*/
_onblur : function() {
this._checkValue(false);
},
/*
---------------------------------------------------------------------------
MAPPING TO RANGE MANAGER
---------------------------------------------------------------------------
*/
/**
* Mapping to the "setValue" method of the Range manager
*
* @type member
* @param nValue {Number} new value of the spinner
* @return {void}
*/
setValue : function(nValue) {
this.getManager().setValue(this.getManager().limit(nValue));
},
/**
* Mapping to the "getValue" method of the Range manager
*
* @type member
* @return {Number} Current value of the spinner
*/
getValue : function()
{
// make sure the manager is uptodate with what is on screen
this._checkValue(true);
return this.getManager().getValue();
},
/**
* Mapping to the "resetValue" method of the Range manager
*
* @type member
* @return {void}
*/
resetValue : function() {
this.getManager().resetValue();
},
/**
* Mapping to the "setMax" method of the Range manager
*
* @type member
* @param vMax {Number} new max value of the spinner
* @return {Number} new max value of the spinner
*/
setMax : function(vMax) {
return this.getManager().setMax(vMax);
},
/**
* Mapping to the "getMax" method of the Range manager
*
* @type member
* @return {Number} current max value of the spinner
*/
getMax : function() {
return this.getManager().getMax();
},
/**
* Mapping to the "setMin" method of the Range manager
*
* @type member
* @param vMin {Number} new min value of the spinner
* @return {Number} new min value of the spinner
*/
setMin : function(vMin) {
return this.getManager().setMin(vMin);
},
/**
* Mapping to the "getMin" method of the Range manager
*
* @type member
* @return {Number} current min value of the spinner
*/
getMin : function() {
return this.getManager().getMin();
},
/*
---------------------------------------------------------------------------
INTERVAL HANDLING
---------------------------------------------------------------------------
*/
_intervalIncrease : null,
/**
* Callback method for the "interval" event.<br/>
* Stops the timer and sets a new interval. Executes the increment
* of the spinner depending on the intervalMode and restarts the timer with
* the new interval.
*
* @type member
* @param e {rwt.event.Event} interval event
* @return {void}
*/
_oninterval : function()
{
this._timer.stop();
this.setInterval(Math.max(this.getMinTimer(), this.getInterval() - this.getTimerDecrease()));
if (this._intervalMode == "page")
{
this._pageIncrement();
}
else
{
if (this.getInterval() == this.getMinTimer()) {
this._computedIncrementAmount = this.getAmountGrowth() * this._computedIncrementAmount;
}
this._increment();
}
var wrap = this.getManager().getWrap();
switch( this._intervalIncrease ) {
case true:
if( !( this.getValue() == this.getMax() && !wrap ) ) {
this._timer.restartWith(this.getInterval());
}
break;
case false:
if( !( this.getValue() == this.getMin() && !wrap ) ) {
this._timer.restartWith(this.getInterval());
}
break;
}
},
/*
---------------------------------------------------------------------------
UTILITY
---------------------------------------------------------------------------
*/
/**
* Default check value utility method
*
* @type member
* @param acceptEmpty {Boolean} Whether empty values are allowed or not.
* @param acceptEdit {Boolean} Whether editing is accepted or not.
* @return {void}
*/
__checkValue : function(acceptEmpty, acceptEdit)
{
var el = this._textfield.getInputElement();
if (!el) {
return;
}
if( ( el.value === "" ) || ( el.value === "-" ) ) {
if (!acceptEmpty)
{
this.resetValue();
return;
}
}
else
{
// cache original value
var str_val = el.value;
// prepare for parsing. We don't use numberFormat itself to parse the
// string, as we want to be a little more liberal at this point since
// we might be currently editing the string. For example, we accept
// things like "4000."
var parsable_str;
parsable_str = str_val;
// parse the string
var val = parseFloat(parsable_str);
var limitedVal = this.getManager().limit(val);
var oldValue = this.getManager().getValue();
var fixedVal = limitedVal;
// NaN means we had a parse error, but we'll be more picky than
// parseFloat (refuse stuff like 5.55-12.5 which parseFloat
// parses as 5.55). We also refuse values outside the range.
if (isNaN(val) || (limitedVal != val) || (val != parsable_str))
{
if (acceptEdit) {
this._textfield.setValue(this._last_value);
} else {
if (isNaN(limitedVal)) {
// reset to last correct value
fixedVal = oldValue;
} else {
fixedVal = limitedVal;
}
}
}
if( acceptEdit ) {
return;
}
var formattedValue;
formattedValue = String(fixedVal);
if ((fixedVal === oldValue) && (str_val !== formattedValue)) {
// "silently" update the displayed value as it won't get
// updated by the range manager since it considers the value as
// unchanged.
this._textfield.setValue(formattedValue);
}
// inform manager
this.getManager().setValue(fixedVal);
}
},
/**
* Performs a normal increment
*
* @type member
* @return {void}
*/
_increment : function()
{
if (this.getManager().incrementValue)
{
this.getManager().incrementValue((this._intervalIncrease ? 1 : -1) *
this._computedIncrementAmount);
}
else
{
var value = this.getManager().getValue() +
((this._intervalIncrease ? 1 : -1) *
this._computedIncrementAmount);
value = this.getManager().limit(value);
this.getManager().setValue(value);
}
},
/**
* Performs a page increment
*
* @type member
* @return {void}
*/
_pageIncrement : function()
{
if (this.getManager().pageIncrementValue)
{
this.getManager().pageIncrementValue();
}
else
{
var value = this.getManager().getValue() +
((this._intervalIncrease ? 1 : -1) *
this.getPageIncrementAmount());
value = this.getManager().limit(value);
this.getManager().setValue(value);
}
},
/**
* Reset the increments
*
* @type member
* @return {void}
*/
_resetIncrements : function()
{
this._computedIncrementAmount = this.getIncrementAmount();
this.resetInterval();
}
},
/*
*****************************************************************************
DESTRUCTOR
*****************************************************************************
*/
destruct : function()
{
var mgr = this.getManager();
if (mgr) {
mgr.dispose();
}
this._disposeObjects("_textfield", "_buttonlayout", "_upbutton", "_downbutton",
"_timer");
}
});