File Coverage

blib/lib/Games/Sudoku/Preset.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1 1     1   23181 use strict;
  1         2  
  1         38  
2 1     1   6 use warnings;
  1         2  
  1         48  
3             #use feature 'say';
4            
5             package Games::Sudoku::Preset;
6            
7 1     1   1222 use version; our $VERSION = qv('0.1'); # PBP
  1         2357  
  1         19  
8            
9 1     1   517 use Tk;
  0            
  0            
10             use List::Util qw(first);
11            
12             my @cells; # array of cell objects (0 .. 80)
13            
14             # ====================================================================
15             # Start methods stuff
16             # ====================================================================
17            
18             # Start method enter
19             # Purpose: enter a new Sudoku puzzle
20             # Usage: my $puzzle = Games::Sudoku::Preset->enter();
21             # Result: the entered and validated puzzle as a string of 81 characters
22             #
23             sub enter {
24             my $class = shift;
25             # _createGUI();
26             GUI::initGUI();
27            
28             Tk::MainLoop();
29             my $game = _mk_result();
30             return $game;
31             }
32            
33             # Start method validate
34             # Purpose: validate a Sudoku puzzle
35             # Usage: my $puzzle = Games::Sudoku::Preset->validate($game);
36             # Result: validated puzzle as a string of 81 characters
37             #
38             sub validate {
39             my ( $class, $game ) = @_;
40            
41             my $err_ref = _eval_initGUI_with_game($game);
42            
43             Tk::MainLoop() if ($err_ref);
44             $game = _mk_result();
45             return $game;
46             }
47            
48             # Start method edit
49             # Purpose: edit a Sudoku puzzle
50             # Usage: my $puzzle = Games::Sudoku::Preset->edit($game);
51             # Result: edited and validated puzzle as a string of 81 characters
52             #
53             sub edit {
54             my ( $class, $game ) = @_;
55            
56             my $err_ref = _eval_initGUI_with_game($game);
57            
58             Tk::MainLoop();
59             $game = _mk_result();
60             return $game;
61             }
62            
63             sub _eval_initGUI_with_game {
64             my $game = shift;
65            
66             my $err_ref = eval {_initGUI_with_game($game)};
67             if ($@) {
68             print STDERR "Fatal error: $@\n";
69             $err_ref = 1;
70             }
71             return $err_ref;
72             }
73            
74             sub _initGUI_with_game {
75             my $game = shift;
76            
77             $game = _purify($game);
78             return '' unless $game; # after wrong ref type
79             my $holder = first {$_ !~ /([1-9])/} ( split '', $game );
80             'Cell'->placeholder($holder); # keep for return
81             GUI::initGUI();
82             _insert_to_board($game);
83             my $err_ref = ::verify_board($game);
84             return $err_ref;
85             }
86            
87             # purify the supplied game
88             # remove comment lines, newlines
89             # ignore whitespace unless used as placeholder
90             # my $game_as_string = _purify($game_org);
91             # $game_org: string or ref to array
92             #
93             sub _purify {
94             my $game = shift;
95            
96             if ( !ref $game ) {
97             if ( $game =~ m'^#' ) {
98             # needs ignore preceeding comment lines
99             my @game = split( qr"\n", $game );
100             $game = \@game;
101             }
102             }
103            
104             if ( ref $game ) {
105             if ( ref $game eq 'ARRAY' ) {
106             # ignore preceeding comment lines
107             while ( ${$game}[0] =~ /^#/ ) {shift @$game}
108             $game = join( '', @$game );
109             } else {
110             die 'Parameter "game" must be a string or an array reference';
111             }
112             }
113            
114             # ignore whitespace
115             if ( length($game) > 81 ) {$game =~ s/\s//g}
116            
117             return $game if ( length($game) == 81 );
118            
119             my $l = length($game);
120             $game =~ s/(.{9})(?=.)/$1\n/g;
121             my @msg = ("Length of puzzle string is $l, should be 81\n\n$game\n");
122             die @msg;
123             } ## end sub _purify
124            
125             # Populate the cell objects with the given game
126             # the given game is purified already
127             # _insert_to_board($gamestring);
128             #
129             sub _insert_to_board {
130             my $gamestring = shift;
131             return unless $gamestring;
132            
133             my @game = split( //, $gamestring );
134             foreach my $cell_idx ( 0 .. 80 ) {
135             my $cell = $cells[$cell_idx];
136             $cell->cellvalue( $game[$cell_idx] ) if $game[$cell_idx] =~ /[1-9]/;
137             }
138            
139             my $count;
140             $count++ while $gamestring =~ /[1-9]/g;
141             GUI::show_initial_count($count);
142             return;
143             }
144            
145             # convert the sudoku board to a string of 81 characters
146             # and return this to the caller (a start method).
147             # This becomes the return value of the start method
148             #
149             sub _mk_result {
150             # placeholder for unknown digits in sudoku output files
151             my $unknown_digit = 'Cell'->placeholder();
152            
153             my @alldigits = map( {
154             $_->cellvalue() || $unknown_digit;
155             } @cells );
156             return join '', @alldigits;
157             }
158            
159             # ====================================================================
160             #<<< hands off, perltidy!
161             package
162             GUI;
163             #>>>
164             # ====================================================================
165            
166             use List::Util qw(first);
167            
168             # ====================================================================
169             # GUI stuff
170             # ====================================================================
171            
172             my $mw; # the MainWindow
173             my $tinysize = 10; # size of a tiny square (pixels)
174             my $fieldsize; # size of a sudoku field
175             my $clickfield; # the toplevel which covers the active sudoku field
176             # for clicking
177             my @tiny_fields = (undef); # the tiny squares of the clickfield (indexed 1 .. 9)
178             my $valuecount = 0; # count of entered values
179             my $status_lb; # the status Label
180            
181             sub initGUI {
182             # $mw = shift;
183            
184             $mw or $mw = MainWindow->new();
185             _createGUI();
186             return;
187             }
188            
189             sub _createGUI {
190            
191             $mw or $mw = MainWindow->new();
192             # let a click on the kill button (at the right side of the titlebar)
193             # cancel the program
194             $mw->protocol( 'WM_DELETE_WINDOW', \&Tk::exit );
195             $fieldsize = 3 * ( $tinysize + 1 ) - 1; # size of sudoku field
196             my $totalsize = 9 * ( $fieldsize + 1 ) - 1;
197             _create_board($totalsize);
198            
199             # create bottom area
200            
201             my $but_fr = $mw->Frame()->pack( -side => 'bottom', -fill => 'x' );
202            
203             # make clickfield invisible while mouse is over the bottom frame
204             $but_fr->bind( '', sub {$clickfield->withdraw} );
205             my $stat_fr = $but_fr->Frame()->pack( -fill => 'x' );
206            
207             # create value count labels
208            
209             $stat_fr->Label( -text => 'values' )->pack( -side => 'right' );
210             $stat_fr->Label(
211             -textvariable => \$valuecount,
212             -width => 2,
213             -anchor => 'e'
214             )->pack( -side => 'right' );
215            
216             # create status label
217            
218             $status_lb = $stat_fr->Label()->pack( -fill => 'x' );
219            
220             # create Done button
221            
222             my $done_b = $but_fr->Button(
223             -text => 'Done',
224             -command => sub {
225             my $err = ::verify_board();
226             $err or { $mw->destroy() };
227             },
228             )->pack( -side => 'left', -padx => 10, -pady => 3 );
229            
230             # create Save&Cancel button
231            
232             $but_fr->Button(
233             -text => 'Save & Cancel',
234             -command => sub {
235             my $ok = ::save_sudoku($mw);
236             $ok and Tk::exit();
237             },
238             )->pack( -side => 'left', -padx => 10, -pady => 3 );
239            
240             # create Cancel button
241             $but_fr->Button( -text => 'Cancel', -command => sub {Tk::exit()} )
242             ->pack( -side => 'left' );
243            
244             # set window size
245            
246             my $size_y = $totalsize + $done_b->reqheight + $status_lb->reqheight + 6;
247             $mw->geometry("${totalsize}x$size_y");
248             $mw->resizable( 0, 0 ); # freeze window size
249             _create_clickfield($mw);
250            
251             # On Windows XP, the sudoku window likes to hide itself behind the "DOS"
252             # shell window at the moment where the clickfield pops up for the 2nd time.
253             # This can be avoided by
254             # $mw->raise() or $mw->focus() or $cells[any]->property('Button')->focus().
255             # Set the initial focus to the 1st field
256             $cells[0]->property('Button')->focus();
257            
258             return;
259             } ## end sub _createGUI
260            
261             sub _create_board {
262             my $totalsize = shift;
263            
264             my $field_index = 0;
265             foreach my $i ( 0 .. 8 ) {
266             foreach my $j ( 0 .. 8 ) {
267             my $fieldID = _create_field( $j, $i );
268             my $cellobj = 'Cell'->new( $field_index, $fieldID );
269             $fieldID->configure(
270             -textvariable => \$cellobj->property('Value') );
271             push @cells, $cellobj;
272             $field_index++;
273             }
274             }
275            
276             # draw block separator lines
277             foreach my $pos ( 3, 6 ) {
278             my $where = $pos * ( $fieldsize + 1 ) - 1;
279             $mw->Frame(
280             -width => 1,
281             -height => $totalsize,
282             -background => 'black'
283             )->place( -x => $where, -y => 0 );
284             $mw->Frame(
285             -width => $totalsize,
286             -height => 1,
287             -background => 'black'
288             )->place( -x => 0, -y => $where );
289             }
290             return;
291             } ## end sub _create_board
292            
293             # create a sudoku field
294             #
295             sub _create_field {
296             my ( $w, $h ) = my ( $w_num, $h_num ) = @_; # pos. num.s of sudoku field (0 .. 8)
297             my $field_index = $w + 9 * $h; # index of sudoku field (0 .. 80)
298             $w *= $fieldsize + 1; # pos. of sudoku field (pixels)
299             $h *= $fieldsize + 1; #
300            
301             # first create a square Frame to force a square Button in it
302             my $space = $mw->Frame( -width => $fieldsize, -height => $fieldsize )
303             ->place( -x => $w, -y => $h );
304             $space->packPropagate(0); # prevent resizing the frame
305             my $fieldID = $space->Button()->pack( -fill => 'both', -expand => 1 );
306            
307             # mouse and keyboard bindings
308            
309             $fieldID->bind( '', [\&_move_clickfield] );
310             foreach my $digit ( 1 .. 9 ) {
311             #alpha keypad
312             $fieldID->bind( "" => [\&_change_digit, $digit] );
313            
314             #numeric keypad
315             $fieldID->bind( "" => [\&_change_digit, $digit] );
316             }
317             # delete digit
318             foreach my $key (qw/0 KP_0 space Delete/) {
319             $fieldID->bind( "<$key>" => [\&_change_digit] );
320             }
321            
322             # keyboard focus move
323             $fieldID->bind( "" => [\&_move_focus, $w_num, $h_num - 1] );
324             $fieldID->bind( "" => [\&_move_focus, $w_num, $h_num + 1] );
325             $fieldID->bind( "" => [\&_move_focus, $w_num - 1, $h_num] );
326             $fieldID->bind( "" => [\&_move_focus, $w_num + 1, $h_num] );
327             return $fieldID;
328             } ## end sub _create_field
329            
330             # move focus to neighbouring sudoku field
331             # callback of the arrow keys
332             #
333             sub _move_focus {
334             my ( $fieldID, $w_new, $h_new ) = @_;
335            
336             $clickfield->withdraw; # make clickfield invisible
337             $w_new %= 9; # end-around
338             $h_new %= 9;
339             $cells[$w_new + 9 * $h_new]->property('Button')->focus();
340             return;
341             }
342            
343             # delete resp. replace sudoku digit
344             # callback of some keys (alpha or numeric keypad)
345             # also called from _change_my_digit
346             #
347             sub _change_digit {
348             my ( $fieldID, $digit_num ) = @_; # ID of button, digit
349            
350             $clickfield->withdraw; # make clickfield invisible
351             _reset_colors() if $status_lb->cget('-fg') eq 'red';
352            
353             my $cell = first {$_->property('Button') eq $fieldID} @cells;
354             defined $cell or die "Code error: Button $fieldID not found in cells";
355            
356             if ($digit_num) {
357             # set or replace old digit
358             ${ $fieldID->cget('-textvariable') } or $valuecount++;
359             $cell->cellvalue($digit_num);
360             } else {
361             # delete old digit
362             $cell->cellvalue('');
363             $valuecount--;
364             }
365            
366             return;
367             }
368            
369             sub show_initial_count {
370             $valuecount = shift;
371             return;
372             }
373            
374             # ====================================================================
375             # clickfield stuff
376             # ====================================================================
377            
378             sub _create_clickfield {
379             my $mw = shift;
380            
381             $clickfield = $mw->Toplevel( -width => $fieldsize, -height => $fieldsize );
382             $clickfield->overrideredirect(1); # suppress window frame
383             foreach my $i ( 0 .. 2 ) {
384             foreach my $j ( 0 .. 2 ) {
385             _create_tinysquare( $j, $i );
386             }
387             }
388             $clickfield->withdraw; # make clickfield invisible
389             # make clickfield invisible when the window gets moved
390             $mw->bind( '' => sub {$clickfield->withdraw} );
391             return;
392             }
393            
394             sub _create_tinysquare {
395             my ( $w, $h ) = my ( $w_num, $h_num ) = @_; # pos. num.s of tiny square
396             $w *= $tinysize + 1; # pos. of tiny square
397             $h *= $tinysize + 1; #
398            
399             # first create a square Frame to force a square Button in it
400             my $space = $clickfield->Frame( -width => $tinysize, -height => $tinysize )
401             ->place( -x => $w, -y => $h );
402             $space->packPropagate(0); # prevent resizing the frame
403             my $tiny = $space->Button(
404             -relief => 'flat',
405             -background => 'black',
406             -command => [\&_change_my_digit, $w_num + 3 * $h_num + 1],
407             )->pack( -fill => 'both', -expand => 1 );
408             push( @tiny_fields, $tiny );
409             return;
410             }
411            
412             # position the clickfield over the entered sudoku field
413             # callback of the event
414             #
415             sub _move_clickfield {
416             my ($fieldID) = shift; # ID of button to be covered
417            
418             # ignore re-entering the active field
419             # (this happens when withdrawing the clickfield)
420            
421             # Color change and popup required when returning from the bottom row,
422             # so no return to caller in this case
423             return
424             if ( $fieldID == ( 'Cell'->activefield() || 0 )
425             and $clickfield->state eq 'normal' );
426            
427             'Cell'->activefield($fieldID);
428             $clickfield->withdraw; # make clickfield invisible
429            
430             # mark the tiny square of the current digit by a different color
431             foreach my $tiny ( @tiny_fields[1 .. 9] ) {
432             $tiny->configure( -background => 'black',
433             -activebackground => 'black' );
434             }
435             if ( my $digit = ${ $fieldID->cget('-textvariable') } ) {
436             $tiny_fields[$digit]->configure( -background => 'red',
437             -activebackground => 'orange' );
438             }
439             $clickfield->configure( -popover => $fieldID );
440             $clickfield->Popup(); # make clickfield visible
441             return;
442             }
443            
444             # delete resp. replace old digit of the active sudoku field
445             # callback of the tiny squares
446             #
447             sub _change_my_digit {
448             my $digit_num = shift; # digit of the clicked tiny square
449             my $actfield = 'Cell'->activefield();
450             my $olddigit = ${ $actfield->cget('-textvariable') };
451             if ( $olddigit eq $digit_num ) {$digit_num = undef}
452             _change_digit( $actfield, $digit_num );
453             return;
454             }
455            
456             # ====================================================================
457            
458             # show problem cells in red on the board.
459             #
460             sub mark_problem_cells {
461             my $err_ref = shift;
462             my ( $errtxt, $errcells_ref ) = @$err_ref;
463             $status_lb->configure( -text => $errtxt, -fg => 'red' );
464             foreach my $errcell (@$errcells_ref) {
465             my $fieldID = $errcell->property('Button');
466             if ( $errcell->property('Value') ) {
467             $fieldID->configure( -fg => 'red' );
468             } else {
469             $fieldID->configure( -bg => 'red' );
470             }
471             $errcell->is_errcell(1);
472             }
473             return;
474             }
475            
476             # reset the default colors of the problem cells
477             # on the 1st digit change after the error display
478             #
479             sub _reset_colors {
480             my @errcells = grep {$_->is_errcell} @cells;
481             return unless @errcells;
482            
483             my $field = first {$_->property('Button')->cget('-bg') ne 'red'} @cells;
484             my $bg_standard = $field->{Button}->cget('-bg');
485            
486             foreach my $cell (@errcells) {
487             $cell->property('Button')
488             ->configure( -fg => 'black', -bg => $bg_standard );
489             $cell->is_errcell(0);
490             }
491             # clear error text in status
492             $status_lb->configure( -text => '', -fg => 'black' );
493             return;
494             }
495            
496             # Show message in messageBox widget
497             # _showmessage(message_lines);
498             #
499             sub _showmessage {
500             $mw->messageBox(@_);
501             return;
502             }
503            
504             # ====================================================================
505             package main; # end GUI block
506             # ====================================================================
507            
508             # ====================================================================
509             # puzzle verification stuff
510             # ====================================================================
511            
512             # verify the state of the sudoku board
513             # callback of the 'Done' button
514             # $err_ref = ::verify_board();
515             # $err_ref is a ref to the first error info as returned
516             # by the error check routines
517             # An error will inhibit the end of the module
518             #
519             sub verify_board {
520             my $err_ref = _has_doubles() || _cell_nocand() || _unit_nocand();
521             if ($err_ref) {
522             GUI::mark_problem_cells($err_ref);
523             }
524             return $err_ref;
525             }
526            
527             # for each value cell search for a sibling with the same value
528             #
529             sub _has_doubles {
530             my @presets = grep {$_->property('Value')} @cells;
531            
532             foreach my $idx1 ( 0 .. $#presets - 1 ) {
533             my $val1 = $presets[$idx1]->property('Value');
534            
535             my @dupl;
536             foreach my $sibltype (qw/Block_num Row_num Col_num/) {
537             my $unitname = $presets[$idx1]->property($sibltype);
538             push @dupl, grep {
539             $_->property($sibltype) eq $unitname
540             and $_->property('Value') eq $val1
541             } @presets[$idx1 + 1 .. $#presets];
542             }
543             next unless @dupl;
544            
545             unshift @dupl, $presets[$idx1];
546             return ["duplicate value $val1", \@dupl];
547             }
548             return;
549             }
550            
551             # for each empty cell check whether each poss. value is occupied by siblings
552             #
553             sub _cell_nocand {
554             my @presets = grep {$_->property('Value')} @cells;
555            
556             foreach my $cell (@cells) {
557             next if $cell->property('Value');
558             my @sibls;
559             foreach my $sibltype (qw/Block_num Row_num Col_num/) {
560             my $typeidx = $cell->property($sibltype);
561             push @sibls, grep {$_->property($sibltype) eq $typeidx} @presets;
562             }
563             my %seen;
564             foreach (@sibls) {$seen{ $_->property('Value') }++;}
565             next if keys %seen != 9;
566            
567             return ["no value possible", [$cell]];
568             }
569             return;
570             }
571            
572             # for each unit check whether any poss. value is invalid for each member cell
573             #
574             sub _unit_nocand {
575             my @presets = grep {$_->property('Value')} @cells;
576            
577             UNIT:
578             foreach my $type (qw/Block_num Row_num Col_num/) {
579             foreach my $unitidx ( 1 .. 9 ) {
580             # collect the cells of this unit
581             my $unittype = substr( $type, 0, 1 );
582             my $unitname = lc($unittype) . $unitidx;
583             my @unitcells = grep {
584             $_->property($type) eq $unitname
585             and not $_->property('Value')
586             } @cells;
587             # skip if all values found in this unit
588             next UNIT unless @unitcells;
589            
590             my $cands;
591             foreach my $cell (@unitcells) {
592             my %seen;
593             $seen{$_} = undef foreach ( 1 .. 9 ); # define(!) all keys 1..9
594             # collect the siblings of this cell
595             my @sibls = $cell->sibling_cells( \@presets );
596            
597             # all values of siblings are invalid as cands in this cell
598            
599             foreach my $cand ( 1 .. 9 ) {
600             foreach my $sibl (@sibls) {
601             my $val = $sibl->property('Value');
602             delete $seen{$val} if exists $seen{$val};
603             }
604             }
605             # collect the valid cands
606             $cands .= join '|', ( sort keys %seen );
607             }
608            
609             foreach my $val ( 1 .. 9 ) {
610             next if $cands =~ /$val/;
611             # ... and values in this unit are invalid too in the cell,
612             # but not in the unit
613             next
614             if grep {
615             $_->property($type) eq $unitname
616             and $_->property('Value') eq $val
617             } @cells;
618            
619             return ["value $val not possible", \@unitcells];
620             }
621             } ## end foreach...
622             } ## end UNIT: foreach...
623             return;
624             } ## end sub _unit_nocand
625            
626             # ====================================================================
627            
628             # Callback of the "Save & Cancel" Button
629             #
630             sub save_sudoku {
631             my $mw = shift;
632            
633             my $file = _ask_filename($mw);
634             return unless defined $file;
635             my $game = Games::Sudoku::Preset::_mk_result();
636             my $ok = _write_text( $mw, $file, $game );
637             return $ok;
638             }
639            
640             sub _ask_filename {
641             my $mw = shift;
642             my $file;
643            
644             $file = $mw->getSaveFile(
645             -title => 'Sudoku output file',
646             -filetypes => [
647             ['Sudoku Files', '.sudo'],
648             ['Text Files', ['.txt', '.text']],
649             ['All Files', ['*']],
650             ],
651             -defaultextension => '.sudo',
652             );
653             return unless defined $file;
654             use Encode;
655             $file = encode( 'iso-8859-1', $file );
656             return $file;
657             }
658            
659             # write text to a file
660             # _write_text($mw, $outfile, text);
661             #
662             sub _write_text {
663             my ( $mw, $outfile, $text ) = @_;
664             open( my $out, '>', $outfile ) or do {
665             _fatal_err( $mw, "Cannot open $outfile:\n$!" );
666             return;
667             };
668             print $out $text;
669             close($out) or do {
670             _fatal_err( $mw, "Cannot close $outfile:\n$!" );
671             return;
672             };
673             return 1;
674             }
675            
676             sub _fatal_err {
677             showmessage(
678             -title => 'Fatal error',
679             -message => "@_",
680             -icon => 'error'
681             );
682             return;
683             }
684            
685             # ====================================================================
686             #<<< hands off, perltidy!
687             package
688             Cell;
689             #>>>
690             # ====================================================================
691            
692             # constructor for cell objects
693             #
694             sub new {
695             my $class = shift;
696             my ( $cell_idx, $button ) = @_; # cell index (0 .. 80), Button widget
697            
698             my $row = int( $cell_idx / 9 ) + 1;
699             my $col = $cell_idx % 9 + 1;
700             my $block = int( ( $col - 1 ) / 3 ) + 3 * int( ( $row - 1 ) / 3 ) + 1;
701            
702             # cell properties
703            
704             my %props = ( # cell properties
705             # 'Name' => "r${row}c$col", # for tests
706             'Row_num' => "r$row", # row name (r0 .. r8)
707             'Col_num' => "c$col", # col name (c0 .. c8)
708             'Block_num' => "b$block", # blk name (b0 .. b8)
709             'Value' => '', # cell value
710             'Button' => $button, # ID of board field Button
711             );
712            
713             my $self = \%props;
714             bless( $self, $class );
715             return $self;
716             }
717            
718             # general getter for common cell object properties
719             #
720             sub property {
721             my ( $self, $propname, $propval ) = @_;
722            
723             exists $self->{$propname}
724             or die "Code error: attempt to use unknown cell prop. $propname";
725            
726             if ( $propname eq 'Value' ) {return $self->cellvalue($propval)}
727            
728             defined $propval and die "You cannot change property $propname";
729             return $self->{$propname};
730             }
731            
732             # setter/getter for cell object property 'Value'
733             #
734             sub cellvalue {
735             my ( $self, $propval ) = @_;
736            
737             if ( defined $propval ) {
738             my $txtvar = $self->{Button}->cget( -textvariable );
739             $$txtvar = $propval;
740             $self->{'Value'} = $propval;
741             return;
742             }
743             return $self->{'Value'};
744             }
745            
746             # setter/getter for property 'err'
747             #
748             sub is_errcell {
749             my ( $self, $bool ) = @_;
750            
751             if ($bool) {
752             $self->{err} = $bool;
753             return;
754             }
755             return $self->{err};
756             }
757            
758             # return all siblings of a given cell
759             # my @sibling_cells = $cell->sibling_cells($presets_ref);
760             # $presets_ref: Ref to array with preset values
761             #
762             sub sibling_cells {
763             my ( $self, $presets_ref ) = @_;
764            
765             my @sibls;
766             foreach my $sibltype (qw/Block_num Row_num Col_num/) {
767             my $unitname = $self->property($sibltype);
768             push @sibls, grep {$_->property($sibltype) eq $unitname} @$presets_ref;
769             }
770             return @sibls;
771             }
772            
773             # class properties
774             # ----------------
775            
776             my $Unknown_digit = '-'; # default
777             my $Act_field = 0; # default 0 instead of undef to allow numeric compare
778            
779             # setter/getter for class property 'Unknown_digit'
780             #
781             sub placeholder {
782             shift; # ignore caller
783             my $char = shift;
784            
785             if ($char) {
786             $Unknown_digit = $char;
787             return;
788             }
789             return $Unknown_digit;
790             }
791            
792             # setter/getter for property 'Act_field'
793             # the active field is the Button that belongs to the current cell
794             #
795             sub activefield {
796             shift; # ignore caller
797             my $cell = shift;
798            
799             if ($cell) {
800             $Act_field = $cell;
801             return;
802             }
803             return $Act_field;
804             }
805            
806             1;
807            
808             __END__