File Coverage

blib/lib/Games/Go/TDFinder.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


line stmt bran cond sub pod time code
1             # $Id: TDFinder.pm,v 1.17 2005/01/17 03:04:43 reid Exp $
2              
3             # TDFinder: find players in TDLIST and enter them into
4             # an appropriate .tde file. The most recent
5             # TDLIST is available from the AGA at:
6             # http:www.usgo.org
7             # Copyright (C) 2004, 2005 Reid Augustin reid@netchip.com
8             # 1000 San Mateo Dr.
9             # Menlo Park, CA 94025 USA
10              
11             # This library is free software; you can redistribute it and/or modify it
12             # under the same terms as Perl itself, either Perl version 5.8.5 or, at your
13             # option, any later version of Perl 5 you may have available.
14             #
15             # This program is distributed in the hope that it will be useful, but WITHOUT
16             # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
17             # FITNESS FOR A PARTICULAR PURPOSE.
18             #
19              
20             #
21             # ToDo:
22             # double click match to copy into tdeText
23             # drag matches into tdeText
24             # add menu button with:
25             # help, sort options,
26              
27             =head1 NAME
28              
29             TDFinder - a widget to support preparing Go tournament registration
30              
31             =head1 SYNOPSIS
32              
33             use Games::Go::TDFinder;
34              
35             $tdFinder = $parent-EGames::Go::TDFinder ( ? options ? );
36              
37             =head1 DESCRIPTION
38              
39             TDFinder is a widget to assist in preparing a Go Tournament register.tde file in AGA (American
40             Go Association) format. It consists of three main parts: a TDEntry widget at the bottom, a
41             'match' list in the middle (which is an ROText widget), and the tde information at the top (A
42             TextUndo widget).
43              
44             The widget opens the TDLIST file for searching. Tournemant directors should download the most
45             recent TDLIST from the AGA shortly before the tournament. The most recent TDLIST is available
46             from the AGA at: L
47              
48             Typing search keys into the TDEntry field causes the TDFinder to search through the TDLIST
49             looking for matches. When the number of matches is small enough to fit into the 'match' list
50             ROText widget, they are posted there. Individual TDLIST entries can be selected either by
51             further refining the search keys, or by using the Up/Down arrow keys. Typing 'Enter', double
52             clicking a match (BUGBUG: TBD), or dragging a match to the tde text widget (BUGBUG: TBD)
53             transfers a match to the tde file.
54              
55             The caller is responsible for make sure the final register.tde file corresponds to the
56             information in the tde part of the TDFinder widget.
57              
58             =cut
59              
60             package Games::Go::TDFinder; # composite widget for finding entries in TDLIST from AGA
61              
62 1     1   26737 use 5.005;
  1         4  
  1         36  
63 1     1   5 use strict;
  1         1  
  1         39  
64 1     1   6 use warnings;
  1         1  
  1         42  
65 1     1   1062 use IO::File;
  1         16044  
  1         149  
66 1     1   1197 use File::stat; # stat fields by name
  1         13018  
  1         10  
67 1     1   1465 use Games::Go::AGATourn;
  1         8780  
  1         79  
68 1     1   501 use Tk;
  0            
  0            
69             use Tk::widgets qw/ Entry TextUndo ROText Adjuster /;
70             use Games::Go::TDEntry;
71             use Carp;
72              
73             use base qw(Tk::Frame); # TDFinder is a composite widget
74              
75             Construct Tk::Widget 'TDFinder';
76              
77             BEGIN {
78             our $VERSION = sprintf "%d.%03d", '$Revision: 1.17 $' =~ /(\d+)/g;
79             }
80              
81             # class variables:
82             our (@tdList); # there should be one and only one TDLIST file
83              
84             ######################################################
85             #
86             # methods
87             #
88             #####################################################
89              
90             sub ClassInit {
91             my ($class, $mw) = @_;
92              
93             $class->SUPER::ClassInit($mw);
94             }
95              
96             sub Populate {
97             my ($self, $args) = @_;
98              
99             $self->SUPER::Populate($args);
100              
101             $self->_initTDFinder();
102             $self->ConfigSpecs(
103             -tdListFile => ['PASSIVE', 'tdListFile', 'TDListFile', 'tdlist' ],
104             -height => [$self->{matchText}, 'height', 'Height', 12 ],
105             -scrollbars => [$self->{tdeText}, 'scrollbars', 'Scrollbars', 'osow' ],
106             -namelength => ['PASSIVE', 'namelength', 'Namelength', 20 ],
107             -clublength => ['PASSIVE', 'clublength', 'Clublength', 10 ],
108             DEFAULT => [$self->{tdeText}],
109             );
110              
111             =head1 OPTIONS
112              
113             =over 4
114              
115             =item B<-tdListFile> => filename
116              
117             Specify the filename of the current TDLIST file of players (from the AGA). If
118             B<-tdListFile> => undef, no TDLIST file is opened (and you can't really do much of
119             anything), otherwise if TDLIST can't be opened, TDFinder complains and dies.
120              
121             TDFinder checks the date of the tdListFile. If it is less than two weeks old,
122             TDFinder presents a warning dialog box.
123              
124             B<-tdListFile> may only be specified at widget creation. Configuring it later
125             has no effect.
126              
127             Default: 'tdlist' (in the current directory - a symlink is acceptable)
128              
129             =item B<-height> => height in chars
130              
131             Height is passed to the matchText widget.
132              
133             Default: 12
134              
135             =item B<-scrollbars> => a scrollbar 'where string
136              
137             The scrollbar 'where' string is passed to the tdeText widget. See the
138             B<-scrollbars> option in L for details.
139              
140             Default: 'osow'
141              
142             =item B<-namelength> => number
143              
144             The starting length of names in the tdeText widget. Lines are formatted so
145             that all the names take the same amount of space. This number grows if a
146             longer name is entered into tdeText.
147              
148             Default: 20
149              
150             =item B<-clublength> => number
151              
152             The starting length of club names in the tdeText widget. Lines are formatted
153             so that all the club names take the same amount of space. This number grows
154             if a longer name is entered into tdeText.
155              
156             Default: 10
157              
158             =item B
159              
160             All other options are passed to the tdeText widget.
161              
162             =back
163              
164             =cut
165              
166             $self->Delegates(DEFAULT => $self->{tdeText}); # all unknown methods
167             $self->toplevel->withdraw;
168             $self->_initTdList($args);
169             $self->toplevel->deiconify;
170             return($self);
171             }
172              
173             ######################################################
174             #
175             # Private methods
176             #
177             #####################################################
178              
179             sub _initTDFinder {
180             my $self = shift;
181              
182             # an undo-able Text widget for the register.tde file
183             my $t = $self->{tdeText} = $self->Scrolled(
184             'TextUndo',
185             -wrap => 'word',
186             -exportselection => 'true', );
187             $t->delete('1.0', 'end');
188              
189             $t->bind('Tk::TextUndo', '', [ 'undo']);
190             $t->bind('Tk::TextUndo', '', [ 'redo']);
191              
192             # a read-only Text widget to show list of matches
193             my $m = $self->{matchText} = $self->ROText(
194             -wrap => 'word',
195             -takefocus => 0,
196             -exportselection => 'true', );
197             my $a = $self->Adjuster();
198             # TDEntry widget for entering search keys
199             $self->{tdEntry} = $self->TDEntry(-text => 'Search:');
200              
201             # pack all the widgets
202             $self->{tdEntry}->pack(
203             -side => 'bottom',
204             -expand => 'false',
205             -fill => 'x');
206             $m->pack(
207             -side => 'bottom',
208             -expand => 'true',
209             -fill => 'both');
210             $a->packAfter($m,
211             -side => 'bottom',
212             -expand => 'true',
213             -fill => 'both');
214             $t->pack(
215             -side => 'bottom',
216             -expand => 'true',
217             -fill => 'both');
218              
219             # bindings:
220             my $e = $self->{entry} = $self->{tdEntry}->Subwidget('entry');
221             $e->bind('' => [$self => '_entryKeyPress', Ev('A'), ]); # new key in search field
222             $e->bind('' => [$self => '_moveListSelection', -1]);
223             $e->bind('' => [$self => '_moveListSelection', +1]);
224             $e->bind('' => [$self => '_changeAgaRating', +1]);
225             $e->bind('' => [$self => '_changeAgaRating', -1]);
226             $e->bind('' => [$self => '_addMatchSelection']);
227             $e->bind('' => [$self => '_Escape']);
228              
229             $m->tagConfigure("match",
230             -background => 'lightblue',
231             -relief => 'raised',
232             -underline => 'true');
233             $t->tagConfigure('dup',
234             -foreground => 'red');
235             $m->tagConfigure('dup',
236             -foreground => 'red');
237             $self->Advertise(entry => $e);
238             $self->Advertise(tdeText => $t);
239              
240             =head1 ADVERTISED WIDGETS
241              
242             =over 4
243              
244             =item B
245              
246             The TDEntry support widget: consists of a label, an entry widget, and a 'Case sensitive'
247             Checkbutton.
248              
249             You might want to do something like:
250              
251             $tdFinder->Subwidget('entry')->focus(); # start with focus in entry widget.
252              
253             =item B
254              
255             The TextUndo widget which holds the current register.tde contents. The caller is
256             reponsible for maintaining the on-disk file contents and making sure the tdeText content
257             matches the register.tde file (see L(1)).
258              
259             Use something like:
260              
261             $register_tde = tdFinder->Subwidget('tdeText')->get('1.0', 'end')
262              
263             to get the current contents of the tdeText widget.
264              
265             =back
266              
267             =cut
268              
269             $self->{mostRecentInsert} = 'none';
270             $self->{matchForeground} = $self->{matchText}->cget('-foreground');
271             $self->{tdeForeground} = $self->{tdeText}->cget('-foreground');
272             $self->{agaTourn} = Games::Go::AGATourn->new(register_tde => undef,
273             Round => 0);
274             my $height = $m->reqheight - (2 * $m->cget('-pady')); # pixel height
275             $self->{matchFontHeight} = int($height / $self->{matchText}->cget('-height')); # div by lines
276             # initialize:
277             $self->clear;
278             }
279              
280             sub _initTdList {
281             my ($self, $args) = @_;
282              
283             unless (scalar @tdList) { # init class data once only
284             my $tdListFile = exists($args->{'-tdListFile'}) ? $args->{'-tdListFile'} : 'tdlist';
285             if (defined($tdListFile)) {
286             my $fd = IO::File->new("<$tdListFile") or croak "can't open TDLIST $tdListFile: $!\n";
287             $self->_checkTime($tdListFile);
288             while (<$fd>) {
289             push (@tdList, $_);
290             }
291             close($fd);
292             }
293             }
294             $self->_clearListSelection; # fake a keypress to get TDLIST count into match window
295             }
296              
297             sub _checkTime {
298             my ($self, $file, @args) = @_;
299              
300             if (-f $file) { # seems to follow symbolic links just fine...
301             my $week = 60 * 60 * 24 * 7; # seconds in a week
302             if (stat($file)->mtime < time - (2 * $week)) { # too old?
303             my $rsp = $self->Dialog(
304             -text => "$file is more than two weeks old.\n\n" .
305             "Please get the most recent TDListN.txt file from the AGA at:\n\n" .
306             " http://usgo.org/ratings/default.asp\n",
307             -buttons => ['Quit', 'Continue'],
308             -default_button => 'Quit',
309             )->Show;
310             &Tk::exit(1) if ($rsp eq 'Quit');
311             }
312             } else {
313             croak ("Don't know how to handle $file - doesn't seem to be a regular file\n");
314             }
315             }
316              
317             sub _getDupKeys {
318             my $self = shift;
319              
320             my $t = $self->{tdeText};
321             my @lines = ('dummy', split ("\n", $t->get("1.0", "end"))); # dummy line in front
322             $self->{pids} = {};
323             $self->{names} = {};
324             for (my $ii = 1; $ii < @lines; $ii++) {
325             $lines[$ii] =~ s/^\s*#.*//s; # filter out comment only lines
326             $lines[$ii] =~ s/^\s*//s; # filter out empty lines
327             next if ($lines[$ii] eq '');
328             my $p = $self->{agaTourn}->ParseRegisterLine($lines[$ii]);
329             my $pid = lc("$p->{country}$p->{agaNum}");
330             my $name = lc($p->{name}); # lower case name to create key
331             $name =~ s/\s//g; # and remove all whitespace
332             push (@{$self->{pids}{$pid}}, $ii);
333             push (@{$self->{names}{$name}}, $ii);
334             }
335             }
336              
337             sub _markTdeDups {
338             my $self = shift;
339              
340             my $t = $self->{tdeText};
341             my @lines = ('dummy', split ("\n", $t->get("1.0", "end"))); # dummy line in front
342             $self->_getDupKeys();
343             $t->tagDelete('dup'); # remove all previous duplicate tags
344             foreach my $pid (keys(%{$self->{pids}})) {
345             if (scalar(@{$self->{pids}{$pid}}) > 1) {
346             # uh oh, a duplicate:
347             foreach my $ii (@{$self->{pids}{$pid}}) {
348             $t->tagAdd('dup', "$ii.0", "$ii.8");
349             }
350             }
351             }
352             foreach my $name (keys(%{$self->{names}})) {
353             if (scalar(@{$self->{names}{$name}}) > 1) {
354             # uh oh, a duplicate:
355             foreach my $ii (@{$self->{names}{$name}}) {
356             $t->tagAdd('dup', "$ii.9", "$ii.9 + " . $self->cget('-namelength') . " chars");
357             }
358             }
359             }
360             $t->tagConfigure('dup',
361             -foreground => 'red');
362             }
363              
364             sub _rankCompare {
365             my $self = shift;
366              
367             my $ratingA = ($a->{agaRating});
368             $ratingA = -99 unless (defined($ratingA));
369             my $ratingB = ($b->{agaRating});
370             $ratingB = -99 unless (defined($ratingB));
371             my $d = ($ratingB <=> $ratingA); # reverse order to put stronger players at the top of the list
372             my $s = 'R';
373             if ($d == 0) {
374             $s = 'n';
375             $d = ($a->{name} cmp $b->{name});
376             }
377             # my $nameLen = 25;
378             # printf("%-*s %-5s %s$s %5s %*s\n",
379             # $nameLen, $a->{name}, $a->{agaRating},
380             # ($d > 0) ? '>' : (($d < 0) ? '<' : '='),
381             # $b->{agaRating}, $nameLen, $b->{name},);
382             return $d;
383             }
384              
385             sub _Escape {
386             my ($self) = @_;
387              
388             $self->sort();
389             $self->_clearListSelection();
390             }
391              
392             sub _clearListSelection {
393             my ($self) = @_;
394              
395             $self->{entry}->delete(0, 'end');
396             $self->_entryKeyPress('x'); # fake a key press
397             }
398              
399             sub _parseTdListLine {
400             my ($self, $td) = @_;
401              
402             my $p = ($self->{agaTourn}->ParseTdListLine($td));
403             # convert from TDLIST format to TDE format
404             $p->{comment} = join(' ', $p->{memType}, $p->{expire}, $p->{state});
405             $p->{comment} =~ s/ */ /g;
406             delete($p->{memType});
407             delete($p->{expire});
408             delete($p->{state});
409             return $p
410             }
411              
412             sub _parseRegisterLine {
413             my ($self, $tde) = @_;
414              
415             return $self->{agaTourn}->ParseRegisterLine($tde);
416             }
417              
418             sub _addMatchSelection {
419             my ($self) = @_;
420              
421             my $m = $self->{matchText}; # the match text widget
422             if ($self->{matchListValid} > 0) { # add the activated line to TDE
423             $self->addPlayer($self->{matches}[$self->{active} - 1]);
424             } elsif ($self->{matchListValid} < 0) { # no matches, a tmp player?
425             my $entry = $self->{tdEntry}->get();
426             my ($rank, $name);
427             if ($entry =~ s/\s+([0-9]+[dkDK])\s*$//) {
428             $rank = $1;
429             } else {
430             $m->delete('1.0', 'end');
431             $m->insert('1.0', 'Unlisted player needs rank (like 3D or 4k) at the end');
432             return;
433             }
434             if ($entry =~ m/\s*(.+,.*)/) {
435             $name = $1;
436             } else {
437             $m->delete('1.0', 'end');
438             $m->insert('1.0', 'Unlisted player name needs last name, comma, then first name (and optional middle/honorific, etc).');
439             return;
440             }
441             # cannonicalize the hash key
442             $name =~ s/\s+/ /g; # turn all whitespace into single space
443             $name =~ s/^\s*//; # delete preceding whitespace
444             $name =~ s/\s*$//; # delete following whitespace
445             $self->{tmpNum}++;
446             $self->addTDE("TMP$self->{tmpNum} $name $rank");
447             } # else - need to narrow the search more, ignore...
448             }
449              
450             sub _moveListSelection {
451             my ($self, $change) = @_;
452              
453             my $m = $self->{matchText};
454             my $active = $self->{active} + $change;
455             return if (($active < 1) or ($active >= $m->index('end') - 1));
456             $self->{active} = $active;
457             $m->tagRemove('match', '1.0', 'end');
458             $m->tagAdd('match', "$active.0", "$active.0 lineend");
459             }
460              
461             sub _changeAgaRating {
462             my ($self, $change) = @_;
463              
464             my $m = $self->{matchText};
465             my $t = $self->{tdeEntry};
466             my $active = $self->{active};
467             my $p = $self->{matches}[$active - 1];
468             my $pid = "$p->{country}$p->{agaNum}";
469             if (($self->{ratingChanged}{$pid} == -99) and ($change > 0)) {
470             $self->{ratingChanged}{$pid} = -31; # change 99k to 30k
471             }
472             $self->{ratingChanged}{$pid} += $change;
473             if ($self->{ratingChanged}{$pid} == 0) {
474             $self->{ratingChanged}{$pid} += $change; # skip over 0
475             }
476             $m->delete("$active.0", "$active.0 lineend");
477             $m->insert("$active.0", $self->_format($p));
478             $m->tagAdd('match', "$active.0", "$active.0 lineend");
479             }
480              
481             sub _entryKeyPress {
482             my ($self, $char) = @_;
483              
484             my $m = $self->{matchText};
485             $char =~ s/\s*//g; # turn whitespace chars to nothing
486             return if ($char eq ''); # ignore whitespace and control type chars
487             my $width = $m->reqwidth; # insert changes widget back to it's original size.
488             my $height = $m->reqheight;
489             my $lines = $m->cget('-height');
490             # print("lines=$lines, height=$height, ");
491             $lines = int($height / $self->{matchFontHeight});
492             # print("new lines=$lines\n");
493             $m->configure('-height', $lines);
494             $self->{matchListValid} = 0;
495             $m->delete('1.0', 'end');
496             $m->configure(-foreground => $self->{matchForeground});
497             my $srchString = $self->{tdEntry}->get();
498             $srchString =~ s/^\s*//;
499             if ($srchString eq '') {
500             $m->insert('end', scalar(@tdList) . " players in TDLIST\n");
501             } else {
502             my $matches = $self->{matches} = $self->_search($srchString);
503             if (@$matches == 0) {
504             $m->configure(-foreground => 'red');
505             if (scalar(@tdList)) {
506             $m->insert('end', "No matches\n");
507             } else {
508             $m->insert('end', "No TDLIST\n");
509             }
510             $self->{matchListValid} = -1;
511             } elsif (@$matches >= $lines) {
512             $m->insert('end', scalar(@$matches) . " matches\n");
513             } else { # insert the matches into the matchText widget
514             foreach (@{$matches}) {
515             $_ = $self->_parseTdListLine($_); # convert TDLIST line to player
516             $m->insert('end', $self->_format($_) . "\n");
517             }
518             $self->{active} = 1;
519             $self->_moveListSelection(0);
520             $self->{matchListValid} = 1;
521             $self->_markMatchDups();
522             }
523             }
524             # Restore size:
525             $m->GeometryRequest($width, $height);
526             }
527              
528             sub _markMatchDups {
529             my $self = shift;
530              
531             my $m = $self->{matchText};
532             my @lines = ('dummy', split ("\n", $m->get("1.0", "end"))); # dummy line in front
533             $self->_getDupKeys(); # make sure dup keys are up to date
534             $m->tagDelete('dup'); # remove all previous duplicate tags
535             for (my $ii = 1; $ii < @{$self->{matches}} + 1; $ii++) {
536             my $p = $self->{matches}[$ii - 1];
537             my $pid = lc("$p->{country}$p->{agaNum}");
538             my $name = lc($p->{name}); # lower case name to create key
539             $name =~ s/\s//g; # and remove all whitespace
540             $m->tagAdd('dup', "$ii.0", "$ii.8")
541             if (exists($self->{pids}{$pid}));
542             $m->tagAdd('dup', "$ii.9", "$ii.9 + " . $self->cget('-namelength') . " chars")
543             if (exists($self->{names}{$name}));
544             }
545             $m->tagConfigure('dup',
546             -foreground => 'red');
547             }
548              
549             sub _search {
550             my ($self, $srchString) = @_;
551              
552             my @keys = (split '\s+', $srchString);
553             return () unless(@keys);
554             my @filtered = @tdList;
555             while (@keys) {
556             my $re = shift(@keys);
557             if ($self->{tdEntry}->case()) {
558             eval { @filtered = grep(/$re/, @filtered) };
559             } else {
560             eval { @filtered = grep(/$re/i, @filtered) };
561             }
562             if ($@) {
563             return ('Illegal or incomplete regular expression:', $@);
564             }
565             }
566             return \@filtered;
567             }
568              
569             # format a playerRef into register.tde format
570             sub _format {
571             my ($self, $p) = @_;
572              
573             $p->{name} =~ s/\s+/ /g; # turn all multiple whitespace into single space
574             $p->{name} =~ s/ ,/,/g; # no space in front of comma
575             if (length($p->{name}) > $self->cget('-namelength')) {
576             $self->configure(-namelength => length($p->{name}));
577             $self->{lengthChange} = 1;
578             }
579             $p->{club} =~ s/^club=\s*//i;
580             if ($p->{club} eq '') {
581             if ($p->{name} =~ m/(.*?),/) {
582             # use last name as club (reduce inter-family pairings)
583             $p->{club} = $1;
584             $p->{club} =~ s/\W//g; # remove all non-word chars
585             }
586             }
587             if (length($p->{club}) > $self->cget('-clublength')) {
588             $self->configure(-clublength => length($p->{club}));
589             $self->{lengthChange} = 1;
590             }
591             unless ($p->{club} eq '') {
592             $p->{club} = "CLUB=$p->{club}"
593             }
594             unless (exists($p->{country})) {
595             $p->{country} = 'TMP';
596             }
597             my $pid = "$p->{country}$p->{agaNum}";
598             unless(exists($self->{ratingChanged}{$pid})) {
599             $self->{ratingOrg}{$pid} =
600             $self->{ratingChanged}{$pid} = int($self->{agaTourn}->RankToRating($p->{agaRating}));
601             }
602             my $r;
603             if ($self->{ratingOrg}{$pid} == $self->{ratingChanged}{$pid}) {
604             # original - use rating or a rank?
605             if ((defined($p->{agaRank}) or # always exists, but is undefined if rating is valid
606             (lc($p->{country}) eq 'tmp'))) { # TMPs always use low accuraccy D/K style
607             if (defined($p->{agaRank})) {
608             $r = uc($p->{agaRank});
609             } else {
610             $r = uc(_ratingToRank($p->{agaRating}));
611             }
612             } else {
613             $r = $p->{agaRating};
614             }
615             } else {
616             $r = _ratingToRank($self->{ratingChanged}{$pid}); # changes are always a rank
617             }
618             return sprintf("$p->{country}%05d %-*s %5s %-*s $p->{flags} # $p->{comment}",
619             $p->{agaNum}, $self->cget('-namelength'), $p->{name}, $r,
620             $self->cget('-clublength'), $p->{club});
621             }
622              
623             sub _ratingToRank {
624             my ($rating) = @_;
625              
626             return sprintf(" %2d%s", ($rating > 0) ? $rating : -$rating, ($rating > 0) ? 'D' : 'K');
627             }
628              
629             ######################################################
630             #
631             # Public methods
632             #
633             #####################################################
634              
635             =head1 METHODS
636              
637             =over 4
638              
639             =item $tdFinder->B()
640              
641             Clears the entire TDFinder, including the tdeText, matchText, and tdEntry
642             subwidgets.
643              
644             =cut
645              
646             sub clear {
647             my ($self) = @_;
648              
649             $self->{matchListValid} = 0; # doesn't contain valid TDE entries
650             $self->{ratingOrg} = {};
651             $self->{ratingChanged} = {};
652             $self->{tmpNum} = 1;
653             $self->{tdeText}->delete('1.0', 'end');
654             $self->{matchText}->delete('1.0', 'end');
655             $self->_clearListSelection; # clear entry widget
656             }
657              
658             =item $tdeFinder->B($player)
659              
660             Adds a player to the TDFinder. Player should be a reference to a hash
661             containing the following members:
662              
663             $p->{agaNum} required
664             $p->{country} required
665             $p->{name} required
666             $p->{agaRating} required
667             $p->{club} optional
668             $p->{flags} optional
669             $p->{comment} optional
670              
671             =cut
672              
673             sub addPlayer {
674             my ($self, $p) = @_;
675              
676             my $t = $self->{tdeText};
677             $t->tagConfigure($self->{mostRecentInsert},
678             -foreground => $self->{tdeForeground}); # back to normal
679             return unless (defined $p); # so we can un-mark by adding undef
680             foreach (qw(agaNum country name agaRating)) {
681             next if(defined($p->{$_}));
682             carp ("No $_ defined for player\n");
683             return;
684             }
685             foreach (qw(club flags comment)) {
686             $p->{$_} = '' unless defined($p->{$_});
687             }
688             my $player = $self->_format($p);
689             $player =~ m/^\s*(\S*)/;
690             my $tag = $self->{mostRecentInsert} = lc($1); # save most recent insertion
691             # print "insert player(tag=$tag) at end: $player\n";
692             $t->insert('end', "$player\n", $tag);
693             $t->tagConfigure($tag,
694             -foreground => 'darkgreen',);
695             $t->see('end');
696             $self->_markTdeDups();
697             $self->eventGenerate('<>');
698             }
699              
700             =item $tdFinder->B('line in TDLIST format')
701              
702             Parses a line from the TDLIST file and adds the player to tdeText.
703              
704             =cut
705              
706             sub addTD {
707             my ($self, $td) = @_;
708              
709             $self->addPlayer($self->_parseTdListLine($td));
710             }
711              
712             =item $tdFinder->B('line in register.tde format')
713              
714             Parses a line from the register.tde file and adds the player to tdeText.
715              
716             =cut
717              
718             sub addTDE {
719             my ($self, $tde) = @_;
720              
721             my $p = $self->_parseRegisterLine($tde);
722             $self->addPlayer($p);
723             if ((lc ($p->{country}) eq 'tmp') and
724             ($p->{agaNum} >= $self->{tmpNum})) {
725             $self->{tmpNum} = $p->{agaNum};
726             }
727             }
728              
729             =item $tdFinder->B()
730              
731             Sorts the entries in tdeText. Currently, only sorting by rank (strongest
732             first) is supported. Comments lines are skipped over.
733              
734             =cut
735              
736              
737             sub sort {
738             my $self = shift;
739              
740             my $t = $self->{tdeText};
741             my $ii;
742             my @lines = ('dummy', split ("\n", $t->get("1.0", "end"))); # dummy line in front
743             my @players;
744             for ($ii = 1; $ii < @lines; $ii++) {
745             $lines[$ii] =~ s/^\s*#.*//s; # filter out comment only lines
746             next if ($lines[$ii] eq '');
747             push(@players, $self->{agaTourn}->ParseRegisterLine($lines[$ii]));
748             }
749             my @sortedPlayers = sort(_rankCompare @players);
750             for ($ii = 1; $ii < @lines; $ii++) {
751             next if ($lines[$ii] eq '');
752             my $player = $self->_format(shift(@sortedPlayers));
753             next if ($t->get("$ii.0", "$ii.0 lineend") eq $player);
754             # print "delete line: $ii\n";
755             $t->delete("$ii.0", "$ii.0 lineend");
756             $player =~ m/^\s*(\S*)/;
757             my $tag = lc($1);
758             # print "insert player(tag=$tag) at line $ii: $player\n";
759             $t->insert("$ii.0", $player, $tag); # tag with AGA number (lower cased)
760             }
761             if (@sortedPlayers) {
762             $self->Error('Players left over after sorting');
763             while (@sortedPlayers) {
764             my $player = $self->_format(shift(@sortedPlayers));
765             # print "insert leftover player at end: $player\n";
766             $t->insert('end', $player, 'error');
767             }
768             $t->tagConfigure('error',
769             -foreground => 'red',
770             -underline => 'true');
771             }
772             $self->_markTdeDups();
773             $self->eventGenerate('<>');
774             }
775              
776             1;
777              
778             __END__