File Coverage

blib/lib/Tk/FormUI.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             package Tk::FormUI;
2             ##----------------------------------------------------------------------------
3             ## :mode=perl:indentSize=2:tabSize=2:noTabs=true:
4             ##****************************************************************************
5             ## NOTES:
6             ## * Before comitting this file to the repository, ensure Perl Critic can be
7             ## invoked at the HARSH [3] level with no errors
8             ##****************************************************************************
9            
10             =head1 NAME
11            
12             Tk::FormUI - A Moo based object oriented interface for creating forms for
13             use with Tk
14            
15             =head1 VERSION
16            
17             Version 1.06
18            
19             =head1 SYNOPSIS
20            
21             use Tk::FormUI;
22            
23             my $form = Tk::FormUI->new;
24            
25             ## Add an Entry field for text
26             $form->add_field(
27             key => 'user_name',
28             label => 'User name',
29             type => $Tk::FormUI::ENTRY,
30             width => 40,
31             default => 'John Doe',
32             );
33            
34             ## Add a Radio Button field
35             $form->add_field(
36             key => 'gender',
37             label => 'Gender',
38             type => $Tk::FormUI::RADIOBUTTON,
39             choices => [
40             {
41             label => 'Male',
42             value => 'male',
43             },
44             {
45             label => 'Female',
46             value => 'female',
47             },
48             ],
49             );
50            
51             ## Display the form and capture the data returned
52             my $data = $form->show;
53            
54             =cut
55            
56             ##****************************************************************************
57             ##****************************************************************************
58 2     2   40169 use Moo;
  2         27722  
  2         15  
59             ## Moo enables strictures
60             ## no critic (TestingAndDebugging::RequireUseStrict)
61             ## no critic (TestingAndDebugging::RequireUseWarnings)
62 2     2   4584 use Readonly;
  2         5709  
  2         95  
63 2     2   11 use Carp qw(confess cluck);
  2         5  
  2         114  
64 2     2   2968 use Tk;
  0            
  0            
65             use Tk::FormUI::Field::Entry;
66             use Tk::FormUI::Field::Radiobutton;
67             use Tk::FormUI::Field::Checkbox;
68             use Tk::FormUI::Field::Combobox;
69             use Tk::FormUI::Field::Directory;
70             use Data::Dumper;
71             use JSON;
72             use Try::Tiny;
73            
74             ## Version string
75             our $VERSION = qq{1.06};
76            
77             Readonly::Scalar our $READONLY => 1;
78            
79             ## Used when importing a form, these are "simple" non-array attributes
80             Readonly::Array my @SIMPLE_ATTRIBUTES => (
81             qw(title message message_font button_label button_font min_width min_height)
82             );
83            
84             ##****************************************************************************
85             ## Various Types
86             ##****************************************************************************
87            
88             =head1 TYPES
89            
90             The Tk::FormUI recognizes the following values for the "type" key when
91             adding or defing a field.
92            
93             =cut
94            
95             ##****************************************************************************
96             ##****************************************************************************
97            
98            
99             =head2 Entry
100            
101             =over 2
102            
103             A Tk::Entry widget
104            
105             CONSTANT: $Tk::FormUI::ENTRY
106            
107             =back
108            
109             =cut
110            
111             ##----------------------------------------------------------------------------
112             Readonly::Scalar our $ENTRY => qq{Entry};
113            
114             ##****************************************************************************
115             ##****************************************************************************
116            
117             =head2 Checkbox
118            
119             =over 2
120            
121             A group of Tk::CheckButton widgets that correspond to the choices
122            
123             CONSTANT: $Tk::FormUI::CHECKBOX
124            
125             =back
126            
127             =cut
128            
129             ##----------------------------------------------------------------------------
130             Readonly::Scalar our $CHECKBOX => qq{Checkbox};
131            
132             ##****************************************************************************
133             ##****************************************************************************
134            
135             =head2 RadioButton
136            
137             =over 2
138            
139             A group of Tk::RadioButton widgets that correspond to the choices
140            
141             CONSTANT: $Tk::FormUI::RADIOBUTTON
142            
143             =back
144            
145             =cut
146            
147             ##----------------------------------------------------------------------------
148             Readonly::Scalar our $RADIOBUTTON => qq{RadioButton};
149            
150             ##****************************************************************************
151             ##****************************************************************************
152            
153             =head2 Combobox
154            
155             =over 2
156            
157             A Tk::BrowserEntry widget with a drop-down list that correspond to the choices
158            
159             CONSTANT: $Tk::FormUI::COMBOBOX
160            
161             =back
162            
163             =cut
164            
165             ##----------------------------------------------------------------------------
166             Readonly::Scalar our $COMBOBOX => qq{Combobox};
167            
168             ##****************************************************************************
169             ##****************************************************************************
170            
171            
172             =head2 Directory
173            
174             =over 2
175            
176             A Tk::Entry widget with a button that will open a Tk::chooseDirectory window
177            
178             CONSTANT: $Tk::FormUI::DIRECTORY
179            
180             =back
181            
182             =cut
183            
184             ##----------------------------------------------------------------------------
185             Readonly::Scalar our $DIRECTORY => qq{Directory};
186            
187             Readonly::Array my @KNOWN_FIELD_TYPES => (
188             $ENTRY, $CHECKBOX, $RADIOBUTTON, $COMBOBOX, $DIRECTORY,
189             );
190            
191             ##****************************************************************************
192             ## Object attribute
193             ##****************************************************************************
194            
195             =head1 ATTRIBUTES
196            
197             =cut
198            
199             ##****************************************************************************
200             ##****************************************************************************
201            
202             =head2 title
203            
204             =over 2
205            
206             Title of the form.
207            
208             DEFAULT: 'Form'
209            
210             =back
211            
212             =cut
213            
214             ##----------------------------------------------------------------------------
215             has title => (
216             is => qq{rw},
217             default => qq{Form},
218             );
219            
220             ##****************************************************************************
221             ##****************************************************************************
222            
223             =head2 B
224            
225             =over 2
226            
227             Message to display at the top of the form.
228            
229             DEFAULT: ''
230            
231             =back
232            
233             =cut
234            
235             ##----------------------------------------------------------------------------
236             has message => (
237             is => qq{rw},
238             default => qq{},
239             );
240            
241             ##****************************************************************************
242             ##****************************************************************************
243            
244             =head2 message_font
245            
246             =over 2
247            
248             Font to use for the form's message
249            
250             DEFAULT: 'times 12 bold'
251            
252             =back
253            
254             =cut
255            
256             ##----------------------------------------------------------------------------
257             has message_font => (
258             is => qq{rw},
259             default => qq{times 12 bold},
260             );
261            
262             ##****************************************************************************
263             ##****************************************************************************
264            
265             =head2 fields
266            
267             =over 2
268            
269             The fields contained in this form.
270            
271             =back
272            
273             =cut
274            
275             ##----------------------------------------------------------------------------
276             has fields => (
277             is => qq{rwp},
278             );
279            
280             ##****************************************************************************
281             ##****************************************************************************
282            
283             =head2 button_label
284            
285             =over 2
286            
287             The text to appear on the button at the bottom of the form.
288            
289             You may place the ampersand before the character you want to use as
290             a "hot key" indicating holding the Alt key and the specified character
291             will do the same thing as pressing the button.
292            
293             DEAULT: '&OK'
294            
295             =back
296            
297             =cut
298            
299             ##----------------------------------------------------------------------------
300             has button_label => (
301             is => qq{rw},
302             default => qq{&OK},
303             );
304            
305             ##****************************************************************************
306            
307             =head2 button_font
308            
309             =over 2
310            
311             Font to use for the form's button.
312            
313             DEFAULT: 'times 10'
314            
315             =back
316            
317             =cut
318            
319             ##----------------------------------------------------------------------------
320             has button_font => (
321             is => qq{rw},
322             default => qq{times 10},
323             );
324            
325             ##****************************************************************************
326            
327             =head2 min_width
328            
329             =over 2
330            
331             Minimum width of the form window.
332            
333             DEFAULT: 300
334            
335             =back
336            
337             =cut
338            
339             ##----------------------------------------------------------------------------
340             has min_width => (
341             is => qq{rw},
342             default => 300,
343             );
344            
345             ##****************************************************************************
346            
347             =head2 min_height
348            
349             =over 2
350            
351             Minimum height of the form window.
352            
353             DEFAULT: 80
354            
355             =back
356            
357             =cut
358            
359             ##----------------------------------------------------------------------------
360             has min_height => (
361             is => qq{rw},
362             default => 80,
363             );
364            
365             ##****************************************************************************
366            
367             =head2 submit_on_enter
368            
369             =over 2
370            
371             Boolean value indicating if pressing the Enter key should simulate clicking
372             the button to submit the form.
373            
374             DEFAULT: 1
375            
376             =back
377            
378             =cut
379            
380             ##----------------------------------------------------------------------------
381             has submit_on_enter => (
382             is => qq{rw},
383             default => 1,
384             );
385            
386             ##****************************************************************************
387            
388             =head2 cancel_on_escape
389            
390             =over 2
391            
392             Boolean value indicating if pressing the Escape key should simulate closing
393             the window and canceling the form.
394            
395             DEFAULT: 1
396            
397             =back
398            
399             =cut
400            
401             ##----------------------------------------------------------------------------
402             has cancel_on_escape => (
403             is => qq{rw},
404             default => 1,
405             );
406            
407             ##****************************************************************************
408             ##****************************************************************************
409            
410             =head2 error_font
411            
412             =over 2
413            
414             Font to use for the form's error messages.
415            
416             DEFAULT: 'times 12 bold'
417            
418             =back
419            
420             =cut
421            
422             ##----------------------------------------------------------------------------
423             has error_font => (
424             is => qq{rw},
425             default => qq{times 12 bold},
426             );
427            
428             ##****************************************************************************
429             ##****************************************************************************
430            
431             =head2 error_marker
432            
433             =over 2
434            
435             String used to indicate an error
436            
437             DEFAULT: '!'
438            
439             =back
440            
441             =cut
442            
443             ##----------------------------------------------------------------------------
444             has error_marker => (
445             is => qq{rw},
446             default => qq{!},
447             );
448            
449             ##****************************************************************************
450             ##****************************************************************************
451            
452             =head2 error_font_color
453            
454             =over 2
455            
456             Font color to use when displaying error message and error marker
457            
458             DEFAULT: 'red'
459            
460             =back
461            
462             =cut
463            
464             ##----------------------------------------------------------------------------
465             has error_font_color => (
466             is => qq{rw},
467             default => qq{red},
468             );
469            
470             ##****************************************************************************
471             ## "Private" atributes
472             ##***************************************************************************
473            
474             ## Holds reference to variable Tk watches for dialog completion
475             has _watch_variable => (
476             is => qq{rw},
477             );
478            
479             ##****************************************************************************
480             ## Object Methods
481             ##****************************************************************************
482            
483             =head1 METHODS
484            
485             =cut
486            
487             =for Pod::Coverage BUILD
488             This causes Test::Pod::Coverage to ignore the list of subs
489             =cut
490             ##----------------------------------------------------------------------------
491             ## @fn BUILD()
492             ## @brief Moo calls BUILD after the constructor is complete
493             ## @return
494             ## @note
495             ##----------------------------------------------------------------------------
496             sub BUILD
497             {
498             my $self = shift;
499            
500             ## Create an empty list of fields
501             $self->_set_fields([]);
502            
503             return($self);
504             }
505            
506             ##****************************************************************************
507             ##****************************************************************************
508            
509             =head2 add_field(...)
510            
511             =over 2
512            
513             =item B
514            
515             Add a field to the form.
516            
517             =item B
518            
519             A list of key / value pairs should be provide
520            
521             type - Type of field
522             key - Key to use in hash returned by the show() method
523             label - Text to display next to the field
524             readonly - Boolean indicating if field is read only and cannot be
525             modified
526             choices - ARRAY reference containing hashes that define the possible
527             values for the field.
528             REQUIRED for Checkbox, RadioButton, and Combobox
529             Each hash must have the following key/value pairs
530             label - String to be displayed
531             value - Value to return if selected
532            
533             =item B
534            
535             UNDEF on error, or the field object created
536            
537             =back
538            
539             =cut
540            
541             ##----------------------------------------------------------------------------
542             sub add_field ## no critic (RequireArgUnpacking,ProhibitUnusedPrivateSubroutines)
543             {
544             my $self = shift;
545             my %params = (@_);
546            
547             ## Check for missing keys
548             my @missing = ();
549             foreach my $key (qw(type key label))
550             {
551             push(@missing, $key) unless(exists($params{$key}));
552             }
553             if (scalar(@missing))
554             {
555             cluck(qq{Field missing the following reuired key(s): "},
556             join(qq{", "}, @missing),
557             qq{"}
558             );
559             }
560            
561             ## Now see what type field this is
562             foreach my $type (@KNOWN_FIELD_TYPES)
563             {
564             if (uc($params{type}) eq uc($type))
565             {
566             my $class = qq{Tk::FormUI::Field::} . ucfirst(lc($type));
567             my $field = $class->new(@_);
568            
569             confess(qq{Could not create $class}) unless ($field);
570            
571             ## Save the field in the object's fields attribute
572             push(@{$self->fields}, $field) if ($field);
573            
574             return($field);
575             }
576             }
577             cluck(qq{Unknown field type "$params{type}"});
578             return;
579            
580             }
581            
582             ##****************************************************************************
583             ##****************************************************************************
584            
585             =head2 show($parent)
586            
587             =over 2
588            
589             =item B
590            
591             Show the form as a child of the given parent, or as a new MainWindow if
592             a parent is not specified.
593            
594             The function will return if the users cancels the form or submits a
595             form with no errors.
596            
597             =item B
598            
599             $parent - Parent window, if none is specified, a new MainWindow will be
600             created
601            
602             =item B
603            
604             UNDEF when canceled, or a HASH reference containing whose keys correspond
605             to the key attributes of the form's fields
606            
607             =back
608            
609             =cut
610            
611             ##----------------------------------------------------------------------------
612             sub show
613             {
614             my $self = shift;
615             my $parent = shift;
616             my $test = shift;
617            
618             my $data;
619             my $finished;
620             while (!$finished)
621             {
622             ## Set the current data
623             $self->set_field_data($data) if ($data);
624            
625             ## Show the form
626             $data = $self->show_once($parent, $test);
627            
628             if ($data)
629             {
630             ## Finished only if there are no errors
631             $finished = !$self->has_errors;
632             }
633             else
634             {
635             $finished = 1;
636             }
637             }
638            
639             return($data);
640             }
641            
642             ##****************************************************************************
643             ##****************************************************************************
644            
645             =head2 show_once($parent)
646            
647             =over 2
648            
649             =item B
650            
651             Show the form as a child of the given parent, or as a new MainWindow if
652             a parent is not specified.
653            
654             Once the user submits or cancels the form, the function will return.
655            
656             =item B
657            
658             $parent - Parent window, if none is specified, a new MainWindow will be
659             created
660            
661             =item B
662            
663             UNDEF when canceled, or a HASH reference containing whose keys correspond
664             to the key attributes of the form's fields
665            
666             =back
667            
668             =cut
669            
670             ##----------------------------------------------------------------------------
671             sub show_once
672             {
673             my $self = shift;
674             my $parent = shift;
675             my $test = shift;
676             my $win; ## Window widget
677             my $result; ## Variable used to capture the result
678            
679             ## Create the window
680             if ($parent)
681             {
682             ## Create as a TopLevel to the specified parent
683             $win = $parent->TopLevel(-title => $self->title);
684             }
685             else
686             {
687             ## Create as a new MainWindow
688             $win = MainWindow->new(-title => $self->title);
689             }
690            
691             ## Hide the window
692             $win->withdraw;
693            
694             ## Do not allow user to resize
695             $win->resizable(0,0);
696            
697             ## Now use the grid geometry manager to layout everything
698             my $grid_row = 0;
699            
700             ## See if we have a message
701             if ($self->message)
702             {
703             ## Leave space for the message and a spacer
704             ## but wait to create the widget
705             $grid_row = 2;
706             }
707            
708             my $first_field;
709             ## Now add the fields
710             foreach my $field (@{$self->fields})
711             {
712             ## See if the widget was created
713             if (my $widget = $field->build_widget($win))
714             {
715             ## See if there's an error
716             my $err = $field->error;
717             if ($err)
718             {
719             ## Display the error message
720             $win->Label(
721             -text => $err,
722             -font => $self->error_font,
723             -anchor => qq{w},
724             -justify => qq{left},
725             -foreground => $self->error_font_color,
726             )
727             ->grid(
728             -row => $grid_row++,
729             -rowspan => 1,
730             -column => 0,
731             -columnspan => 2,
732             -sticky => qq{w},
733             );
734             }
735            
736             ## Create the label
737             my $label = $field->build_label($win);
738            
739             ## See if there's an error
740             if ($err)
741             {
742             ## Update the field's label to use the error marker, font,
743             ## and font color
744             $label->configure(
745             -text => $self->error_marker . qq{ } . $field->label . qq{:},
746             -font => $self->error_font,
747             -foreground => $self->error_font_color,
748             );
749             }
750            
751             ## Place the label
752             $label->grid(
753             -row => $grid_row,
754             -rowspan => 1,
755             -column => 0,
756             -columnspan => 1,
757             -sticky => qq{ne},
758             );
759            
760             ## Place the widget
761             $widget->grid(
762             -row => $grid_row,
763             -rowspan => 1,
764             -column => 1,
765             -columnspan => 1,
766             -sticky => qq{w},
767             );
768            
769             ## Increment the row index
770             $grid_row++;
771            
772             ## See if this is our first non-readonly field
773             if (!$first_field && !$field->readonly)
774             {
775             $first_field = $field;
776             }
777             }
778             }
779            
780             ## Use an empty frame as a spacer
781             $win->Frame(-height => 5)->grid(-row => $grid_row++);
782            
783             ## Create the button
784             my $button_text = $self->button_label;
785             my $underline = index($button_text, qq{&});
786             $button_text =~ s/\&//gx; ## Remove the &
787             $win->Button(
788             -text => $button_text,
789             -font => $self->button_font,
790             -width => length($button_text) + 2,
791             -command => sub {$result = 1;},
792             -underline => $underline,
793             )
794             ->grid(
795             -row => $grid_row++,
796             -rowspan => 1,
797             -column => 0,
798             -columnspan => 2,
799             -sticky => qq{},
800             );
801            
802             ## Set the form's message
803             $self->_set_message($win);
804            
805             $self->_watch_variable(\$result);
806            
807             ## Setup any keyboard bindings
808             $self->_set_key_bindings($win);
809            
810             ## Calculate the geometry
811             $self->_calc_geometry($win);
812            
813             ## Display the window
814             $win->deiconify;
815            
816             ## Detect user closing the window
817             $win->protocol('WM_DELETE_WINDOW',sub {$result = 0;});
818            
819             ## See if we are testing
820             if ($test)
821             {
822             ## Make sure the string is the correct format
823             if ($test =~ /TEST:\s+(\d)/x)
824             {
825             ## 0 == "CANCEL" 1 == "SUBMIT"
826             $test = $1;
827            
828             ## Set a callback to close the window
829             $win->after(1500, sub {$result = $test;});
830             }
831             }
832            
833             ## See if we have a first field specified
834             if ($first_field)
835             {
836             if ($first_field->is_type($ENTRY))
837             {
838             ## If this is an entry field, select the entire string
839             ## and place the cursor at the end of the string
840             $first_field->widget->selectionRange(0, 'end');
841             $first_field->widget->icursor('end');
842             }
843            
844             ## Set the focus to the field
845             $first_field->widget->focus();
846            
847             }
848             ## Wait for variable to change
849             $win->waitVariable(\$result);
850            
851             ## Hide the window
852             $win->withdraw();
853            
854             ## Clear all errors until form data is validated again
855             $self->clear_errors;
856            
857             if ($result)
858             {
859             ## Build the result
860             $result = {};
861             $result->{$_->key} = $_->value foreach (@{$self->fields});
862            
863             ## Validate each field
864             $_->validate() foreach (@{$self->fields});
865            
866             }
867             else
868             {
869             $result = undef;
870             }
871            
872             ## Destroy the window and all its widgets
873             $win->destroy();
874            
875             return($result);
876             }
877            
878             ##----------------------------------------------------------------------------
879             ## @fn _determine_dimensions($parent)
880             ## @brief Determine the overal dimensions of the given widgets
881             ## @param $parent - Refernce to parent widget
882             ## @return ($width, $height) - The width and height
883             ## @note
884             ##----------------------------------------------------------------------------
885             sub _determine_dimensions
886             {
887             my $parent = shift;
888             my @children = $parent->children;
889             my $max_width = 0;
890             my $max_height = 0;
891            
892             foreach my $widget (@children)
893             {
894             my ($width, $height, $x_pos, $y_pos) = split(/[x\+]/x, $widget->geometry());
895             $width += $x_pos;
896             $height += $y_pos;
897            
898             $max_width = $width if ($width > $max_width);
899             $max_height = $height if ($height > $max_height);
900            
901             }
902            
903             return($max_width, $max_height);
904             }
905            
906             ##----------------------------------------------------------------------------
907             ## @fn _calc_geometry($parent)
908             ## @brief Calculate window geometry to place the given window in the center
909             ## of the screen
910             ## @param $parent - Reference to the Main window widget
911             ## @return void
912             ## @note
913             ##----------------------------------------------------------------------------
914             sub _calc_geometry
915             {
916             my $self = shift;
917             my $parent = shift;
918            
919             return if (!defined($parent));
920             return if (ref($parent) ne "MainWindow");
921            
922             ## Allow the geometry manager to update all sizes
923             $parent->update();
924            
925             ## Determine the windows dimensions
926             my ($width, $height) = _determine_dimensions($parent);
927            
928             ## Determine the width and make sure it is at least $self->min_width
929             $width = $self->min_width if ($width < $self->min_width);
930            
931             ## Determine the height and make sure it is at least $self->min_height
932             $height = $self->min_height if ($height < $self->min_height);
933            
934             ## Calculate the X and Y to center on the screen
935             my $pos_x = int(($parent->screenwidth - $width) / 2);
936             my $pos_y = int(($parent->screenheight - $height) / 2);
937            
938             ## Update the geometry with the calculated values
939             $parent->geometry("${width}x${height}+${pos_x}+${pos_y}");
940            
941             return;
942             }
943            
944             ##----------------------------------------------------------------------------
945             ## @fn _set_key_bindings($win)
946             ## @brief Set key bindings for the given window
947             ## @param $win - Window to use for binding keyboard events
948             ## @return NONE
949             ## @note
950             ##----------------------------------------------------------------------------
951             sub _set_key_bindings
952             {
953             my $self = shift;
954             my $win = shift;
955            
956             ## Now add the "hot key"
957             my $button_text = $self->button_label;
958             my $underline = index($button_text, qq{&});
959             if ($underline >= 0)
960             {
961             my $keycap = lc(substr($button_text, $underline + 1, 1));
962            
963             $win->bind(qq{} => sub {${$self->_watch_variable} = 1;});
964             }
965            
966             ## See if option set
967             if ($self->submit_on_enter)
968             {
969             $win->bind(qq{} => sub {${$self->_watch_variable} = 1;});
970             }
971            
972             ## See if option set
973             if ($self->cancel_on_escape)
974             {
975             $win->bind(qq{} => sub {${$self->_watch_variable} = 0;});
976             }
977            
978             return;
979             }
980            
981             ##----------------------------------------------------------------------------
982             ## @fn _set_message($win)
983             ## @brief Set the message at the top of the form's window
984             ## @param $win - Window object
985             ## @return NONE
986             ## @note
987             ##----------------------------------------------------------------------------
988             sub _set_message
989             {
990             my $self = shift;
991             my $win = shift;
992            
993             ## See if we have a message
994             if ($self->message)
995             {
996             ## To keep the message from making the dialog box too
997             ## large, we will look at the current window width and
998             ## wrap the message accordingly
999            
1000             ## Allow gemoetry manager to calculate all widgets
1001             $win->update;
1002            
1003             ## Determine number of rows and columns in the grid
1004             my ($columns, $rows) = $win->gridSize();
1005            
1006             ## Use the dialog's minimum width as the starting point
1007             my $max_x = $self->min_width;
1008            
1009             ## Iterate through all rows and columns
1010             my $row = 0;
1011             while ($row < $rows)
1012             {
1013             my $col = 0;
1014             while ($col < $columns)
1015             {
1016             ## Get the bounding box of the widget
1017             my ($x, $y, $width, $height) = $win->gridBbox($col, $row);
1018             ## Get the max x of the widget
1019             $x += $width;
1020             ## See if this is larger than our current max x
1021             $max_x = $x if ($x > $max_x);
1022            
1023             ## Increment the colums
1024             $col++;
1025             }
1026             ## Increment the rows
1027             $row++;
1028             }
1029            
1030             ## Create a label widget
1031             $win->Label(
1032             -wraplength => $max_x,
1033             -text => $self->message,
1034             -justify => qq{left},
1035             -font => $self->message_font,
1036             )
1037             ->grid(
1038             -row => 0,
1039             -rowspan => 1,
1040             -column => 0,
1041             -columnspan => 2,
1042             -sticky => qq{},
1043             );
1044            
1045             ## Use an empty frame as a spacer
1046             $win->Frame(-height => 5)->grid(-row => 1);
1047             }
1048            
1049             return;
1050             }
1051            
1052            
1053            
1054             ##****************************************************************************
1055             ##****************************************************************************
1056            
1057             =head2 initialize($param)
1058            
1059             =over 2
1060            
1061             =item B
1062            
1063             initialize the form from a HASH reference, JSON string, or JSON file.
1064             In all cases, the hash should have the following format
1065            
1066             {
1067             title => 'My Form',
1068             fields => [
1069             {
1070             type => 'Entry',
1071             key => 'name',
1072             label => 'Name',
1073             },
1074             {
1075             type => 'Radiobutton',
1076             key => 'sex',
1077             label => 'Gender',
1078             choices => [
1079             {
1080             label => 'Male',
1081             value => 'male',
1082             },
1083             {
1084             label => 'Female',
1085             value => 'female',
1086             },
1087             ],
1088             }
1089             ]
1090             }
1091            
1092             =item B
1093            
1094             $param - HASH reference, or scalar containin JSON string, or filename
1095            
1096             =item B
1097            
1098             NONE
1099            
1100             =back
1101            
1102             =cut
1103            
1104             ##----------------------------------------------------------------------------
1105             sub initialize
1106             {
1107             my $self = shift;
1108             my $param = shift;
1109            
1110             unless (ref($param))
1111             {
1112             my $str = qq{};
1113             if (-f qq{$param})
1114             {
1115             if (open(my $fh, qq{<}, $param))
1116             {
1117             ## Read the file
1118             while (my $line = <$fh>)
1119             {
1120             ## trim leading whitespace
1121             $line =~ s/^\s+//x;
1122             ## trim trailing whitespace
1123             $line =~ s/\s+$//x;
1124            
1125             ## See if this is a comment and should be ignored
1126             next if ($line =~ /^[#;]/x);
1127            
1128             ## Add this line to the option string
1129             $str .= $line . qq{ };
1130             }
1131             close($fh);
1132             }
1133             }
1134             else
1135             {
1136             $str = $param;
1137             }
1138            
1139             try
1140             {
1141             $param = JSON->new->utf8(1)->relaxed->decode($str);
1142             };
1143             }
1144            
1145             $self->_import_hash($param);
1146            
1147             ## Return object to allow chaining
1148             return $self;
1149             }
1150            
1151             ##----------------------------------------------------------------------------
1152             ## @fn _import_hash($hash)
1153             ## @brief Load a form using the hash parameters
1154             ## @param $param - Hash reference
1155             ## @return NONE
1156             ## @note
1157             ##----------------------------------------------------------------------------
1158             sub _import_hash
1159             {
1160             my $self = shift;
1161             my $param = shift;
1162            
1163             ## Import the "simple" non-array attributes
1164             foreach my $attr (@SIMPLE_ATTRIBUTES)
1165             {
1166             $self->$attr($param->{$attr}) if (exists($param->{$attr}));
1167             }
1168            
1169             ## Import the fields
1170             if (exists($param->{fields}) && (ref($param->{fields}) eq qq{ARRAY}))
1171             {
1172             foreach my $entry (@{$param->{fields}})
1173             {
1174             unless (my $field = $self->add_field(%{$entry}))
1175             {
1176             cluck(
1177             qq{Unable to create a field\n},
1178             Data::Dumper->Dump([$entry], [qw(entry)]),
1179             qq{\n}
1180             );
1181             }
1182             }
1183             }
1184            
1185             return;
1186             }
1187            
1188             ##****************************************************************************
1189             ##****************************************************************************
1190            
1191             =head2 set_field_data($hash)
1192            
1193             =over 2
1194            
1195             =item B
1196            
1197             Use the key/values of the provided hash to set the corresponding field
1198             values
1199            
1200             =item B
1201            
1202             $hash - Hash reference containing key /values whose keys correspnd to the
1203             various field keys
1204            
1205             =item B
1206            
1207             NONE
1208            
1209             =back
1210            
1211             =cut
1212            
1213             ##----------------------------------------------------------------------------
1214             sub set_field_data
1215             {
1216             my $self = shift;
1217             my $hash = shift;
1218            
1219             ## Silently return if we did not receive a parameter
1220             return if (!defined($hash));
1221            
1222             ## Bail out if the parameter is NOT a hash reference
1223             confess(qq{Expected a HASH reference!}) unless (ref($hash) eq qq{HASH});
1224            
1225             foreach my $key (keys(%{$hash}))
1226             {
1227             my $found;
1228             INNER_FIELD_LOOP:
1229             foreach my $field (@{$self->fields})
1230             {
1231             if ($key eq $field->key)
1232             {
1233             $field->default($hash->{$key});
1234             $found = 1;
1235             last INNER_FIELD_LOOP;
1236             }
1237             }
1238             }
1239            
1240             return;
1241             }
1242            
1243             ##****************************************************************************
1244             ##****************************************************************************
1245            
1246             =head2 clear_errors()
1247            
1248             =over 2
1249            
1250             =item B
1251            
1252             Clear errors on all form fields
1253            
1254             =item B
1255            
1256             NONE
1257            
1258             =item B
1259            
1260             NONE
1261            
1262             =back
1263            
1264             =cut
1265            
1266             ##----------------------------------------------------------------------------
1267             sub clear_errors
1268             {
1269             my $self = shift;
1270            
1271             ## Clear all field errors
1272             $_->error(qq{}) foreach (@{$self->fields});
1273            
1274             return($self);
1275             }
1276            
1277             ##****************************************************************************
1278             ##****************************************************************************
1279            
1280             =head2 field_by_key($key)
1281            
1282             =over 2
1283            
1284             =item B
1285            
1286             Return the field associated with the provided key or UNDEF if not found.
1287            
1288             =item B
1289            
1290             $key - The key associated with the desired field
1291            
1292             =item B
1293            
1294             UNDEF if not found, or a Tk::FormUI field object
1295            
1296             =back
1297            
1298             =cut
1299            
1300             ##----------------------------------------------------------------------------
1301             sub field_by_key
1302             {
1303             my $self = shift;
1304             my $key = shift // qq{};
1305            
1306             return unless($key);
1307            
1308             foreach my $field (@{$self->fields})
1309             {
1310             return($field) if ($key eq $field->key);
1311             }
1312            
1313             return;
1314             }
1315            
1316             ##****************************************************************************
1317             ##****************************************************************************
1318            
1319             =head2 error_by_key($key, $error)
1320            
1321             =over 2
1322            
1323             =item B
1324            
1325             Set the error for the field associated with the given key
1326            
1327             =item B
1328            
1329             $key - The key associated with the desired field
1330            
1331             $error - Error message for the given field
1332            
1333             =item B
1334            
1335             NONE
1336            
1337             =back
1338            
1339             =cut
1340            
1341             ##----------------------------------------------------------------------------
1342             sub error_by_key
1343             {
1344             my $self = shift;
1345             my $key = shift;
1346             my $error = shift // qq{};
1347            
1348             if (my $field = $self->field_by_key($key))
1349             {
1350             $field->error($error);
1351             return($error);
1352             }
1353            
1354             return;
1355             }
1356            
1357             ##****************************************************************************
1358             ##****************************************************************************
1359            
1360             =head2 has_errors()
1361            
1362             =over 2
1363            
1364             =item B
1365            
1366             Returns TRUE if any field in the form has an error
1367            
1368             =item B
1369            
1370             NONE
1371            
1372             =item B
1373            
1374             TRUE if any field has an error
1375            
1376             =back
1377            
1378             =cut
1379            
1380             ##----------------------------------------------------------------------------
1381             sub has_errors
1382             {
1383             my $self = shift;
1384            
1385             foreach my $field (@{$self->fields})
1386             {
1387             return(1) if ($field->error);
1388             }
1389            
1390             return;
1391             }
1392            
1393            
1394             ##****************************************************************************
1395             ## Additional POD documentation
1396             ##****************************************************************************
1397            
1398             =head1 AUTHOR
1399            
1400             Paul Durden Ealabamapaul AT gmail.comE
1401            
1402             =head1 COPYRIGHT & LICENSE
1403            
1404             Copyright (C) 2015 by Paul Durden.
1405            
1406             This program is free software; you can redistribute it and/or modify it
1407             under the same terms as Perl itself.
1408            
1409             =cut
1410            
1411             1; ## End of module
1412             __END__