#!/usr/bin/perl # $Header: /home/abhaile/swmcd/.CVS/Perl/ColorWork,v 1.6 2008/11/24 00:29:45 swmcd Exp $ use 5; use strict; use English; use Tk; use Tk::DialogBox; use constant MAGIC => 'CKnt'; use constant NShades => 4; use constant ColorSize => 50; use constant GridSize => 10; use constant X1 => 0; use constant Y1 => 1; use constant X2 => 2; use constant Y2 => 3; use constant LEFT => 0; use constant TOP => 1; use constant RIGHT => 2; use constant BOTTOM => 3; my($Revision) = '$Revision: 1.6 $'; $Revision =~ s/\$Revision: //; $Revision =~ s/\$$//; my $CurrentFill = 'black'; my @Square = (0 , 0, 1 , 0, 1 , 1, 0 , 1); my @Chevron = (0 , 0, 0.5, 0.5, 1 , 0, 1 , 1, 0.5, 1.5, 0 , 1); my @Color = ( [ 'grey0' , 'grey33' , 'grey67' , 'grey100' ], map { my $base = $_; [ map { $base . $_ } (4, 3, 2, 1) ] } qw(red green blue cyan magenta yellow brown) ); my $MW = new MainWindow; my $KP = new KnitPattern $MW, 24, 32, 8, 8; MainLoop; sub KnitPattern::new { my($package, $MW, $rows, $cols, $rowGuides, $colGuides) = @_; my $colorHeight = (@Color+2) * ColorSize; my $gridHeight = ($rows+3) * GridSize; my $height = max($colorHeight, $gridHeight) + 50; my $width = (NShades+1) * ColorSize + ($cols+1) * GridSize; $MW->geometry("${width}x$height+50+50"); my $frame = $MW->Frame; my $canvas = $frame->Canvas; $frame ->pack(-expand => 1, -fill => 'both'); $canvas->pack(-expand => 1, -fill => 'both'); $canvas->repeat(50, [ \&markee_crawl, $canvas ]); my $gridTop = GridSize; my $gridBottom = $gridTop + $rows * GridSize; my $gridLeft = ColorSize * (NShades + 1); my $gridRight = $gridLeft + $cols * GridSize; my @grid = ($gridLeft, $gridTop, $gridRight, $gridBottom); my $KP = { frame => $frame, canvas => $canvas, rows => $rows, cols => $cols, rowGuides => $rowGuides, colGuides => $colGuides, grid => \@grid, pair => [], # row, col => [ square, chevron ] dual => [], # square <=> chevron RC => [], # square, chevron => [ row, col ] stash => {}, }; bless $KP, $package; $KP->make_palette; $KP->make_grid; $KP->make_guides; $KP->make_buttons; $KP } sub KnitPattern::make_palette { my $KP = shift; my $canvas = $KP->{canvas}; for my $r (0..$#Color) { for my $c (0..NShades-1) { my @corners = map { $_ * ColorSize } ($c, $r, $c+1, $r+1); my $color = $Color[$r][$c]; my $id = $canvas->createRectangle(@corners, -fill => $color, -tags => [ $color ]); $canvas->bind($id, '<1>' => sub { handle_color($canvas) } ); } } my $v = ColorSize * (@Color + 0.5); my $h = ColorSize * 0.75; my @square = Polygon(\@Square, ColorSize, $h, $v); my $squareID = $canvas->createPolygon(@square, -fill => 'black', -tags => ['current-fill']); $canvas->bind($squareID, '<1>' => sub { handle_square($canvas) } ); $h = ColorSize * 2.25; my @chevron = Polygon(\@Chevron, ColorSize, $h, $v); my $chevronID = $canvas->createPolygon(@chevron, -fill => 'black', -tags => ['current-fill']); $canvas->bind($chevronID, '<1>' => sub { handle_chevron($canvas) } ); } sub KnitPattern::make_grid { my $KP = shift; my $canvas = $KP->{canvas}; my $rows = $KP->{rows}; my $cols = $KP->{cols}; my $v = $KP->{grid}[TOP]; for my $r (0..$rows-1) { my $h = $KP->{grid}[LEFT]; for my $c (0..$cols-1) { my @square = Polygon(\@Square, GridSize, $h, $v); my $squareID = $canvas->createPolygon(@square, -outline => 'grey', -fill => undef, -tags => [ qw(grid square) ]); my @chevron = Polygon(\@Chevron, GridSize, $h, $v); my $chevronID = $canvas->createPolygon(@chevron, -fill => undef, -state => 'hidden', -tags => [ qw(grid chevron) ]); $KP->{dual}[$squareID ] = $chevronID; $KP->{dual}[$chevronID] = $squareID; $KP->{pair}[$r][$c] = [ $squareID, $chevronID ]; my @rc = ($r, $c); $KP->{RC}[$squareID ] = \@rc; $KP->{RC}[$chevronID] = \@rc; $h += GridSize; } $v += GridSize; } $canvas->CanvasBind('' => sub { grid_mark ($KP) } ); $canvas->CanvasBind('' => sub { grid_drag ($KP) } ); $canvas->CanvasBind('' => sub { grid_release ($KP) } ); $canvas->CanvasBind('' => sub { selection_mark ($KP) } ); $canvas->CanvasBind('' => sub { selection_drag ($KP) } ); $canvas->CanvasBind('' => sub { selection_release($KP) } ); } sub KnitPattern::make_guides { my $KP = shift; my $canvas = $KP->{canvas}; my $rows = $KP->{rows}; my $cols = $KP->{cols}; my $rowGuides = $KP->{rowGuides}; my $colGuides = $KP->{colGuides}; my $left = $KP->{grid}[LEFT]; my $top = $KP->{grid}[TOP]; my $right = $KP->{grid}[RIGHT]; my $bottom = $KP->{grid}[BOTTOM]; my $v = $top; for (my $r=0; $r<=$rows; $r+=$rowGuides) { $canvas->createLine($left, $v, $right, $v, -tags => [ qw(guide) ]); $v += $rowGuides * GridSize; } my $h = $left; for (my $c=0; $c<=$cols; $c+=$colGuides) { $canvas->createLine($h, $top, $h, $bottom, -tags => [ qw(guide) ]); $h += $colGuides * GridSize; } } sub KnitPattern::make_buttons { my $KP = shift; my $frame = $KP->{frame}; $frame->Button(-text => 'Quit', -command => \&Quit )->pack(-side => 'left' , -padx => 20); $frame->Button(-text => 'Load', -command => \&Load )->pack(-side => 'left' , -padx => 5); $frame->Button(-text => 'Save', -command => \&Save )->pack(-side => 'left'); $frame->Button(-text => 'Print', -command => \&Print )->pack(-side => 'left' , -padx => 5); $frame->Button(-text => 'Resize', -command => \&Resize)->pack(-side => 'left' , -padx => 20); $frame->Button(-text => 'Help', -command => \&Help )->pack(-side => 'right', -padx => 20); } ################################################################################ # Left Button - paint # sub grid_mark { my $KP = shift; my $canvas = $KP->{canvas}; my $stash = $KP->{stash }; my($id) = $canvas->find(withtag => 'source'); my($x, $y) = $canvas->XY; if ($id) { if ($canvas->Inside($id, $x, $y)) { $stash->{active} = 'copy'; copy_mark($KP) } else { $stash->{active} = undef; $canvas->delete('source'); } } else { $stash->{active} = 'square'; square_mark($KP); } } sub grid_drag { my $KP = shift; my $canvas = $KP->{canvas}; my $stash = $KP->{stash }; for ($stash->{active}) { /square/ and square_drag($KP); /copy/ and copy_drag ($KP); } } sub grid_release { my $KP = shift; my $canvas = $KP->{canvas}; my $stash = $KP->{stash }; for ($stash->{active}) { /copy/ and copy_release($KP); } $stash->{active} = undef; } sub square_mark { my $KP = shift; my $canvas = $KP->{canvas}; my $stash = $KP->{stash }; my($x, $y) = $canvas->XY; my($id) = $canvas->find(overlapping => $x, $y, $x, $y); my @tags = $canvas->gettags($id); grep { /grid/ } @tags or return; my $oldFill = $canvas->itemcget($id, -fill); my $fill = $oldFill eq $CurrentFill ? undef : $CurrentFill; for my $id ($id, $KP->{dual}[$id]) { $canvas->itemconfigure($id, -fill => $fill); } $stash->{square}{id } = $id; $stash->{square}{fill} = $fill; $KP->{dirty} = 1; } sub square_drag { my $KP = shift; my $canvas = $KP->{canvas}; my $stash = $KP->{stash }; my($x, $y) = $canvas->XY; my($id) = $canvas->find(overlapping => $x, $y, $x, $y); my @tags = $canvas->gettags($id); grep { /grid/ } @tags or return; $id == $stash->{square}{id} and return; for my $id ($id, $KP->{dual}[$id]) { $canvas->itemconfigure($id, -fill => $stash->{square}{fill}); } $stash->{square}{id} = $id; } sub copy_mark { my $KP = shift; my $canvas = $KP->{canvas}; my $stash = $KP->{stash }; $canvas->delete('dest'); my($source) = $canvas->find(withtag => 'source'); $source or return; my $xy = $canvas->XY; $stash->{dest } = $xy; $stash->{margin} = Margin($canvas, $source, $xy); MakeMarkee($canvas, 'dest', $canvas->coords($source)); } sub copy_drag { my $KP = shift; my $canvas = $KP->{canvas}; my $stash = $KP->{stash }; my $coords = $stash->{dest}; my($x1, $y1) = @$coords; my($x2, $y2) = $canvas->XY; ClipMargin($KP, $x2, $y2); my $dx = $x2 - $x1; my $dy = $y2 - $y1; $dx = int($dx / GridSize) * GridSize; $dy = int($dy / GridSize) * GridSize; if ($dx or $dy) { my $ids = $canvas->find(withtag => 'dest'); for my $id (@$ids) { $canvas->move($id, $dx, $dy); $stash->{dest} = [ $x1+$dx, $y1+$dy ]; } } } sub copy_release { my $KP = shift; my $canvas = $KP->{canvas}; my $idSource = $canvas->find(withtag => 'source'); my $idDest = $canvas->find(withtag => 'dest' ); @$idSource and @$idDest or return; copy_grid($KP, $idSource, $idDest); my @coords = $canvas->coords($idDest->[0]); for my $id (@$idSource) { $canvas->coords($id, @coords); } $canvas->delete('dest'); $KP->{dirty} = 1; } sub copy_grid { my($KP, $idSource, $idDest) = @_; my $canvas = $KP->{canvas}; my @sourceCoords = $canvas->coords($idSource->[0]); my @destCoords = $canvas->coords($idDest ->[0]); my($sx1, $sy1, $sx2, $sy2) = @sourceCoords; my($dx1, $dy1, $dx2, $dy2) = @destCoords; my $h = GridSize / 2; my @sIDs = $canvas->find(overlapping => $sx1+$h, $sy1+$h, $sx1+$h, $sy1+$h); my($sID) = grep { $canvas->find(withtag => 'grid' ) } @sIDs; my $rc = $KP->{RC}[$sID]; $rc or return; my($sr0, $sc0) = @$rc; my @dIDs = $canvas->find(overlapping => $dx1+$h, $dy1+$h, $dx1+$h, $dy1+$h); my($dID) = grep { $canvas->find(withtag => 'grid' ) } @dIDs; $rc = $KP->{RC}[$dID]; $rc or return; my($dr0, $dc0) = @$rc; my $nRows = ($sy2 - $sy1) / GridSize; my $nCols = ($sx2 - $sx1) / GridSize; my @fill; for (my $r=0; $r<$nRows; $r++) { my $sr = $sr0 + $r; for (my $c=0; $c<$nCols; $c++) { my $sc = $sc0 + $c; my $sgID = $KP->{pair}[$sr][$sc][0]; $fill[$sr][$sc] = $canvas->itemcget($sgID, -fill); } } for (my $r=0; $r<$nRows; $r++) { my $sr = $sr0 + $r; my $dr = $dr0 + $r; for (my $c=0; $c<$nCols; $c++) { my $sc = $sc0 + $c; my $dc = $dc0 + $c; my $dgIDs = $KP->{pair}[$dr][$dc]; for my $dgID (@$dgIDs) { $canvas->itemconfigure($dgID, -fill => $fill[$sr][$sc]); } } } } ################################################################################ # Right Button - selection # sub selection_mark { my $KP = shift; my $canvas = $KP->{canvas}; my $stash = $KP->{stash }; $canvas->delete('source'); $stash->{source} = $canvas->XY; } sub selection_drag { my $KP = shift; my $canvas = $KP->{canvas}; my $stash = $KP->{stash }; my $coords = $stash->{source}; $coords or return; my($x1, $y1) = @$coords; my($x2, $y2) = $canvas->XY; if ($x1 != $x2 and $y1 != $y2) { MakeMarkee($canvas, 'source', $x1, $y1, $x2, $y2); } } sub selection_release { my $KP = shift; my $canvas = $KP->{canvas}; my $stash = $KP->{stash }; my($id) = $canvas->find(withtag => 'source'); $id or return; my($x1, $y1, $x2, $y2) = $canvas->coords($id); RoundIn($KP, $x1, $y1, $x2, $y2); if ($x1 < $x2 and $y1 < $y2) { MakeMarkee($canvas, 'source', $x1, $y1, $x2, $y2); } else { $canvas->delete('source'); } } sub MakeMarkee { my($canvas, $name, @xyxy) = @_; $canvas->delete($name); my $rb = $canvas->createRectangle(@xyxy, -outline => 'black', -dash => ",", -dashoffset => 0, -tags => ['markee', $name]); my $rw = $canvas->createRectangle(@xyxy, -outline => 'white', -dash => ",", -dashoffset => 4, -tags => ['markee', $name]); } sub markee_crawl { my $canvas = shift; my @ids = $canvas->find(withtag => 'markee'); for my $id (@ids) { my $offset = $canvas->itemcget($id, '-dashoffset'); $offset--; $offset &= 7; $canvas->itemconfigure($id, -dashoffset => $offset); } } ################################################################################ # Pallet & View # sub handle_color { my $canvas = shift; my $ids = $canvas->find(withtag => 'current'); my $id = shift @$ids; $id or return; my @tags = $canvas->gettags($id); ($CurrentFill) = grep { not /current/ } @tags; $canvas->itemconfigure('current-fill', -fill => $CurrentFill); } sub handle_square { my $canvas = shift; $canvas->itemconfigure('square' , -state => 'normal'); $canvas->itemconfigure('chevron', -state => 'hidden'); $canvas->itemconfigure('guide' , -state => 'normal'); } sub handle_chevron { my $canvas = shift; $canvas->itemconfigure('square' , -state => 'hidden'); $canvas->itemconfigure('chevron', -state => 'normal'); $canvas->itemconfigure('guide' , -state => 'hidden'); } ################################################################################ # Buttons # sub Quit { if ($KP->{dirty}) { my $answer = $MW->messageBox(-icon => 'warning', -type => 'YesNo', -title => '', -message => "You have unsaved changes. Exit anyway?"); $answer eq 'Yes' or return; } exit; } sub Load { if ($KP->{dirty}) { my $answer = $MW->messageBox(-icon => 'warning', -type => 'YesNo', -title => '', -message => "You have unsaved changes. Discard them?"); return unless $answer eq 'Yes'; } my @types = (['ColorWork files', '.ckn']); my @filter = $OSNAME eq 'MSWin32' ? () : (-filetypes => \@types); my $file = $MW->getOpenFile(@filter); $file or return; open FILE, $file or do { $MW->messageBox(-title => 'Error', -type => 'OK', -icon => 'error', -message => "Can't open $file: $!"); return; }; my $line = ; my($magic, @size) = split ' ', $line; if ($magic ne MAGIC or not CheckRange(1,1000, @size)) { $MW->messageBox(-title => 'Error', -type => 'OK', -icon => 'error', -message => "$file is not a valid ColorWork file"); return; } if (not SameSize($KP, @size)) { $KP->{frame}->destroy; $KP = new KnitPattern $MW, @size; } my $canvas = $KP->{canvas}; my $cols = $KP->{cols}; my $r = 0; while () { my @colors = split; for my $c (0..$cols-1) { my $ids = $KP->{pair}[$r][$c]; my $color = $colors[$c]; my $fill = $color eq '.' ? undef : $color; for my $id (@$ids) { $canvas->itemconfigure($id, -fill => $fill); } } $r++; } $KP->{dirty} = 0; } sub SameSize { my($KP, $rows, $cols, $rowGuides, $colGuides) = @_; $rows == $KP->{rows} and $cols == $KP->{cols} and $rowGuides == $KP->{rowGuides} and $colGuides == $KP->{colGuides} } sub Save { my $canvas = $KP->{canvas}; my $rows = $KP->{rows}; my $cols = $KP->{cols}; my $rowGuides = $KP->{rowGuides}; my $colGuides = $KP->{colGuides}; my @types = (['ColorWork files', '.ckn']); my @filter = $OSNAME eq 'MSWin32' ? () : (-filetypes => \@types); my $file = $MW->getSaveFile(-defaultextension => '.ckn', @filter); $file or return; open FILE, "> $file" or do { $MW->messageBox(-title => 'Error', -type => 'OK', -icon => 'error', -message => "Can't open $file: $!"); return; }; print FILE MAGIC, " $rows $cols $rowGuides $colGuides\n"; for my $r (0..$rows-1) { for my $c (0..$cols-1) { my $id = $KP->{pair}[$r][$c][0]; my $fill = $canvas->itemcget($id, -fill); my $color = $fill ? $fill : '.'; print FILE "$color "; } print FILE "\n"; } $KP->{dirty} = 0; } sub Print { my $canvas = $KP->{canvas}; my @types = (['Postscript files', '.ps']); my $file = $MW->getSaveFile( # -filetypes => \@types, -defaultextension => '.ps'); $file or return; my $left = $KP->{grid}[LEFT]; my $top = $KP->{grid}[TOP ]; $canvas->postscript(-file => $file, -rotate => 1, -x => $left, -y => $top); } sub Resize { my $rows = $KP->{rows}; my $cols = $KP->{cols}; my $rowGuides = $KP->{rowGuides}; my $colGuides = $KP->{colGuides}; my $dialog = $MW->DialogBox(-title => 'Resize', -default_button => 'OK', -buttons => [qw(OK Cancel)]); my $rh = $dialog->add('Label', -text => 'Grid size'); my $gh = $dialog->add('Label', -text => 'Guideline interval'); my $rl = $dialog->add('Label', -text => 'Rows'); my $rs = $dialog->add('Entry', -text => $rows , -width => 4); my $rg = $dialog->add('Entry', -text => $rowGuides, -width => 4); my $cl = $dialog->add('Label', -text => 'Cols'); my $cs = $dialog->add('Entry', -text => $cols , -width => 4); my $cg = $dialog->add('Entry', -text => $colGuides, -width => 4); Tk::grid('x', $rh, $gh, -padx => 10, -sticky => 'w'); Tk::grid($rl, $rs, $rg, -padx => 10, -sticky => 'w'); Tk::grid($cl, $cs, $cg, -padx => 10, -sticky => 'w'); my $answer = $dialog->Show; ($rows, $cols, $rowGuides, $colGuides) = map { $_->get } ($rs, $cs, $rg, $cg); if ($answer eq 'OK' and not SameSize($KP, $rows, $cols, $rowGuides, $colGuides)) { if ($KP->{dirty}) { my $answer = $MW->messageBox(-icon => 'warning', -type => 'YesNo', -title => '', -message => "You have unsaved changes. Discard them?"); $answer eq 'Yes' or return; } ClipRange(1, 1000, $rows, $cols, $rowGuides, $colGuides); $KP->{frame}->destroy; $KP = new KnitPattern $MW, $rows, $cols, $rowGuides, $colGuides; } } sub Help { my $dialog = $MW->DialogBox(-title => 'Help', -default_button => 'OK', -buttons => [qw(OK)]); my $message = $dialog->add('Message', -width => 500, -text => <pack; $dialog->Show; } ################################################################################ # Utility # sub RoundIn { my $KP = shift; my $grid = $KP->{grid}; my($left, $top, $right, $bottom) = @$grid; ClipRange($left, $right , $_[X1], $_[X2]); ClipRange($top , $bottom, $_[Y1], $_[Y2]); RoundRight($KP, $_[X1]); RoundDown ($KP, $_[Y1]); RoundLeft ($KP, $_[X2]); RoundUp ($KP, $_[Y2]); } sub RoundUp { my $KP = shift; my $top = $KP->{grid}[TOP]; Round($top, 0, @_) } sub RoundDown { my $KP = shift; my $top = $KP->{grid}[TOP]; Round($top, 1, @_) } sub RoundLeft { my $KP = shift; my $left = $KP->{grid}[LEFT]; Round($left, 0, @_) } sub RoundRight { my $KP = shift; my $left = $KP->{grid}[LEFT]; Round($left, 1, @_) } sub Round { my $origin = shift; my $up = shift; for (@_) { my $dx = $_ - $origin; $dx += GridSize-1 if $up; $dx = int($dx / GridSize) * GridSize; $_ = $origin + $dx } } sub Polygon { my($corners, $size, $h, $v) = @_; my @corners; for (my $i=0; $i<@$corners; $i+=2) { push @corners, $h + $size * $corners->[$i ]; push @corners, $v + $size * $corners->[$i+1]; } @corners } sub Offset { my($corners, $h, $v) = @_; $corners->[0] += $h; $corners->[1] += $v; $corners->[2] += $h; $corners->[3] += $v; } sub max { my $max = shift; for (@_) { $max < $_ and $max = $_ } $max } sub Margin { my($canvas, $rect, $xy) = @_; my($x, $y) = @$xy; my($left, $top, $right, $bottom) = $canvas->coords($rect); [ $x-$left, $y-$top, $right-$x, $bottom-$y ] } sub ClipMargin { my $KP = shift; my $grid = $KP->{grid}; my $margin = $KP->{stash}{margin}; my($gl, $gt, $gr, $gb) = @$grid; my($ml, $mt, $mr, $mb) = @$margin; ClipRange($gl+$ml, $gr-$mr, $_[0]); ClipRange($gt+$mt, $gb-$mb, $_[1]); } sub ClipRange { my $min = shift; my $max = shift; for (@_) { $_ < $min and $_ = $min; $_ > $max and $_ = $max; } } sub CheckRange { my $min = shift; my $max = shift; for (@_) { $_ < $min and return 0; $_ > $max and return 0; } 1 } ################################################################################ # Methods # sub Tk::Canvas::XY { my $canvas = shift; my $x = $canvas->canvasx($Tk::event->x); my $y = $canvas->canvasy($Tk::event->y); my @xy = ($x, $y); wantarray ? @xy : \@xy } sub Tk::Canvas::Inside { my($canvas, $id, $x, $y) = @_; my($x1, $y1, $x2, $y2) = $canvas->coords($id); $x1 <= $x and $x <= $x2 and $y1 <= $y and $y <= $y2 } sub Tk::Canvas::Intersect { my($canvas, $idA, $idB) = @_; my($ax1, $ay1, $ax2, $ay2) = $canvas->coords($idA); my($bx1, $by1, $bx2, $by2) = $canvas->coords($idB); Intersect($ax1, $ax2, $bx1, $bx2) and Intersect($ay1, $ay2, $by1, $by2) } sub Intersect { my($a, $b, $x, $y) = @_; $x < $b and $a < $y } __END__ =head1 NAME ColorWork - colorwork knitting pattern editor =head1 SYNOPSIS B =head1 DESCRIPTION B is an editor for creating colorwork knitting patterns. The stitches can be viewed either as squares (easier to edit), or chevrons (closer to the finished appearance). =head2 Creating patterns Click on the pallet to set the current color. The big square and chevron show the currently selected color. Left-click on the big square for the grid view. Left-click on the big chevron for the chevron view. Left-click on the grid to set a square to the current color. Left-click again to clear it. Left-click and drag to set/clear many squares. Right-click and drag on the grid to select a rectangular region. The selection is shown by a markee (crawling) outline. When you Right-click-up, the markee snaps to the grid lines. Left-click and drag inside the selection picks up the selection and drops a copy of it elsewhere. The markee goes with the copy. After you drop it, you can click and drag to drop another copy. To clear the selection =over 4 =item * left-click outside the selection =item * right-click anywhere =back =head2 Load and Save The B button saves the current pattern to a file, with a C<.ckn> extension The B button loads a saved pattern from a C<.ckn> file. =head2 Resize The B button brings up a dialog box that allows you to change the size of the pattern, and the guideline interval. =head2 Printing The B button writes the current pattern to a PostScript (C<.ps>) file. You have to print the PostScript file yourself. =head1 REQUIRES B is written Perl/Tk. To run it, you need =over 4 =item * Perl =item * C =back If you want to print the patterns, you need some way to print PostScript file. =head1 BUGS =over 4 =item * File extension filtering is broken on Win32. =item * The Resize dialog should do proper validation on the Entry widgets. =back =head1 TODO Nothing planned. Send suggestions, bugs, etc. to =head1 AUTHOR Steven McDougall =head1 COPYRIGHT Copyright (c) 2007-2008 by Steven McDougall. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.