RLE (2014-03-21): On the page tkAdsAdder - to enter TV ad start and end times, and grand total uniquename presented a tool he had designed to allow him to time the amount of commercial (break) time during television shows that he watches.
At the bottom of the page, I suggested an alternate GUI for the tool. uniquename responded that the suggestion was a "Nice idea." and further suggested I code up an example and post it to the Wiki. Well, here is the alternate GUI.
There are a couple shifts from what I had initially outlined on the tkAdsAdder - to enter TV ad start and end times, and grand total page.
As I was finding time here and there to build out the script, uniquename added a further edit regarding the potential need to edit already created records as one is timing a sequence of break intervals. Since I had not posted anything yet, I went ahead and also included edit, insert, and delete functions for the interval values present in the ttk::treeview table. The edit, insert, and delete functions are non-modal, they do not block an existing interval timer that might be running when a user decides to edit, insert, or delete a record.
I also included a running stop-watch like view when the script is timing an interval. I had not suggested that feature on uniquename's page.
Note that because this code uses clock seconds for gathering times, that while raw intervals and interval totals will be accurate, the physical date and time values will only be as accurate as the system clock that Tcl uses to aquire clock seconds values.
A picture of the main GUI:
A picture of the edit/insert GUI (this shows an edit example):
#!/usr/bin/env wish # Copyright 2014 - Richard Ellis # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) # any later version. # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. # You should have received a copy of the GNU General Public License along # with this program. If not, see <http://www.gnu.org/licenses/>. # =========================================================================== # # Note - the code below assumes that the system clock and local timezone are # set accurately # =========================================================================== # # start-timer is fired from the main control button at the start of a timing # segment - state is maintained by reconfiguring the -command attached to # the button proc start-timer { button } { set start_seconds [ clock seconds ] $button configure -text "Stop (0s elapsed)" \ -command [ list stop-timer $button $start_seconds ] # also setup a ticking stop-watch effect on the button after 500 [ list stop-watch $button $start_seconds ] } # =========================================================================== # # stop-timer is fired from the main contorl button at the end of a timing # segment - it stores the interval in the global log, resets the state of # the button, and turns off the stop-watch timer proc stop-timer { button start_seconds } { after cancel [ list stop-watch $button $start_seconds ] set stop_seconds [ clock seconds ] $button configure -text Start \ -command [ list start-timer $button ] # skip adding the interval to the log if the elapsed amount is zero if { ($stop_seconds - $start_seconds) < 1 } { return } add-entry-to-log $start_seconds $stop_seconds } # =========================================================================== # # stop-watch is called from an [after] event to create a stop-watch count up # timer effect in the main button label proc stop-watch { button start_seconds } { $button configure -text "Stop ([ hms [ expr { [ clock seconds ] - $start_seconds } ] ] elapsed)" after 1000 [ list stop-watch $button $start_seconds ] } # =========================================================================== # # add-entry-to-log adds an interval to the ttk::treeview widget and updates # the total interval label proc add-entry-to-log { start_seconds stop_seconds } { set elapsed [ expr { $stop_seconds - $start_seconds } ] .log insert {} end \ -values [ list [ clock format $start_seconds -format %H:%M:%S ] \ [ hms $elapsed ] \ $start_seconds \ $stop_seconds ] .log see [ lindex [ .log children {} ] end ] update-total .log } # =========================================================================== # # update total encapsulates the command details necessary to update the # total label proc update-total { log } { .total configure -text "Total break time is [ hms [ compute-total $log ] ]" } # =========================================================================== # # hms converts an integer number of seconds into a human readable string # listing the total hours, minutes, seconds contained in the integer - used # for display and reporting purposes proc hms { seconds } { set hrs [ expr { $seconds / 3600 } ] set min [ expr { ($seconds - ($hrs * 3600)) / 60 } ] set sec [ expr { $seconds % 60 } ] return [ expr { $hrs > 0 ? "${hrs}h " : "" } ][ expr { $min > 0 ? "${min}m " : "" } ]${sec}s } # =========================================================================== # # compute-total calculates the total time contained in all intervals in the # data parameter proc compute-total { log } { set total 0 foreach item [ $log children {} ] { set temp [ $log set $item ] dict with temp { incr total [ expr { $stop_seconds - $start_seconds } ] } } return $total } # =========================================================================== # # save-data prompts for a filename and saves the contents of the global list # into the given file # the save format is human lines of readable ISO8601 format date-times, two # date-times per line proc save-data {} { set filename [ tk_getSaveFile -parent . ] if { $filename eq "" } { return } if { [ catch {set fp [ open $filename {WRONLY CREAT TRUNC} ]} e1 e2 ] } { tk_dialog .error "Error" "Error Saving Data\n$e1\n$e2" error 0 Dismiss return } set fmt %Y-%m-%dT%H:%M:%S%z foreach item [ .log children {} ] { set temp [ .log set $item ] dict with temp { puts $fp [ list [ clock format $start_seconds -format $fmt ] \ [ clock format $stop_seconds -format $fmt ] ] } } close $fp } # load-data loads the contents of a file into the global variable - then # attempts to update the total and text widgets based upon the contents - if # the contents generate an error an error dialog is presented proc load-data {} { set filename [ tk_getOpenFile -parent . -multiple false ] if { $filename eq "" } { return } if { [ catch {set fp [ open $filename {RDONLY} ]} e1 e2 ] } { tk_dialog .error "Error" "Error Opening File\n$e1\n$e2" error 0 Dismiss return } .log delete [ .log children {} ] event generate .log <<TreeviewSelect>> ; # bug in treeview - deleting selected item does not fire <<TreeviewSelect>> event set temp [ list ] set fmt %Y-%m-%dT%H:%M:%S%z if { [ catch { foreach item [ split [ read -nonewline $fp ] \n ] { lassign $item start_seconds stop_seconds set start_seconds [ clock scan $start_seconds -format $fmt ] set stop_seconds [ clock scan $stop_seconds -format $fmt ] add-entry-to-log $start_seconds $stop_seconds } } e1 e2 ] } { tk_dialog .error "Error" "Error Loading Data\n$e1\n$e2" error 0 Dismiss return } close $fp } # =========================================================================== # proc unique-toplevel {} { set i 0 while { [ catch {set top [ toplevel .report[ incr i] ]} ] } {} return $top } # =========================================================================== # # show-report pops up a new window containing a text widget that displays a # formatted report of the set of intervals contained in the treeview widget proc show-report {} { set top [ unique-toplevel ] # build the report window set text [ text $top.text ] set vsb [ scrollbar $top.vsb -orient vertical -command [ list $text yview ] ] $text configure -yscrollcommand [ list $vsb set ] grid $text $vsb -sticky news grid rowconfigure $top 0 -weight 1 grid columnconfigure $top 0 -weight 1 # test for any data available - do the right thing if no data set items [ .log children {} ] if { 0 == [ llength $items ] } { wm title $top "No Data - No Report" $text insert end "No data available - no report generated." return } # build the report $text insert end [ set temp "Ad-Time Report for [ clock format [ dict get [ .log set [ lindex $items 0 ] ] start_seconds ] -format "%B %d, %Y" ]" ]\n\n wm title $top $temp $text insert end "Ad\nSegment\n" $text insert end "Number Start-time End-time Length\n" $text insert end "------- ---------- ---------- ----------\n" set fmt "%H:%M:%S" set idx 0 foreach item $items { incr idx set temp [ .log set $item ] dict with temp { $text insert end [ format "%7d %-10s %-10s %10s\n" \ $idx \ [ clock format $start_seconds -format $fmt ] \ [ clock format $stop_seconds -format $fmt ] \ [ hms [ expr { $stop_seconds - $start_seconds } ] ] ] } } $text insert end "\n\nTotal Ad Time: [ hms [ compute-total .log ] ]" } # =========================================================================== # proc edit-entry {} { foreach item [ .log selection ] { edit-insert-window [ .log set $item ] $item } } # =========================================================================== # proc insert-entry {} { edit-insert-window } # =========================================================================== # proc edit-insert-window { {data {}} {treeid {}} } { set top [ unique-toplevel ] set state [ expr { 0 == [ dict size $data ] ? "Insert" : "Edit" } ] wm title $top "$state Entry" set fmt "%Y-%m-%d %H:%M:%S" # show the original data from the selected entry if { $state eq "Edit" } { dict with data { label $top.l1 -text "Old Data:" -anchor w label $top.l2 -text "Start: " -anchor e label $top.l3 -text [ clock format $start_seconds -format $fmt ] -anchor w label $top.l4 -text "Stop: " -anchor e label $top.l5 -text [ clock format $stop_seconds -format $fmt ] -anchor w grid $top.l1 - -sticky news grid $top.l2 $top.l3 -sticky news grid $top.l4 $top.l5 -sticky news } } # An instructional label label $top.info -text "Date/time format: YYYY-MM-DD HH:MM:SS\n(24 hour clock)" \ -justify left -anchor w grid $top.info - -sticky news # edit entries set temp [ expr { $state eq "Insert" ? "New Data: " : "Replace With: " } ] label $top.edit -text $temp -anchor w label $top.e1 -text "Start: " -anchor e entry $top.start -justify left label $top.e2 -text "Stop: " -anchor e entry $top.stop -justify left grid $top.edit - -sticky news grid $top.e1 $top.start -sticky news grid $top.e2 $top.stop -sticky news if { $state eq "Edit" } { dict with data { $top.start insert end [ clock format $start_seconds -format $fmt ] $top.stop insert end [ clock format $stop_seconds -format $fmt ] } } # control button frame set f [ frame $top.buttons ] button $f.cancel -text Cancel -command [ list destroy $top ] button $f.save -text Save -command [ list validate-edit $treeid $top ] pack $f.save $f.cancel -side right pack $f.cancel -side right -padx {5m 5m} grid $f - -sticky news -pady {2m 0} } # =========================================================================== # proc validate-edit { treeid top } { # test that the entry values are in the proper format - change background # color if not set fmt "%Y-%m-%d %H:%M:%S" set error false foreach item {start stop} { if { [ catch { set ${item}_seconds [ clock scan [ $top.$item get ] -format $fmt ] } ] } { set error true $top.$item configure -background yellow } } if { $error } { return } if { $stop_seconds < $start_seconds } { # stop timestamp should never be smaller than start timestamp $top.start configure -background yellow $top.stop configure -background yellow return } # Because the edit dialogs are mode-less, it is possible to edit an # entry and while the edit window is open, delete that same entry. If # that happens, just convert save into an insert new operation instead. if { ($treeid ne "") && [ .log exists $treeid ] } { .log set $treeid time_value [ clock format $start_seconds -format %H:%M:%S ] .log set $treeid duration [ hms [ expr { $stop_seconds - $start_seconds } ] ] .log set $treeid start_seconds $start_seconds .log set $treeid stop_seconds $stop_seconds update-total .log } else { add-entry-to-log $start_seconds $stop_seconds } # sort the .log entries into increasing order # this is adapted from the code at https://wiki.tcl-lang.org/20930 set data {} foreach row [ .log children {} ] { lappend data [ list [ .log set $row start_seconds ] $row ] } set r -1 # Now reshuffle the rows into the sorted order foreach info [ lsort -index 0 $data ] { .log move [ lindex $info 1 ] {} [ incr r ] } destroy $top } # =========================================================================== # # now initialize the GUI # points sized fonts instead of pixel sized fonts foreach font [ font names ] { font configure $font -size [ expr { abs([ font configure $font -size ]) } ] } # a large size font for the main control button font create LargeFont {*}[ font actual TkDefaultFont ] font configure LargeFont -size 26 # the main control button button .timer -text Start -command [ list start-timer .timer ] \ -font LargeFont -height 3 # a label to display the total of all captured intervals label .total # The ttk::treeview is used as a simple table to display each interval # The start_seconds and stop_seconds columns are used to store the original # [clock seconds] values - i.e., these two columns are the state storage for # this app. ttk::treeview .log -columns {time_value duration start_seconds stop_seconds} \ -displaycolumns {time_value duration} -show headings -yscrollcommand [ list .vsb set ] .log heading time_value -text "Start Time" .log heading duration -text "Duration" .log column duration -anchor e scrollbar .vsb -orient vertical -command [ list .log yview ] grid .timer - -sticky news grid .total - -sticky news grid .log .vsb -sticky news grid rowconfigure . 1 -weight 1 grid columnconfigure . 0 -weight 1 # a menubar for the extra controls menu .menubar .menubar add cascade -label File -menu .menubar.file .menubar add cascade -label Edit -menu .menubar.edit # File ... menu menu .menubar.file .menubar.file add command -label "Report" -command [ list show-report ] .menubar.file add command -label "Load Data ..." -command [ list load-data ] .menubar.file add command -label "Save Data ..." -command [ list save-data ] .menubar.file add separator .menubar.file add command -label "Clear" -command { .log delete [ .log children {} ] ; .total configure -text "" event generate .log <<TreeviewSelect>> } .menubar.file add separator .menubar.file add command -label "Exit" -command exit # Edit ... menu menu .menubar.edit .menubar.edit add command -label "Edit ..." -state disabled -command edit-entry .menubar.edit add command -label "Insert New ..." -command insert-entry .menubar.edit add separator .menubar.edit add command -label "Delete" -state disabled -command { .log delete [ .log selection ] event generate .log <<TreeviewSelect>> update-total .log } . configure -menu .menubar # set up a binding to automatically enable/disable the edit menu based upon # what is selected in the treeview widget bind .log <<TreeviewSelect>> { ::apply { {log} { set mode [ expr { 0 == [ llength [ $log selection ] ] ? "disabled" : "normal" } ] foreach item {Edit* Delete*} { .menubar.edit entryconfigure $item -state $mode } } } %W } wm title . "Ad Break Timer"