[RLE] (2014-03-21): On the page [http://wiki.tcl.tk/39317%|%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
[http://wiki.tcl.tk/39317%|%tkAdsAdder - to enter TV ad start and end times, and grand total] page.
1. Instead of a [text] widget, I decided to use a [ttk::treeview] widget as a simple table.
1. Instead of storing start and stop [clock seconds] values as a global list of lists, I decided to simply use two hidden columns of the [ttk::treeview] as the "state storage" for the example. I made this change because it eliminated the global list variable for state storage, and simplified handling the "edit an entry" GUI.
1. Instead of writing out the raw [clock seconds] values to a save file, I decided to make the save file a list of lines containing two [http://en.wikipedia.org/wiki/ISO-8601%|%ISO-8601] "combined date and time" values. I made this change because ISO-8601 combined date and time values are human readable (Unix seconds from 1970 values are not so much). This means that editing of saved interval value files requires only a basic text editor.
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:
[Ad Break Timer Screen Shot 1]
A picture of the edit/insert GUI (this shows an edit example):
[Ad Break Timer Screen Shot 2]
======
#!/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 .
# =========================================================================== #
# 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 <> ; # bug in treeview - deleting selected item does not fire <> 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 http://wiki.tcl.tk/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 <>
}
.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 <>
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 <> { ::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"
======
<> GUI | Example