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
    Well, usually, a chess clock resets every time you use it. This just keeps incrementing from where you stopped it last time.
    I think you need some code in activate() to reset the timer, (if you want to use it as a chess clock).
    For tracking time spent working on jobs, it makes perfect sense to simply increment each timer when you switch to it.

      Activate() is called whenever the move changes from black to white to black. When white completes a move, he presses the button, and his clock stops, but black's clock starts ticking, carrying on from where it left off.

      In tournament chess, you have, for instance, 2 hours to make the first 30 moves. The clock for white will show the total time used by white to make his moves so far. If this exceeds 02:00:00 before move 30, he has lost the game.

      The idea with using this app as a chess clock, is that once you have set up the buttons for White and Black, you save these into a file, say chessclock.ini. Now, every time you need a chess clock for a match, open chessclock.ini - save as something else if you want, and the times are reset to 00:00:00.

      Hope this helps

        Oops, my mistake.
        I thought that chess timers were more unitary.
        I mean, I thought that you had three (or two) minutes for each move, and every time you pressed the button the timer was reset to 0.
        I learn something new every day! Thanks!