;+ ; NAME: ; THM_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 thm_ui_spinner_set_max_value/thm_ui_spinner_set_min_value to change max_value, min_value. ; thm_ui_spinner_get_max_value/thm_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. thm_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 thm_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 = thm_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 thm_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 if Tag_Names(event,/Structure_Name) eq 'WIDGET_TEXT_CH' && event.ch eq 10 then begin ; enter was pressed, we should ensure the value is updated widget_control, event.id, set_value=new[0]+state.units state.value = new[0] widget_control, event.id, set_uvalue=state parent = widget_info(event.id, /PARENT) RETURN, {ID: parent, TOP: event.top, HANDLER: event.handler, VALUE: double(new[0]), VALID:is_numeric(new[0])} 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) 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[0]), 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 thm_ui_spinner_up_click, event compile_opt idl2, hidden RETURN, thm_ui_spinner_button_event(event) END ;--------------------------------------------------------------------- FUNCTION thm_ui_spinner_down_click, event compile_opt idl2, hidden RETURN, thm_ui_spinner_button_event(event, /down_click) END ;--------------------------------------------------------------------- FUNCTION thm_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 ;Return NaN if value isn't numeric or double() conversion fails if is_numeric(value) then begin on_ioerror, fail return, double(value) fail: return, !values.D_NAN endif else begin return,!values.D_NAN endelse END ;--------------------------------------------------------------------- PRO thm_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 = thm_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 = thm_ui_spinner_update_event( {ID: text, TOP: base, HANDLER: text} ) END ;--------------------------------------------------------------------- FUNCTION thm_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 thm_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 thm_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 thm_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 thm_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 thm_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 thm_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 = thm_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='thm_ui_spinner_getvalue', $ PRO_SET_VALUE='thm_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='thm_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='thm_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='thm_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='thm_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