| Category: | GUI Programming |
| Author/Contact Info | rinceWind |
| Description: | This is my first Tk application. It can be used by contractors and consultants, out in the field for keeping track of the number of hours worked on various activities.
By defining the activities "White" and "Black", this program can be used as a chess clock. |
#!/usr/bin/perl5 -w
#
# Activity timer application. Doubles up as a chess clock.
#
use strict;
use Tk;
use POSIX qw(mktime strftime);
use Time::HiRes qw(gettimeofday tv_interval);
use File::Spec::Functions qw(splitpath);
#
# Main scoped variables (yuck!) use to control state of activity timer
#
my $current; # Index of current activity or undef
my $curr_timer; # Current timer repeat widget
my @activity; # AoH - holds info about the activities:
#
# $activity[$i]{frame} Frame widget
# {legend} Button legend
# {timstr} Time as string
# {button} Button widget
# {timlab} Time label widget
# {start} Array of start times in
# Time::HiRes form
# {end} Array of finish times in
# Time::HiRes form
# {elapsed} Floating seconds from
# previous run
# Configure main menu bar
my $main = MainWindow->new;
$main->configure(-menu => my $menubar = $main->Menu);
my $file = $menubar->cascade(-label => "File",
-underline => 0,
-tearoff => 0);
$file->command(-label => '~Open', -command => [\&file_dialog, 'open'])
+;
$file->command(-label => '~Save', -command => [\&save]);
$file->command(-label => 'Save ~As', -command => [\&file_dialog, 'save
+']);
my $ori = $file->cascade(-label => 'O~rientation', -tearoff => 0);
$file->command(-label => '~Exit', -command => [\&save, 'exit']);
$file->command(-label => '~Quit', -command => [\&exit]);
$menubar->cascade(qw/-label Button -underline 0 -tearoff 0 -menuitems/
+ =>
[
[command => '~New', -command => [\&activity_configure]],
[command => 'Ne~xt', -command => [sub {&activate($current+1)}
+]],
[command => '~Stop', -command => [\&deactivate]],
]);
# More main scoped variables, This time for frame orientation:
my $orient = 0; # 0=vertical, 1=horizontal
my $toplevel_dirn = 'top';
my $frame_dirn = 'left';
$ori->radiobutton(-label => '~Vertical',
-variable => \$orient,
-value => 0,
-command => [\&render_orientation]);
$ori->radiobutton(-label => '~Horizontal',
-variable => \$orient,
-value => 1,
-command => [\&render_orientation]);
# Now for the main loop
MainLoop;
# --- End of main program
my $filnam; # Current save file - again main program scoped
sub save {
(unshift @_,'save'),goto &file_dialog if !defined $filnam;
OPEN_TRY:
{
if (!open SAVE,">$filnam") {
# Handle what to do when you cannot save
my $whattodo = $main->Dialog(
-title => "Error during save",
-text => "what to do now?",
-buttons => qw(Abort Retry Cancel));
my $resp = $whattodo->Show;
exit if $resp eq 'Abort';
redo OPEN_TRY if $resp eq 'Retry';
return;
}
}
# File was opened successfully. Save details in .ini format
print SAVE <<END;
[Global]
Orientation=$orient
END
for (0..$#activity) {
my ($legend,$time,$start,$end,$elapsed) =
@{%{$activity[$_]}}{qw(legend timstr start end elapsed)};
# Add up times of current activity
my $tsecs = 0;
for my $i (0..$#$end) {
$tsecs += tv_interval($start->[$i],$end->[$i]);
}
$tsecs += $elapsed if $elapsed;
$tsecs += tv_interval($start->[-1],[gettimeofday]) if defined(
+$current) && ($_ == $current);
print SAVE <<END2;
[Button $legend]
time=$time
elapsed=$tsecs
END2
}
close SAVE;
exit if @_ && ('exit' eq shift);
}
# teardown_activities - remove all Tk widgets associated with activiti
+es
sub teardown_activities {
$activity[$current]{timlab}->afterCancel($curr_timer) if $curr_tim
+er;
$_->{frame} && $_->{frame}->destroy for @activity;
undef $curr_timer;
}
# file_dialog used for open and save-as, and first time save.
sub file_dialog {
my $op = ucfirst shift;
my $dialog_method = "get${op}File";
my @types = (
[".ini files", '.ini'],
["all files", '*'],
);
$filnam = $main->$dialog_method(
-filetypes => \@types);
return if !$filnam;
# We've got a file name, make sure it as extension, default .ini
my ($vol,$dir,$fil) = splitpath($filnam);
$filnam .= '.ini' if $fil !~ /\./;
goto &save if $op eq 'Save';
# not Save, so must be Open.
open INI,$filnam or return undef;
teardown_activities;
@activity = ();
my $i = -1;
# Load activities from ini file
while (<INI>) {
if (/^\[button\s+(\w+)\]/i) {
$activity[++$i] = {legend=>$1};
next;
}
next if !(my ($attr,$val) = /^(\w+)=(.*)/);
$orient = $val if lc($attr) eq 'orientation';
if (lc($attr) eq 'time') {
$activity[$i]{timstr} = $val;
}
if (lc($attr) eq 'elapsed') {
$activity[$i]{elapsed} = $val;
}
}
close INI;
undef $current;
# Draw everything on screen
&render_orientation;
}
# render_orientation - used to flip between horizontal and vertical
# also used to render activities from file
sub render_orientation {
$toplevel_dirn = qw(top left)[$orient];
$frame_dirn = qw(left top)[$orient];
teardown_activities;
# Loop for each activity
for my $i (0..$#activity) {
# Frame
my $newfrm = $main->Frame;
$activity[$i]{frame} = $newfrm;
$newfrm->pack(-side => $toplevel_dirn);
# Button
my $newbut = $newfrm->Button(
-text => $activity[$i]{legend},
-command => [\&activate, $i]);
$newbut->pack(-side => $frame_dirn);
# Bind right mouseclick
$newbut->bind('<3>',sub {&activity_configure($i)});
$activity[$i]{button} = $newbut;
# Time display as label
my $newtim = $newfrm->Label(-text => $activity[$i]{timstr});
$newtim->pack(-side => $frame_dirn);
$activity[$i]{timlab} = $newtim;
# Render current activity in white, and enable timer
if (defined $current and $i == $current) {
$newtim->configure(-background => 'white');
$curr_timer = $newtim->repeat(1000,[\&time_add,$i]);
}
}
}
# Add a new activity (callback from mainmenu=>button=>new)
sub add_entry {
my $legend = shift;
$legend = $legend->get if ref $legend; #Get legend if in a call
+back
# New Frame
my $this = $main->Frame;
$this->pack(-side=>$toplevel_dirn);
my $i= @activity;
# New Button
my $button = $this->Button(-text => $legend,
-command => [\&activate, $i],
);
$button->pack(-side => $frame_dirn);
$button->bind('<3>',sub {&activity_configure($i)});
my $cell_time = "00:00:00";
# New time display as label
my $tim = $this->Label(-text => $cell_time);
$tim->pack(-side => $frame_dirn);
# New entry in activity array
push @activity, {
frame => $this,
legend => $legend,
timstr => $cell_time,
timlab => $tim,
button => $button,
};
}
# Configuration toplevel window for an activity. Called with ($idx) if
+ edit otherwise new
sub activity_configure {
my $idx = shift;
my $legend = '';
$legend = $activity[$idx]{legend} if defined $idx; # Retrieve e
+xisting legend
# New window
my $tl = $main->Toplevel(-title => ($legend || "New")." Button");
# Button legend
$tl->Label(-text => 'Button Legend')->pack;
my $legentry = $tl->Entry(-text => \$legend);
$legentry->pack;
# Special stuff in count frame - not yet implemented
my $count = $tl->Frame;
$count->pack;
$count->Label(-text => 'Alarm at')->pack(-side => 'left');
$count->Entry->pack(-side => 'left');
# Command buttons frame
my $cmds = $tl->Frame;
$cmds->pack;
if (defined $idx) {
# Existing activity - OK Apply Delete Cancel
$cmds->Button(-text => 'OK', -command => sub {&edit_entry($idx
+,$legentry);$tl->destroy;})
->pack(-side => 'left');
$cmds->Button(-text => 'Apply', -command => [\&edit_entry, $id
+x, $legentry])
->pack(-side => 'left');
$cmds->Button(-text => 'Delete', -command => sub {&delete_entr
+y($idx);$tl->destroy;})
->pack(-side => 'left');
} else {
# New activity - OK Apply Cancel
$cmds->Button(-text => 'OK', -command => sub {&add_entry($lege
+ntry);$tl->destroy;})
->pack(-side => 'left');
$cmds->Button(-text => 'Apply', -command => [\&add_entry, $leg
+entry])
->pack(-side => 'left');
}
$cmds->Button(-text => 'Cancel', -command => [$tl => 'destroy'])->
+pack(-side => 'left');
}
# deactivate - turn off clock
sub deactivate {
$activity[$current]{timlab}->configure(-background => 'gray');
$activity[$current]{timlab}->afterCancel($curr_timer);
push @{$activity[$current]{end}}, [gettimeofday];
undef $current;
}
# activate - start timing selected activity
sub activate {
my $idx = shift;
return if defined $current and $current == $idx;
$idx = 0 if $idx >= @activity;
my $tim = $activity[$idx]{timlab};
deactivate if defined $current;
$current = $idx;
# calculcate existing time
my $tsecs = 0;
for my $i (0..$#{$activity[$idx]{end}}) {
$tsecs += tv_interval($activity[$idx]{start}[$i],$activity[$id
+x]{end}[$i]);
}
$tsecs += $activity[$idx]{elapsed} if exists $activity[$idx]{elaps
+ed};
my $millisecs = ($tsecs - int $tsecs) * 1000;
push @{$activity[$idx]{start}}, [gettimeofday];
$tim->configure(-background => 'white');
$tim->after($millisecs,[\&time_add,$idx]); # split seco
+nd remaining
$curr_timer = $tim->repeat(1000,[\&time_add,$idx]); # repeat ti
+mer to update every second
}
# delete an activity
sub delete_entry {
my ($idx) = @_;
deactivate if defined($current) && $idx == $current;
$activity[$idx]{frame}->destroy;
splice @activity,$idx,1;
}
# edit an activity
sub edit_entry {
my ($idx,$leg) = @_;
$activity[$idx]{legend} = $leg->get;
$activity[$idx]{button}->configure(-text => $activity[$idx]{legend
+});
}
# time_add called once a second to update time display
sub time_add {
my $idx = shift;
# Calculate new time
my $since = $activity[$idx]{start}[-1];
my $tsecs = tv_interval($since, [gettimeofday]);
for my $i (0..$#{$activity[$idx]{end}}) {
$tsecs += tv_interval($activity[$idx]{start}[$i],$activity[$id
+x]{end}[$i]);
}
$tsecs += $activity[$idx]{elapsed} if exists $activity[$idx]{elaps
+ed};
# convert to HH:MM:SS format and display
my $secs = $tsecs % 60;
my $mins = int($tsecs / 60);
my $hours = int($mins / 60);
$mins = $mins % 60;
my $tim = strftime("%H:%M:%S",$secs,$mins,$hours,0,0,0,0,0);
$activity[$idx]{timstr} = $tim;
$activity[$idx]{timlab}->configure(-text => $tim);
}
|
|
|
|---|
| Replies are listed 'Best First'. | |
|---|---|
|
Re: Activity timer / chess clock
by jdavidboyd (Friar) on Nov 04, 2002 at 19:57 UTC | |
by rinceWind (Monsignor) on Nov 05, 2002 at 00:10 UTC | |
by jdavidboyd (Friar) on Nov 05, 2002 at 15:08 UTC |