Index: modules/datefield/datefield.man ================================================================== --- modules/datefield/datefield.man +++ modules/datefield/datefield.man @@ -1,25 +1,31 @@ [comment {-*- tcl -*- doctools manpage}] -[manpage_begin datefield n 0.2] +[manpage_begin datefield n 0.3] [see_also clock(n)] [see_also entry(n)] [keywords clock] [keywords date] [keywords dateentry] [keywords entry] [keywords widget] -[copyright {Keith Vetter }] +[copyright {Keith Vetter } and +{Thomas Wunderlich }] [moddesc {Tk datefield widget}] [titledesc {Tk datefield widget}] [category Widget] [require Tk] -[require datefield [opt 0.2]] +[require datefield [opt 0.3]] [description] The [package datefield] package provides the datefield widget which -is an enhanced text entry widget for the purpose of date entry. Only -valid dates of the form MM/DD/YYYY can be entered. +is an enhanced text entry widget for the purpose of date entry. There +are three valid formats for the dates which can be entered: +[list_begin [enum]] +[enum] English form MM/DD/YYYY using [arg {-format "%m/%d/%Y"}] (default) +[enum] German form DD.MM.YYYY using [arg {-format "%d.%m.%Y"}] +[enum] ISO form YYYY-MM-DD using [arg {-format "%Y-%m-%d"}] +[list_end] [para] The datefield widget is, in fact, just an entry widget with specialized bindings. This means all the command and options for an @@ -34,11 +40,22 @@ [list_end] [section OPTIONS] -See the [cmd entry] manual entry for details on all available options. +[list_begin definitions] +[call [arg -format]] + +One of "%m/%d/%Y" (English, default if option left), "%d.%m.%Y" (German), +or "%Y-%m-%d" (ISO). + +[list_end] + +[para] + +See the [cmd entry] manual entry for details on all remaining/available +options. [section EXAMPLE] [example { package require datefield @@ -47,11 +64,11 @@ set now [clock scan $::myDate] set ::myDate2 [clock format $now -format %A] } trace variable myDate w DayOfWeek - ::datefield::datefield .df -textvariable myDate + ::datefield::datefield .df -textvariable myDate -format "%m/%d/%Y" label .l1 -text "Enter a date:" -anchor e label .l2 -text "That date is a:" -anchor e label .l3 -textvariable myDate2 -relief sunken -width 12 grid .l1 .df -sticky ew Index: modules/datefield/datefield.tcl ================================================================== --- modules/datefield/datefield.tcl +++ modules/datefield/datefield.tcl @@ -8,25 +8,31 @@ # Datefield creates an entry widget but with a special binding to KeyPress # (based on Iwidget::datefield) to ensure that the current value is always # a valid date. All normal entry commands and configurations still work. # # Usage: -# ::datefield::datefield .df -background yellow -textvariable myDate +# ::datefield::datefield .df -background yellow -textvariable myDate \ +# -format "%Y-%m-%d" # pack .df # # Bugs: # o won't work if you programmatically put in an invalid date # e.g. .df insert end "abc" will cause it to behave erratically # # Revisions: -# KPV Feb 07, 2002 - initial revision +# KPV Feb 07, 2002 - initial revision +# TW Mar 26, 2017 - support more keys and the mouse wheel +# - add option -format to support 3 date-styles: +# "%d.%m.%Y" (for German) +# "%m/%d/%Y" (for English, standard) +# "%Y-%m-%d" (for ISO) # ##+########################################################################## ############################################################################# package require Tk 8.0 -package provide datefield 0.2 +package provide datefield 0.3 namespace eval ::datefield { namespace export datefield # Have the widget use tile/ttk should it be available. @@ -38,182 +44,413 @@ set entry ttk::entry } proc datefield {w args} { variable entry + variable Format + variable Separator + set i [lsearch $args "-form*"] + if {$i == -1} { # Default English + set Format($w) "%m/%d/%Y" + } else { + set Format($w) [lindex [lreplace $args $i $i] $i] + switch -- $Format($w) { + "%d.%m.%Y" { # German + } + "%m/%d/%Y" { # English + } + "%Y-%m-%d" { # ISO + } + default { # Error + error "ERROR: Unknown value for option -format on datefield $w $args" + } + } + set args [lreplace $args $i $i] + set args [lreplace $args $i $i] + } + set Separator($w) [string range $Format($w) 2 2] eval $entry $w -width 10 -justify center $args - $w insert end [clock format [clock seconds] -format "%m/%d/%Y"] + if {([$w get] eq "") \ + || [catch {clock scan [$w get] -format $Format($w)} base]} { + $w delete 0 end + $w insert end [clock format [clock seconds] -format $Format($w)] + } $w icursor 0 - - bind $w [list ::datefield::KeyPress $w %A %K %s] + bind $w [list ::datefield::KeyPress $w %A %K %s] + bind $w [list ::datefield::MouseWheel $w %D] bind $w break bind $w break bind $w break bind $w break bind $w <2> break - return $w } + + proc Spin {w dir unit code} { + variable Format + + set base [clock scan [$w get] -format $Format($w)] + set new [clock add $base $dir $unit] + set date [clock format $new -format $Format($w)] + set icursor [$w index insert] + $w delete 0 end + $w insert end $date + $w icursor $icursor + return $code + } + + proc MouseWheel {w dir} { + $w selection clear + set Dir [expr {$dir / 120}] + return -code [Spin $w $Dir "day" continue] + } # internal routine for all key presses in the datefield entry widget proc KeyPress {w char sym state} { - set icursor [$w index insert] - - # Handle some non-number characters first - if {$sym == "plus" || $sym == "Up" || \ - $sym == "minus" || $sym == "Down"} { - set dir "1 day" - if {$sym == "minus" || $sym == "Down"} { - set dir "-1 day" - } - set base [clock scan [$w get]] - if {[catch {set new [clock scan $dir -base $base]}] != 0} { - bell - return -code break - } - set date [clock format $new -format "%m/%d/%Y"] - if {[catch {clock scan $date}]} { - bell - return -code break - } - $w delete 0 end - $w insert end $date - $w icursor $icursor - return -code break - } elseif {$sym == "Right" || $sym == "Left" || $sym == "BackSpace" || \ - $sym == "Delete"} { - set dir -1 - if {$sym == "Right"} {set dir 1} - - set icursor [expr {($icursor + 10 + $dir) % 10}] - if {$icursor == 2 || $icursor == 5} {;# Don't land on a slash - set icursor [expr {($icursor + 10 + $dir) % 10}] - } - $w icursor $icursor - return -code break - } elseif {($sym == "Control_L") || ($sym == "Shift_L") || \ - ($sym == "Control_R") || ($sym == "Shift_R")} { - return -code break - } elseif {$sym == "Tab" && $state == 0} {;# Tab key - if {$icursor < 3} { - $w icursor 3 - } elseif {$icursor < 6} { - $w icursor 8 - } else { - return -code continue - } - return -code break - } elseif {$sym == "Tab" && ($state == 1 || $state == 4)} { - if {$icursor > 4} { - $w icursor 3 - } elseif {$icursor > 1} { - $w icursor 0 - } else { - return -code continue - } - return -code break - } - - if {! [regexp {[0-9]} $char]} { ;# Unknown character - bell - return -code break - } - - if {$icursor >= 10} { ;# Can't add beyond end - bell - return -code break - } - foreach {month day year} [split [$w get] "/"] break - - # MONTH SECTION - if {$icursor < 2} { - foreach {m1 m2} [split $month ""] break - set cursor 3 ;# Where to leave the cursor - if {$icursor == 0} { ;# 1st digit of month - if {$char < 2} { - set month "$char$m2" - set cursor 1 - } else { - set month "0$char" - } - if {$month > 12} {set month 10} - if {$month == "00"} {set month "01"} - } else { ;# 2nd digit of month - set month "$m1$char" - if {$month > 12} {set month "0$char"} - if {$month == "00"} { - bell - return -code break - } - } - $w delete 0 2 - $w insert 0 $month - # Validate the day of the month - if {$day > [set endday [lastDay $month $year]]} { - $w delete 3 5 - $w insert 3 $endday - } - $w icursor $cursor - - return -code break - } - # DAY SECTION - if {$icursor < 5} { ;# DAY - set endday [lastDay $month $year] - foreach {d1 d2} [split $day ""] break - set cursor 6 ;# Where to leave the cursor - if {$icursor <= 3} { ;# 1st digit of day - if {$char < 3 || ($char == 3 && $month != "02")} { - set day "$char$d2" - if {$day == "00"} { set day "01" } - if {$day > $endday} {set day $endday} - set cursor 4 - } else { - set day "0$char" - } - } else { ;# 2nd digit of day - set day "$d1$char" - if {$day > $endday || $day == "00"} { - bell - return -code break - } - } - $w delete 3 5 - $w insert 3 $day - $w icursor $cursor - return -code break - } - - # YEAR SECTION - set y1 [lindex [split $year ""] 0] - if {$icursor < 7} { ;# 1st digit of year - if {$char != "1" && $char != "2"} { - bell - return -code break - } - if {$char != $y1} { ;# Different century - set y 1999 - if {$char == "2"} {set y 2000 } - $w delete 6 end - $w insert end $y - } - $w icursor 7 - return -code break - } - $w delete $icursor - $w insert $icursor $char - if {[catch {clock scan [$w get]}] != 0} {;# Validate the year - $w delete 6 end - $w insert end $year ;# Put back in the old year - $w icursor $icursor - bell - return -code break - } - return -code break - } - # internal routine that returns the last valid day of a given month and year - proc lastDay {month year} { - set days [clock format [clock scan "+1 month -1 day" \ - -base [clock scan "$month/01/$year"]] -format %d] + variable Format + variable Separator + + proc Move {w dir} { + variable Format + + set icursor [$w index insert] + set icursor [expr {($icursor + 10 + $dir) % 10}] + if {$Format($w) ne "%Y-%m-%d"} { # English or German + if {($icursor == 2) || ($icursor == 5)} { # Don't land on a / or . + set icursor [expr {($icursor + 10 + $dir) % 10}] + } + } \ + elseif {($icursor == 4) || ($icursor == 7)} { # ISO # Don't land on a - + set icursor [expr {($icursor + 10 + $dir) % 10}] + } + $w icursor $icursor + } + + set icursor [$w index insert] + $w selection clear + # Handle some non-number characters first + switch -exact -- $sym { + "Down" {return -code [Spin $w -1 "day" continue]} + "End" {$w icursor 9; return -code break} + "minus" {return -code [Spin $w -1 "day" break]} + "Next" {return -code [Spin $w -1 "month" continue]} + "plus" {return -code [Spin $w 1 "day" break]} + "Prior" {return -code [Spin $w 1 "month" continue]} + "Up" {return -code [Spin $w 1 "day" continue]} + "BackSpace" - + "Delete" - + "Left" {Move $w -1; return -code break} + "Right" {Move $w 1; return -code break} + "Tab" { + if {$Format($w) ne "%Y-%m-%d"} { # English or German + if {($state & 5) == 0} { # ->| + if {$icursor < 3} { # from 1st to 2nd + $w icursor 3 + } \ + elseif {$icursor < 6} { # from 2nd to 10th-year + $w icursor 8 + } else { # next widget + return -code continue + } + } \ + elseif {$icursor > 4} { # |<- + $w icursor 3 ;# from year to 2nd + } \ + elseif {$icursor > 1} { # from 2nd to 1st + $w icursor 0 + } else { # previous widget + return -code continue + } + } \ + elseif {($state & 5) == 0} { # ->| ISO + if {$icursor < 5} { # from year to month + $w icursor 5 + } \ + elseif {$icursor < 8} { # from month to day + $w icursor 8 + } else { # next widget + return -code continue + } + } \ + elseif {$icursor > 6} { # |<- + $w icursor 5 ;# from day to month + } \ + elseif {$icursor > 2} { # from month to 10th-year + $w icursor 2 + } else { # previous widget + return -code continue + } + return -code break + } + } + if {$char eq ""} { # remaining special keys + return -code continue + } + if {! [regexp -- {[0-9]} $char]} { # Unknown character + bell + return -code break + } + if {$icursor >= 10} { # Can't add beyond end + bell + return -code break + } + switch -- $Separator($w) { + "." { # German + foreach {day month year} [split [$w get] $Separator($w)] break + if {$icursor < 2} { # DAY SECTION + set endday [lastDay $month $year] + foreach {d1 d2} [split $day ""] break + set cursor 3 ;# Where to leave the cursor + if {$icursor == 0} { # 1st digit of day + if {($char < 3) \ + || (($char == 3) && ($month ne "02"))} { + set day "$char$d2" + if {$day eq "00"} {set day "01"} + if {$day > $endday} {set day $endday} + set cursor 1 + } else { + set day "0$char" + } + } else { # 2nd digit of day + set day "$d1$char" + if {($day > $endday) || ($day eq "00")} { + bell + return -code break + } + } + $w delete 0 2 + $w insert 0 $day + $w icursor $cursor + return -code break + } + if {$icursor < 5} { # MONTH SECTION + foreach {m1 m2} [split $month ""] break + set cursor 6 ;# Where to leave the cursor + if {$icursor == 3} { # 1st digit of month + if {$char < 2} { + set month "$char$m2" + set cursor 4 + } else { + set month "0$char" + } + if {$month > 12} {set month "10"} + if {$month eq "00"} {set month "01"} + } else { # 2nd digit of month + set month "$m1$char" + if {$month > 12} {set month "0$char"} + if {$month eq "00"} { + bell + return -code break + } + } + $w delete 3 5 + $w insert 3 $month + # Validate the day of the month + if {$day > [set endday [lastDay $month $year]]} { + $w delete 0 2 + $w insert 0 $endday + } + $w icursor $cursor + return -code break + } + set y1 [string range $year 0 0]; # YEAR SECTION + if {$icursor < 7} { # 1st digit of year + if {($char ne "1") && ($char ne "2")} { + bell + return -code break + } + if {$char != $y1} { # Different century + set y 1999 + if {$char eq "2"} {set y 2000} + $w delete 6 end + $w insert end $y + } + $w icursor 7 + return -code break + } + $w delete $icursor + $w insert $icursor $char + if {[catch {clock scan [$w get] -format $Format($w)}] != 0} { # Validate the year + $w delete 6 end + $w insert end $year ;# Put back in the old year + $w icursor $icursor + bell + } + } + "/" { # English + foreach {month day year} [split [$w get] $Separator($w)] break + if {$icursor < 2} { # MONTH SECTION + foreach {m1 m2} [split $month ""] break + set cursor 3 ;# Where to leave the cursor + if {$icursor == 0} { # 1st digit of month + if {$char < 2} { + set month "$char$m2" + set cursor 1 + } else { + set month "0$char" + } + if {$month > 12} {set month "10"} + if {$month eq "00"} {set month "01"} + } else { # 2nd digit of month + set month "$m1$char" + if {$month > 12} {set month "0$char"} + if {$month eq "00"} { + bell + return -code break + } + } + $w delete 0 2 + $w insert 0 $month + # Validate the day of the month + if {$day > [set endday [lastDay $month $year]]} { + $w delete 3 5 + $w insert 3 $endday + } + $w icursor $cursor + return -code break + } + if {$icursor < 5} { # DAY SECTION + set endday [lastDay $month $year] + foreach {d1 d2} [split $day ""] break + set cursor 6 ;# Where to leave the cursor + if {$icursor == 3} { # 1st digit of day + if {($char < 3) \ + || (($char == 3) && ($month ne "02"))} { + set day "$char$d2" + if {$day eq "00"} {set day "01"} + if {$day > $endday} {set day $endday} + set cursor 4 + } else { + set day "0$char" + } + } else { # 2nd digit of day + set day "$d1$char" + if {($day > $endday) || ($day eq "00")} { + bell + return -code break + } + } + $w delete 3 5 + $w insert 3 $day + $w icursor $cursor + return -code break + } + set y1 [string range $year 0 0]; # YEAR SECTION + if {$icursor < 7} { # 1st digit of year + if {($char ne "1") && ($char ne "2")} { + bell + return -code break + } + if {$char != $y1} { # Different century + set y 1999 + if {$char eq "2"} {set y 2000} + $w delete 6 end + $w insert end $y + } + $w icursor 7 + return -code break + } + $w delete $icursor + $w insert $icursor $char + if {[catch {clock scan [$w get] -format $Format($w)}] != 0} { # Validate the year + $w delete 6 end + $w insert end $year ;# Put back in the old year + $w icursor $icursor + bell + } + } + default { # ISO + foreach {year month day} [split [$w get] $Separator($w)] break + if {$icursor < 4} { # YEAR SECTION + set y1 [string range $year 0 0]; + if {$icursor == 0} { # 1st digit of year + if {($char ne "1") && ($char ne "2")} { + bell + return -code break + } + if {$char != $y1} { # Different century + set y 1999 + if {$char eq "2"} {set y 2000} + $w delete 0 4 + $w insert 0 $y + } + $w icursor 1 + return -code break + } + $w delete $icursor + $w insert $icursor $char + if {[catch {clock scan [$w get] -format $Format($w)}] != 0} { # Validate the year + $w delete 0 4 + $w insert 0 $year ;# Put back in the old year + $w icursor $icursor + bell + } + if {$icursor == 3} { # last digit of year + $w icursor 5 ;# Don't land on a - + } + return -code break + } + if {$icursor < 7} { # MONTH SECTION + foreach {m1 m2} [split $month ""] break + set cursor 8 ;# Where to leave the cursor + if {$icursor == 5} { # 1st digit of month + if {$char < 2} { + set month "$char$m2" + set cursor 6 + } else { + set month "0$char" + } + if {$month > 12} {set month "10"} + if {$month eq "00"} {set month "01"} + } else { # 2nd digit of month + set month "$m1$char" + if {$month > 12} {set month "0$char"} + if {$month eq "00"} { + bell + return -code break + } + } + $w delete 5 7 + $w insert 5 $month + # Validate the day of the month + if {$day > [set endday [lastDay $month $year]]} { + $w delete 8 end + $w insert end $endday + } + $w icursor $cursor + return -code break + } + set endday [lastDay $month $year] ;# DAY SECTION + foreach {d1 d2} [split $day ""] break + set cursor 10 ;# Where to leave the cursor + if {$icursor == 8} { # 1st digit of day + if {($char < 3) \ + || (($char == 3) && ($month ne "02"))} { + set day "$char$d2" + if {$day eq "00"} {set day "01"} + if {$day > $endday} {set day $endday} + set cursor 9 + } else { + set day "0$char" + } + } else { # 2nd digit of day + set day "$d1$char" + if {($day > $endday) || ($day eq "00")} { + bell + return -code break + } + } + $w delete 8 end + $w insert end $day + $w icursor $cursor + } + } + return -code break + } + + # internal routine that returns the last valid day of a given month and year + proc lastDay {month year} { + return [clock format [clock scan "+1 month -1 day" \ + -base [clock scan "$month/01/$year"]] -format %d] } } Index: modules/datefield/pkgIndex.tcl ================================================================== --- modules/datefield/pkgIndex.tcl +++ modules/datefield/pkgIndex.tcl @@ -1,1 +1,1 @@ -package ifneeded datefield 0.2 [list source [file join $dir datefield.tcl]] +package ifneeded datefield 0.3 [list source [file join $dir datefield.tcl]]