# Spindate 
# ----------------------------------------------------------------------
# Implements a Date spinner widget.  A date spinner contains three
# Spinner widgets:  one Spinner for months, one SpinInt for days,
# and one SpinInt for years.  Months can be specified as abbreviated
# strings, integers or a user-defined list.  Options exist to manage to 
# behavior, appearance, and format of each component spinner.
#
# ----------------------------------------------------------------------
#  AUTHOR: Sue Yockey                  EMAIL: yockey@actc.com
#          Mark L. Ulferts                    mulferts@austin.dsccc.com
#
#   @(#) $Id: spindate.itk,v 1.5 2001/08/22 15:51:13 smithc Exp $
# ----------------------------------------------------------------------
#            Copyright (c) 1997 DSC Technologies Corporation
# ======================================================================
# Permission to use, copy, modify, distribute and license this software 
# and its documentation for any purpose, and without fee or written 
# agreement with DSC, is hereby granted, provided that the above copyright 
# notice appears in all copies and that both the copyright notice and 
# warranty disclaimer below appear in supporting documentation, and that 
# the names of DSC Technologies Corporation or DSC Communications 
# Corporation not be used in advertising or publicity pertaining to the 
# software without specific, written prior permission.
# 
# DSC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING 
# ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, AND NON-
# INFRINGEMENT. THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, AND THE
# AUTHORS AND DISTRIBUTORS HAVE NO OBLIGATION TO PROVIDE MAINTENANCE, 
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. IN NO EVENT SHALL 
# DSC BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR 
# ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, 
# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTUOUS ACTION,
# ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS 
# SOFTWARE.
# ======================================================================

#
# Default resources.
#
option add *Spindate.monthLabel "Month" widgetDefault
option add *Spindate.dayLabel "Day" widgetDefault
option add *Spindate.yearLabel "Year" widgetDefault
option add *Spindate.monthWidth 4 widgetDefault
option add *Spindate.dayWidth 4 widgetDefault
option add *Spindate.yearWidth 4 widgetDefault

#
# Usual options.
#
itk::usual Spindate {
    keep -background -cursor -foreground -labelfont -textbackground -textfont 
}

# ------------------------------------------------------------------
#                            SPINDATE
# ------------------------------------------------------------------
itcl::class iwidgets::Spindate {
    inherit itk::Widget 
    
    constructor {args} {}
    destructor {}
    
    itk_option define -labelpos labelPos Position w
    itk_option define -orient orient Orient vertical 
    itk_option define -monthon monthOn MonthOn true 
    itk_option define -dayon dayOn DayOn true 
    itk_option define -yearon yearOn YearOn true 
    itk_option define -datemargin dateMargin Margin 1 
    itk_option define -yeardigits yearDigits YearDigits 4
    itk_option define -monthformat monthFormat MonthFormat integer 

    public {
	method get {{format "-string"}} 
	method show {{date now}} 
    }

    protected {
	method _packDate {{when later}} 
	variable _repack {}             ;# Reconfiguration flag.
    }

    private {
	method _lastDay {month year}
	method _spinMonth {direction} 
	method _spinDay {direction} 

	variable _monthFormatStr "%m"
	variable _yearFormatStr "%Y"
	variable _interior
    }
}

#
# Provide a lowercased access method for the Spindate class.
# 
proc ::iwidgets::spindate {pathName args} {
    uplevel ::iwidgets::Spindate $pathName $args
}

# ------------------------------------------------------------------
#                        CONSTRUCTOR
# ------------------------------------------------------------------
itcl::body iwidgets::Spindate::constructor {args} {
    set _interior $itk_interior

    set clicks [clock seconds]

    #
    # Create Month Spinner
    #
    itk_component add month {
	iwidgets::Spinner $itk_interior.month  -fixed 2 -justify right \
	    -decrement [itcl::code $this _spinMonth -1] \
	    -increment [itcl::code $this _spinMonth 1] 
    } {
	keep -background -cursor -arroworient -foreground \
		-labelfont -labelmargin -relief -textbackground \
		-textfont -repeatdelay -repeatinterval

	rename -labeltext -monthlabel monthLabel Text
	rename -width -monthwidth monthWidth Width
    }

    #
    # Take off the default bindings for selction and motion.
    #
    bind [$itk_component(month) component entry] <1> {break}
    bind [$itk_component(month) component entry] <Button1-Motion> {break}
    
    #
    # Create Day Spinner
    #
    itk_component add day {
	iwidgets::Spinint $itk_interior.day -fixed 2 -justify right \
	    -decrement [itcl::code $this _spinDay -1] \
	    -increment [itcl::code $this _spinDay 1]
    } {
	keep -background -cursor -arroworient -foreground \
		-labelfont -labelmargin -relief -textbackground \
		-textfont -repeatdelay -repeatinterval

	rename -labeltext -daylabel dayLabel Text
	rename -width -daywidth dayWidth Width
    }
    
    #
    # Take off the default bindings for selction and motion.
    #
    bind [$itk_component(day) component entry] <1> {break}
    bind [$itk_component(day) component entry] <Button1-Motion> {break}
    
    #
    # Create Year Spinner
    #
    itk_component add year {
	iwidgets::Spinint $itk_interior.year -fixed 2 -justify right \
	    -range {1900 3000}
    } {
	keep -background -cursor -arroworient -foreground \
		-labelfont -labelmargin -relief -textbackground \
		-textfont -repeatdelay -repeatinterval

	rename -labeltext -yearlabel yearLabel Text
	rename -width -yearwidth yearWidth Width
    }

    #
    # Take off the default bindings for selction and motion.
    #
    bind [$itk_component(year) component entry] <1> {break}
    bind [$itk_component(year) component entry] <Button1-Motion> {break}
    
    #
    # Initialize the widget based on the command line options.
    #
    eval itk_initialize $args

    #
    # Show the current date.
    #
    show now
}

# ------------------------------------------------------------------
#                           DESTRUCTOR
# ------------------------------------------------------------------
itcl::body iwidgets::Spindate::destructor {} {
    if {$_repack != ""} {after cancel $_repack}
}

# ------------------------------------------------------------------
#                             OPTIONS
# ------------------------------------------------------------------

# ------------------------------------------------------------------
# OPTION: -labelpos
#
# Specifies the location of all 3 spinners' labels.
# ------------------------------------------------------------------
itcl::configbody iwidgets::Spindate::labelpos {
    switch $itk_option(-labelpos) {
	n {
	    $itk_component(month) configure -labelpos n
	    $itk_component(day) configure -labelpos n
	    $itk_component(year) configure -labelpos n
	    
	    #
	    # Un-align labels
	    #
	    $itk_component(month) configure -labelmargin 1
	    $itk_component(day) configure -labelmargin 1
	    $itk_component(year) configure -labelmargin 1 
	}
	
	s {
	    $itk_component(month) configure -labelpos s
	    $itk_component(day) configure -labelpos s
	    $itk_component(year) configure -labelpos s
	    
	    #
	    # Un-align labels
	    #
	    $itk_component(month) configure -labelmargin 1
	    $itk_component(day) configure -labelmargin 1
	    $itk_component(year) configure -labelmargin 1 
	}
	
	w {
	    $itk_component(month) configure -labelpos w
	    $itk_component(day) configure -labelpos w
	    $itk_component(year) configure -labelpos w
	}
	
	e {
	    $itk_component(month) configure -labelpos e
	    $itk_component(day) configure -labelpos e
	    $itk_component(year) configure -labelpos e
	    
	    #
	    # Un-align labels
	    #
	    $itk_component(month) configure -labelmargin 1
	    $itk_component(day) configure -labelmargin 1
	    $itk_component(year) configure -labelmargin 1 
	}
	
	default {
	    error "bad labelpos option \"$itk_option(-labelpos)\",\
		    should be n, s, w or e" 
	}
    }

    _packDate
}

# ------------------------------------------------------------------
# OPTION: -orient
# 
# Specifies the orientation of the 3 spinners for Month, Day 
# and year.
# ------------------------------------------------------------------
itcl::configbody iwidgets::Spindate::orient {
    _packDate
}

# ------------------------------------------------------------------
# OPTION: -monthon
# 
# Specifies whether or not to display the month spinner.
# ------------------------------------------------------------------
itcl::configbody iwidgets::Spindate::monthon {
    _packDate
}

# ------------------------------------------------------------------
# OPTION: -dayon
# 
# Specifies whether or not to display the day spinner.
# ------------------------------------------------------------------
itcl::configbody iwidgets::Spindate::dayon {
    _packDate
}

# ------------------------------------------------------------------
# OPTION: -yearon
# 
# Specifies whether or not to display the year spinner.
# ------------------------------------------------------------------
itcl::configbody iwidgets::Spindate::yearon {
    _packDate
}

# ------------------------------------------------------------------
# OPTION: -datemargin
# 
# Specifies the margin space between the month and day spinners 
# and the day and year spinners. 
# ------------------------------------------------------------------
itcl::configbody iwidgets::Spindate::datemargin {
    _packDate
}

# ------------------------------------------------------------------
# OPTION: -yeardigits
#
# Number of digits for year display, 2 or 4 
# ------------------------------------------------------------------
itcl::configbody iwidgets::Spindate::yeardigits {
    set clicks [clock seconds]

    switch $itk_option(-yeardigits) {
	"2" {
	    $itk_component(year) configure -width 2 -fixed 2
	    $itk_component(year) clear
	    $itk_component(year) insert 0 [clock format $clicks -format "%y"]
	    set _yearFormatStr "%y"
	}
	
	"4" {
	    $itk_component(year) configure -width 4 -fixed 4
	    $itk_component(year) clear
	    $itk_component(year) insert 0 [clock format $clicks -format "%Y"]
	    set _yearFormatStr "%Y"
	}
	
	default {
	    error "bad yeardigits option \"$itk_option(-yeardigits)\",\
		    should be 2 or 4"
	}
    } 
}

# ------------------------------------------------------------------
# OPTION: -monthformat
#
# Format of month display, integers (1-12) or brief strings (Jan - 
# Dec), or full strings (January - December).
# ------------------------------------------------------------------
itcl::configbody iwidgets::Spindate::monthformat {
    set clicks [clock seconds]

    if {$itk_option(-monthformat) == "brief"} {
	$itk_component(month) configure -width 3 -fixed 3
	$itk_component(month) delete 0 end
	$itk_component(month) insert 0 [clock format $clicks -format "%b"]
	set _monthFormatStr "%b"
	
    } elseif {$itk_option(-monthformat) == "full"} {
	$itk_component(month) configure -width 9 -fixed 9
	$itk_component(month) delete 0 end
	$itk_component(month) insert 0 [clock format $clicks -format "%B"]
	set _monthFormatStr "%B"
	
    } elseif {$itk_option(-monthformat) == "integer"} {
	$itk_component(month) configure -width 2 -fixed 2 
	$itk_component(month) delete 0 end
	$itk_component(month) insert 0 [clock format $clicks -format "%m"]
	set _monthFormatStr "%m"
	
    } else {
	error "bad monthformat option\
		\"$itk_option(-monthformat)\", should be\
		\"integer\", \"brief\" or \"full\""
    }
}

# ------------------------------------------------------------------
#                            METHODS
# ------------------------------------------------------------------

# ------------------------------------------------------------------
# METHOD: get ?format?
#
# Return the current contents of the spindate widget in one of 
# two formats string or as an integer clock value using the -string 
# and -clicks options respectively.  The default is by string.  
# Reference the clock command for more information on obtaining dates 
# and their formats.
# ------------------------------------------------------------------
itcl::body iwidgets::Spindate::get {{format "-string"}} { 
    set month [$itk_component(month) get]
    set day [$itk_component(day) get]
    set year [$itk_component(year) get]

    if {[regexp {[0-9]+} $month]} {
	set datestr "$month/$day/$year"
    } else {
	set datestr "$day $month $year"
    }

    switch -- $format {
	"-string" {
	    return $datestr
	}
	"-clicks" {
	    return [clock scan $datestr]
	}
	default {
	    error "bad format option \"$format\":\
                   should be -string or -clicks"
	}
    }
}

# ------------------------------------------------------------------
# PUBLIC METHOD: show date
#
# Changes the currently displayed date to be that of the date 
# argument.  The date may be specified either as a string or an
# integer clock value.  Reference the clock command for more 
# information on obtaining dates and their formats.
# ------------------------------------------------------------------
itcl::body iwidgets::Spindate::show {{date "now"}} {
    #
    # Convert the date to a clock clicks value.
    #
    if {$date == "now"} {
	set seconds [clock seconds]
    } else {
	if {[catch {clock format $date}] == 0} {
	    set seconds $date
	} elseif {[catch {set seconds [clock scan $date]}] != 0} {
	    error "bad date: \"$date\", must be a valid date\
               string, clock clicks value or the keyword now"
	}
    }

    #
    # Display the month based on the -monthformat option.
    #
    switch $itk_option(-monthformat) {
	"brief" {
	    $itk_component(month) delete 0 end
	    $itk_component(month) insert 0 [clock format $seconds -format "%b"]
	}	
	"full" {
	    $itk_component(month) delete 0 end
	    $itk_component(month) insert 0 [clock format $seconds -format "%B"]
	}	
	"integer" {
	    $itk_component(month) delete 0 end
	    $itk_component(month) insert 0 [clock format $seconds -format "%m"]
	}
    }

    #
    # Display the day.
    #
    $itk_component(day) delete 0 end
    $itk_component(day) insert end [clock format $seconds -format "%d"]

    #
    # Display the year based on the -yeardigits option.
    #
    switch $itk_option(-yeardigits) {
	"2" {
	    $itk_component(year) delete 0 end
	    $itk_component(year) insert 0 [clock format $seconds -format "%y"]
	}
	
	"4" {
	    $itk_component(year) delete 0 end
	    $itk_component(year) insert 0 [clock format $seconds -format "%Y"]
	}
    }

    return
}

# ----------------------------------------------------------------
# PRIVATE METHOD: _spinMonth direction
#
# Increment or decrement month value.  We need to get the values
# for all three fields so we can make sure the day agrees with
# the month.  Should the current day be greater than the day for
# the spun month, then the day is set to the last day for the
# new month.
# ----------------------------------------------------------------
itcl::body iwidgets::Spindate::_spinMonth {direction} {
    set month [$itk_component(month) get]
    set day [$itk_component(day) get]
    set year [$itk_component(year) get]

    #
    # There appears to be a bug in the Tcl clock command in that it
    # can't scan a date like "12/31/1999 1 month" or any other date with
    # a year above 2000, but it has no problem scanning "07/01/1998 1 month".
    # So, we're going to play a game and increment by days until this
    # is fixed in Tcl.
    #
    if {$direction == 1} {
	set incrdays 32
	set day 01
    } else {
	set incrdays -28
	set day 28
    }

    if {[regexp {[0-9]+} $month]} {
	set clicks [clock scan "$month/$day/$year $incrdays day"]
    } else {
	set clicks [clock scan "$day $month $year $incrdays day"]
    }

    $itk_component(month) clear
    $itk_component(month) insert 0 \
	[clock format $clicks -format $_monthFormatStr]

    set currday [$itk_component(day) get]
    set lastday [_lastDay [$itk_component(month) get] $year]

    if {$currday > $lastday} {
	$itk_component(day) clear
	$itk_component(day) insert end $lastday
    }
}

# ----------------------------------------------------------------
# PRIVATE METHOD: _spinDay direction
#
# Increment or decrement day value.  If the previous day was the
# first, then set the new day to the last day for the current
# month.  If it was the last day of the month, change it to the
# first.  Otherwise, spin it to the next day.
# ----------------------------------------------------------------
itcl::body iwidgets::Spindate::_spinDay {direction} {
    set month [$itk_component(month) get]
    set day [$itk_component(day) get]
    set year [$itk_component(year) get]
    set lastday [_lastDay $month $year]
    set currclicks [get -clicks]

    $itk_component(day) delete 0 end

    if {(($day == "01") || ($day == "1")) && ($direction == -1)} {
	$itk_component(day) insert 0 $lastday
        return
    }

    if {($day == $lastday) && ($direction == 1)} {
	$itk_component(day) insert 0 "01"
        return
    }

    set clicks [clock scan "$direction day" -base $currclicks]
    $itk_component(day) insert 0 [clock format $clicks -format "%d"]
}

# ------------------------------------------------------------------
# PRIVATE METHOD: _packDate when
#
# Pack the components of the date spinner.  If "when" is "now", the 
# change is applied immediately.  If it is "later" or it is not 
# specified, then the change is applied later, when the application 
# is idle.
# ------------------------------------------------------------------
itcl::body iwidgets::Spindate::_packDate {{when later}} {
    if {$when == "later"} {
	if {$_repack == ""} {
	    set _repack [after idle [itcl::code $this _packDate now]]
	}
	return
    } elseif {$when != "now"} {
	error "bad option \"$when\": should be now or later"
    }

    #
    # Turn off the minsizes for all the rows and columns.  
    #
    for {set i 0} {$i < 5} {incr i} {
	grid rowconfigure $_interior $i -minsize 0
	grid columnconfigure $_interior $i -minsize 0
    }

    set _repack ""

    #
    # Based on the orientation, use the grid to place the components into
    # the proper rows and columns.
    #
    switch $itk_option(-orient) {
	vertical {
	    set row -1
	    
	    if {$itk_option(-monthon)} {
		grid $itk_component(month) -row [incr row] -column 0 \
		    -sticky nsew 
	    } else {
		grid forget $itk_component(month)
	    }
	    
	    if {$itk_option(-dayon)} {
		if {$itk_option(-dayon)} {
		    grid rowconfigure $_interior [incr row] \
			-minsize $itk_option(-datemargin)
		}

		grid $itk_component(day) -row [incr row] -column 0 \
		    -sticky nsew 
	    } else {
		grid forget $itk_component(day)
	    }
	    
	    if {$itk_option(-yearon)} {
		if {$itk_option(-monthon) || $itk_option(-dayon)} {
		    grid rowconfigure $_interior [incr row] \
			-minsize $itk_option(-datemargin)
		}

		grid $itk_component(year) -row [incr row] -column 0 \
		    -sticky nsew 
	    } else {
		grid forget $itk_component(year)
	    }
	    
	    if {$itk_option(-labelpos) == "w"} {
		iwidgets::Labeledwidget::alignlabels $itk_component(month) \
			$itk_component(day) $itk_component(year)
	    }
	}
	
	horizontal {
	    set column -1

	    if {$itk_option(-monthon)} {
		grid $itk_component(month) -row 0 -column [incr column] \
		    -sticky nsew 
	    } else {
		grid forget $itk_component(month)
	    }
	    
	    if {$itk_option(-dayon)} {
		if {$itk_option(-monthon)} {
		    grid columnconfigure $_interior [incr column] \
			-minsize $itk_option(-datemargin)
		}

		grid $itk_component(day) -row 0 -column [incr column] \
		    -sticky nsew 
	    } else {
		grid forget $itk_component(day)
	    }
	    
	    if {$itk_option(-yearon)} {
		if {$itk_option(-monthon) || $itk_option(-dayon)} {
		    grid columnconfigure $_interior [incr column] \
			-minsize $itk_option(-datemargin)
		}

		grid $itk_component(year) -row 0 -column [incr column] \
		    -sticky nsew 
	    } else {
		grid forget $itk_component(year)
	    }
	    
	    #
	    # Un-align labels.
	    #
	    $itk_component(month) configure -labelmargin 1
	    $itk_component(day) configure -labelmargin 1
	    $itk_component(year) configure -labelmargin 1
	}
	
	default {
	    error "bad orient option \"$itk_option(-orient)\", should\
		    be \"vertical\" or \"horizontal\""
	} 
    }
} 

# ------------------------------------------------------------------
# PRIVATE METHOD: _lastDay month year
#
# Internal method which determines the last day of the month for
# the given month and year.  We start at 28 and go forward till
# we fail.  Crude but effective.
# ------------------------------------------------------------------
itcl::body iwidgets::Spindate::_lastDay {month year} {
    set lastone 28

    for {set lastone 28} {$lastone < 32} {incr lastone} {
	if {[regexp {[0-9]+} $month]} {
	  if {[catch {clock scan "$month/[expr {$lastone + 1}]/$year"}] != 0} {
		return $lastone
	  }
	} else {
	  if {[catch {clock scan "[expr {$lastone + 1}] $month $year"}] != 0} {
		return $lastone
	  }
	}
    }
}
