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.07
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   40956 use Moo;
  2         27649  
  2         11  
59             ## Moo enables strictures
60             ## no critic (TestingAndDebugging::RequireUseStrict)
61             ## no critic (TestingAndDebugging::RequireUseWarnings)
62 2     2   4462 use Readonly;
  2         5705  
  2         95  
63 2     2   10 use Carp qw(confess cluck);
  2         4  
  2         119  
64 2     2   2816 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.07};
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__