;+
; 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:
;    INCREMENT: Numerical ammount spinner value is incremented/decremented
;    LABEL: Text label for spinner
;    SPIN_TIME: Delay before 'spinning'
;    UNITS: Units (text) to appear to the right of the value
;    VALUE: Initial value
;    XLABELSIZE: Size of the text label
;    TEXT_BOX_SIZE: Size (in char) of the text box
;    TOOLTIP: Tooltip (text)
;    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)
;-

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
    
    ; 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}
;      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
   
  ;carriage return
  if  Tag_Names(event,/Structure_Name) eq 'WIDGET_TEXT_CH' && event.ch eq 10 then begin
    widget_control,event.id,set_value=state.value+state.units
    parent = widget_info(event.id, /PARENT)
    RETURN, {ID: parent, TOP: event.top, HANDLER: event.handler, VALUE: new[0], VALID:is_numeric(new[0])}
  endif


  ;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
  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, $
  ALL_EVENTS=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 
  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)


;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