;+
; NAME:
; SPD_UI_SPINNER
;
; PURPOSE:
; A compound 'spinner' widget for editing numerical values.
; Consists of up and down buttons and a text field for display
; and direct editing.
;
; CALLING SEQUENCE:
; Result = SPINNER(parent)
;
; KEYWORD PARAMETERS:
; VALUE: Initial value (float/double)
; INCREMENT: Numerical ammount spinner value is incremented/decremented
; LABEL: String to be used as the widget's label
; UNITS: String containing the units to appear to the right of the value
; SPIN_TIME: Delay before 'spinning' when clicking up/down buttons
; XLABELSIZE: Size of the text label in points
; getXLabelSize: Set to a named variable to pass back the length of the
; spinner's label in points
; TEXT_BOX_SIZE: Size of the text box in # of characters
; TOOLTIP: String to be used at the buttons' tooltip
; MAX_VALUE: The maximum allowed value for the spinner (optional)
; MIN_VALUE: The minimum allowed value for the spinner (optional)
;
; EVENT STRUCTURE:
; When the field is modified (either directly) or by the
; up/down buttons, the following event is returned:
; {ID: id, TOP: top, HANDLER: handler, VALUE: value, VALID: valid}
;
; VALUE: formatted, double precision number from widget's text field;
; value is NaN when the text widget contains an unrecognizable
; or unconvertable number
; VALID: 0 if value is not a recogizable/convertable number, or if buttons
; attempt to go outside min/max range, 1 otherwise
;
;
; GETTING/SETTINGS VALUES
; Using get_value from widget_control will return the same as the VALUE
; field in the event structure.
; The set_value procedure expects numerical input (e.g. doubles, floats, ints).
; Use spd_ui_spinner_set_max_value/spd_ui_spinner_set_min_value to change max_value, min_value.
; spd_ui_spinner_get_max_value/spd_ui_spinner_get_min_value return MAX_VALUE, MIN_VALUE
;
; NOTES:
; (lphilpott 06/10/11)
; Setting the max_value and min_value will restrict the use of the up and down buttons
; to reach values outside that range.
; They do not impact on values that are typed into the text field (these
; should be handled by the calling function) or any values simply passed to the set_value procedure.
; (lphilpott 06/15/11)
; Use of VALID: VALID=0 if the event is not valid. This includes non-recognizable
; numbers but ALSO cases where the user tries to go outside the allowed min-max range using up and down buttons.
; If the user clicks the up/down button and reaches the limit, the value returned is the limit value and valid=0.
; If the user enters a non numerical value the value returned is NAN and valid=0
; This allows the calling procedure to issue messages to the user such as invalid entry based on both VALUE and VALID.
; Note: valid = 0 only if the starting value of the spinner is at the bounds. If the user holds down the buttons and reaches
; the bounds valid = 1. This avoids problems where existing code processes events only if event.valid = 1.
;
; I have not used max and min to restrict values typed into the field due to the difficulty of knowing when the user
; has finished typing - do not want to reset the value while the user is still entering it.
;
; When using a spinner cases where the user types in a value outside the allowed range need to be handled, also
; invalid entries. If necessary warning messages should be issued to user when they use the up/down buttons and have
; reached the allowed limits.
;
;
; Added get and set procedures for min and max designed to be called externally, eg. spd_ui_spinner_set_max_value
; Idea is to allow max and min to be set in case they need to be changed after spinner is created (e.g for range spinners where allowed values
; depend on whether scaling is linear or log)
;
;HISTORY:
;
;$LastChangedBy: $
;$LastChangedDate: $
;$LastChangedRevision: $
;$URL: $
;---------------------------------------------------------------------------------
FUNCTION spd_ui_spinner_button_event, event, down_click=down
compile_opt idl2, hidden
; Save current select state to later see if the mouse is released.
widget_control, event.id, SET_UVALUE=event.select
IF (event.select eq 0) then begin
widget_control, event.id, /CLEAR_EVENTS
RETURN, 0
ENDIF
; The widget Base and Text Field widget IDs.
base = widget_info(widget_info(event.id, /PARENT), /PARENT)
text = widget_info(base, FIND_BY_UNAME='_text')
handler = base
; Find parent's event handler.
WHILE (widget_info(handler, /VALID)) do begin
par_event_func = widget_info(handler, /EVENT_FUNC)
par_event_pro = widget_info(handler, /EVENT_PRO)
IF (par_event_func || par_event_pro) then BREAK
handler = widget_info(handler, /PARENT)
ENDWHILE
; Iterations before starting spin; allows for slow press w/o activating spinner.
delay = 10
result = 0
widget_control, text, GET_UVALUE=state
i = keyword_set(down) ? -1L : 1L
prevvel = double(state.value)
; To stop if the mouse is released.
WHILE (1) do begin
;widget_control, text, GET_VALUE=old
old = double(state.value)
oldint = old*(1d/state.increment)
time = systime(1)
; ; If rounded to nearest fraction, then increment; otherwise, round off.
; ; Avoid roundoff errors by testing against a tiny #.
; IF ( abs(oldint - round(oldint,/L64)) lt 1d-7 ) then $
; new = old + i*state.increment $ ; + 1d-9*i ;tiny number needed?
; ELSE new = (keyword_set(down) ? floor(oldint,/L64) : ceil(oldint,/L64) )*state.increment
;try simple increment instead of rounding
;rounding errors *should* be solved by the use of formatannotation
new = old + i * state.increment
; check whether new is within the min max range
;only set valid = 0 if user hits up/down button when value is already min/max (not when user holds down button from other value)
tmpvalid = 1
if finite(state.minValue) then begin
if new lt state.minValue then begin
new = state.minValue
if prevvel eq state.minValue then tmpvalid = 0
endif
endif
if finite(state.maxValue) && tmpvalid then begin
if new gt state.maxValue then begin
new = state.maxValue
if prevvel eq state.maxValue then tmpvalid = 0
endif
endif
; Update the text widget.
new = spd_ui_spinner_num_to_string(new, state.units,state.precision)
state.value = new
widget_control, text, SET_VALUE=new, SET_UVALUE=state
; Create and feed a new event into any parent's event handler.
IF (par_event_func || par_event_pro) then begin
; new_event = {ID: base, TOP: event.top, HANDLER: handler, VALUE: double(new), VALID: tmpvalid}
new_event = {ID: base, TOP: event.top, HANDLER: handler, VALUE: new, VALID: tmpvalid}
; if (par_event_func) $
; then result = call_function(par_event_func, new_event) $
; else call_procedure, par_event_pro, new_event
ENDIF
; Delay start of spin
; ### loop ###
repeat begin
; Check for new event (mouse - unclick)
newevent = widget_event(event.top, BAD_ID=bad, /NOWAIT)
; Quit if bad ID occurs.
IF (bad ne 0L) then RETURN, 0
widget_control, event.id, GET_UVALUE = x
; End if mouse was released and
; send to parent function/procedure
IF (x eq 0) then begin
if (par_event_func) then begin
return, call_function(par_event_func, new_event)
endif else if (par_event_pro) then begin
call_procedure, par_event_pro, new_event
endif
return,0
ENDIF
; Delay before starting spin.
IF (delay ne 0) then begin
WAIT, 0.05d
delay--
ENDIF
endrep until delay eq 0
elapsed = systime(1) - time
IF (elapsed lt state.spinTime) then wait, state.spinTime - elapsed
ENDWHILE
END ;---------------------------------------------------------------------
FUNCTION spd_ui_spinner_update_event, event
compile_opt idl2, hidden
on_ioerror, null
; Pull new values and save old for reset
widget_control, event.id, GET_VALUE=new, GET_UVALUE=state
; For testing/debugging only
;if new[0] eq (state.value+state.units) then return,0
;if new[0] eq '-*' then return,0
;If there are units on the value, then remove them
if stregex(new,'.*' + state.units + '.*$',/boolean) then begin
new = (stregex(new,' *(.*)'+state.units + '.*$',/extract,/subexpr))[1]
endif
;carriage return - enter was pressed, we should ensure the value is updated
if Tag_Names(event,/Structure_Name) eq 'WIDGET_TEXT_CH' && event.ch eq 10 then begin
if is_numeric(new,/sci) then begin
; if we have a valid number, use calc to evaluate the expression
a='value='
b=new
calc, a+b
widget_control, event.id, set_value=strtrim(string(value),2)+state.units
state.value = value
widget_control, event.id, set_uvalue=state
parent = widget_info(event.id, /PARENT)
RETURN, {ID: parent, TOP: event.top, HANDLER: event.handler, VALUE: double(value), VALID:is_numeric(value,/sci)}
endif
endif
offset = widget_info(event.id, /text_select)
widget_control,event.id,set_value=new+state.units,set_text_select=offset
parent = widget_info(event.id, /PARENT)
if is_numeric(new,/sci) then begin ;if the input is a correctly formatted numerical value then store it, and generate an event
state.value = new
widget_control,event.id,set_uvalue=state
on_ioerror, fail
RETURN, {ID: parent, TOP: event.top, HANDLER: event.handler, VALUE: double(new), VALID: 1}
fail: RETURN, {ID: parent, TOP: event.top, HANDLER: event.handler, VALUE: !values.D_NAN, VALID: 0}
endif else begin
return, {ID: parent, TOP: event.top, HANDLER: event.handler, VALUE: !values.D_NAN, VALID: 0}
endelse
END ;---------------------------------------------------------------------
FUNCTION spd_ui_spinner_up_click, event
compile_opt idl2, hidden
RETURN, spd_ui_spinner_button_event(event)
END ;---------------------------------------------------------------------
FUNCTION spd_ui_spinner_down_click, event
compile_opt idl2, hidden
RETURN, spd_ui_spinner_button_event(event, /down_click)
END ;---------------------------------------------------------------------
FUNCTION spd_ui_spinner_getvalue, base
compile_opt idl2, hidden
widget_control, widget_info(base, FIND_BY_UNAME='_text'), GET_VALUE=value,get_uvalue=state
value = value[0]
;If there are units on the value, then remove them
if stregex(value,'.*' + state.units + '.*$',/boolean) then begin
value = (stregex(value,' *(.*)'+state.units + '.*$',/extract,/subexpr))[1]
endif
if is_numeric(value[0],/sci) then begin
; if we have a valid number, use calc to evaluate the expression
a='cexp='
b=string(value[0])
; if the value is in decimal notation, append D0 to force calc to treat it as a double
if is_numeric(value[0], /decimal) then calc, a+b+'D0' else calc, a+b
endif else cexp = ''
;Return NaN if value isn't numeric or double() conversion fails
if is_numeric(string(cexp),/sci) then begin
on_ioerror, fail
return, double(cexp)
fail: return, !values.D_NAN
endif else begin
return,!values.D_NAN
endelse
END ;---------------------------------------------------------------------
PRO spd_ui_spinner_setvalue, base, value
compile_opt idl2, hidden
; Set text widget value
text = widget_info(base, FIND_BY_UNAME='_text')
offset = widget_info(text, /text_select)
;widget_control, text, SET_VALUE=string(value), SET_TEXT_SELECT = offset
widget_control,text,get_uvalue=state
state.value = spd_ui_spinner_num_to_string(value,state.units,state.precision)
widget_control, text, SET_VALUE=state.value,set_text_select=offset
;Update spinner value with event handler
set = spd_ui_spinner_update_event( {ID: text, TOP: base, HANDLER: text} )
END ;---------------------------------------------------------------------
FUNCTION spd_ui_spinner_num_to_string, num_in, units_in,precision
compile_opt idl2, hidden
data = {timeaxis:0,$
formatid:precision,$ ;default 10
scaling:0,$
exponent:0,$
noformatcodes:1}
;exponent:1} ;changing to autoformat
; RETURN, string(num_in,FORMAT='(G0)')+ units_in
RETURN, formatannotation(0,0,num_in,data=data) + units_in
END ;---------------------------------------------------------------------
PRO spd_ui_spinner_remove_spaces, string_in
compile_opt idl2, hidden
if ~strmatch(string_in, '*[! ]*') then return
while strmatch(string_in, '*[ ]*') do begin
bst = byte(string_in)
space = byte(' ')
bst = bst[where(bst ne space[0],count)]
string_in = string(bst)
endwhile
END ;---------------------------------------------------------------------
pro spd_ui_spinner_set_max_value, base, maxvalue
text = widget_info(base, FIND_BY_UNAME='_text')
widget_control,text,get_uvalue=state
state.maxValue = maxvalue
widget_control, text, set_uvalue=state
end
pro spd_ui_spinner_set_min_value, base, minvalue
text = widget_info(base, FIND_BY_UNAME='_text')
widget_control,text,get_uvalue=state
state.minValue = minvalue
widget_control, text, set_uvalue=state
end
pro spd_ui_spinner_get_max_value, base, maxvalue
text = widget_info(base, FIND_BY_UNAME='_text')
widget_control,text,get_uvalue=state
maxvalue = state.maxValue
end
pro spd_ui_spinner_get_min_value, base, minvalue
text = widget_info(base, FIND_BY_UNAME='_text')
widget_control,text,get_uvalue=state
minvalue = state.minValue
end
FUNCTION spd_ui_spinner, parent, $
INCREMENT=inc_set, $
LABEL=label_set, $
SPIN_TIME=spin_time_set, $
UNITS=unit_set, $
VALUE=value_set, $
XLABELSIZE=label_size_set, $
TEXT_BOX_SIZE=text_box_size, $
getXLabelSize=size_out_var, $
DISABLE_ALL_EVENTS=disable_all_events,$
TOOLTIP=tooltip, $
precision=precision, $
MAX_VALUE=max_value_set, $
MIN_VALUE=min_value_set, $
_EXTRA=_extra
compile_opt idl2, hidden
if ~keyword_set(precision) then begin
; lphilpott 6-mar-2012 reducing default precision as 16 seems like more precision than is possible
;precision = 16
precision = 13
endif
; Initiations & checks
increment = n_elements(inc_set) ? double(inc_set[0]) : 0.1d
spinTime = keyword_set(spin_time_set) ? (double(spin_time_set) > 0d) : 0.05d
units = keyword_set(unit_set) ? unit_set : ''
valuenum = keyword_set(value_set) ? value_set : 0
value = spd_ui_spinner_num_to_string(valuenum, units,precision)
tboxsize = keyword_set(text_box_size) ? text_box_size : 8
; if value has been given ensure max and min are moved to allow it
if n_elements(max_value_set) then begin
maxvalue = keyword_set(value_set) ? (double(max_value_set) > valuenum) : double(max_value_set)
endif else maxValue = !values.d_nan
if n_elements(min_value_set) then begin
minvalue = keyword_set(value_set) ? (double(min_value_set) < valuenum) : double(min_value_set)
endif else minValue = !values.d_nan
state = {VALUE: value, INCREMENT: increment, SPINTIME: spinTime, UNITS: units,$
precision:precision, MAXVALUE: maxValue, MINVALUE: minValue}
; General base for each part of the compound widget
base = widget_base(parent, /ROW, $
FUNC_GET_VALUE='spd_ui_spinner_getvalue', $
PRO_SET_VALUE='spd_ui_spinner_setvalue', $
XPAD=0, YPAD=0, SPACE=1, _EXTRA=_extra)
; Label
label = (keyword_set(label_set)) ? $
widget_label(base, VALUE=label_set, XSIZE=label_size_set) : ''
if arg_present(size_out_var) then begin
geo_info = widget_info(label,/geometry)
size_out_var = geo_info.scr_xsize
endif
; Text
; Note: To fix the issue with zoom redrawing after every keystroke, we pass
; the 'disable_all_events' keyword when creating the zoom spinner widget
if undefined(disable_all_events) then begin
text = widget_text(base, /editable, /all_events, $
EVENT_FUNC='spd_ui_spinner_update_event', $
IGNORE_ACCELERATORS=['Ctrl+C','Ctrl+V','Ctrl+X','Del'], $
VALUE=value, UNAME='_text', UVALUE=state, XSIZE=tboxsize)
endif else begin
text = widget_text(base, /editable, $
EVENT_FUNC='spd_ui_spinner_update_event', $
IGNORE_ACCELERATORS=['Ctrl+C','Ctrl+V','Ctrl+X','Del'], $
VALUE=value, UNAME='_text', UVALUE=state, XSIZE=tboxsize)
endelse
;Buttons
button_base = widget_base(base, /ALIGN_CENTER, /COLUMN, /TOOLBAR, XPAD=0, YPAD=0, SPACE=0)
; Extra pixel added to button padding for Windows
one = !version.os_family ne 'Windows'
; 'Up' Button
up_button = widget_button(button_base, EVENT_FUNC='spd_ui_spinner_up_click', $
/BITMAP, VALUE= filepath('spinup.bmp', SUBDIR=['resource','bitmaps']), $
/PUSHBUTTON_EVENTS, UNAME='_up',UVALUE=0, XSIZE=16+one, YSIZE=10+one, $
tooltip=tooltip)
; 'Down' Button
down_button = widget_button(button_base, EVENT_FUNC='spd_ui_spinner_down_click', $
/BITMAP, VALUE=filepath('spindown.bmp', SUBDIR=['resource','bitmaps']), $
/PUSHBUTTON_EVENTS, UNAME='_down', UVALUE=0, XSIZE=16+one, YSIZE=10+one, $
tooltip=tooltip)
RETURN, base
END