————————package
Tk::QuickForm;
=head1 NAME
Tk::QuickForm - Quickly set up a form.
=cut
use
strict;
use
warnings;
$VERSION
=
'0.09'
;
use
Tk;
Construct Tk::Widget
'QuickForm'
;
require
Tk::LabFrame;
require
Tk::NoteBook;
require
Tk::PopColor;
#require Tk::FilePicker;
use
Tk::PNG;
=head1 SYNOPSIS
require Tk::QuickForm;
my $form= $window->QuickForm(@options,
-structure => [
'*page' => 'Page name', #Adds a new page to the notebook. Creates the notebook if needed.
'*section' => 'Section name', #Adds a new section.
some_list => ['radio', 'My values', -values => \@listvalues], #Adds an array of radio buttons.
'*end', #Ends a section or frame.
'*column', #Starts a new column of fields
'*expand', #Expand the next entry in height.
]
)->pack;
$form->createForm;
=head1 DESCRIPTION
This widget allows you to quickly set up a form for the user to fill out or modify.
Attempts are made to make it clear and elegant.
Inherits L<Tk::Frame>. With the B<-structure> option you can define
fields the user can modify as well as its layout.
With the B<put> and B<get> methods you can set or retrieve values as a hash.
=head1 OPTIONS
=over 4
=item Switch: B<-acceptempty>
Default value 0. If set the validate method will not trigger on
fields containing empty strings.
=item Switch: B<-autovalidate>
By default 1. Validate the form whenever an entry changes value.
=item Switch: B<-fileimage>
Set an image object for I<file> items. By default it is the file icon from Tk.
=item Switch: B<-folderimage>
Set an image object for I<folder> items. By default it is the folder icon from Tk.
=item Switch: B<-fontimage>
Set an image object for I<folder> items. By default it is the I<font_icon.png> in this package.
=item Switch: B<-postvalidatecall>
Set this callback if you want to take action on the validation result.
=item Switch: B<-structure>
You have to set this option. This is the example we use in our testfile:
[
*page' => 'Arrays',
'*section' => 'List',
-set_list_command => ['list', 'List command test', -values => sub { return @listvalues } ],
-set_list_values => ['list', 'List values test', -values => \@listvalues],
'*end',
'*section' => 'Radio',
-set_radio_command => ['radio', 'Radio Command test', -values => sub { return @radiovalues }],
-set_radio_values => ['radio', 'Radio values test', -values => \@radiovalues],
'*end',
'*page' => 'Scalars',
'*section' => 'Numbers',
-set_boolean => ['boolean', 'Boolean test'],
-set_float => ['float', 'Float test'],
-set_integer => ['integer', 'Integer test'],
'*end',
'*section' => 'Scale and Spin',
-set_scale => ['scale', 'Scale test'],
-set_spin => ['spin', 'Spinbox test'],
'*end',
'*section' => 'Files',
-set_file => ['file', 'File test'],
-set_folder => ['folder', 'Folder test'],
'*end',
'*column',
'*section' => 'Colors and fonts',
-set_color => ['color', 'Color test'],
-set_font => ['font', 'Font test'],
'*end',
'*section' => 'Free text',
-set_text => ['text', 'Text test'],
'*end',
'*page' => 'User defined',
'*section' => 'Scale',
-set_scale10 => ['scale10', 'Scale 10 test'],
'*end',
'*section' => 'Boolean',
-set_onoff => ['onoff', 'On/Off 10 test', -offvalue => 'Uit', -onvalue => 'Aan'],
'*end',
'*section' => 'Text',
-set_https => ['https', 'Web link'],
'*end',
'*page' => 'External',
'*section' => 'Single column',
-set_ext1 => ['ext1', 'External color test', 'Tk::QuickForm::CColorItem'],
'*end',
'*expand',
'*section' => 'Double column',
'*expand',
-set_ext2 => ['ext2', 'MyExternal', -height => 8, -width => 40],
'*end',
],
Only available at create time. See below.
=item Switch: B<-types>
Add a list of user defined types.
Only available at create time. See below.
=back
=head1 FILE DIALOG OPTIONS
A number of config variables are forwarded to the file dialog widget used
in this module. Look in L<Tk::FilePicker> and L<Tk::FileBrowser> for their meaning.
They are:
=over 4
=item B<-diriconcall>
=item B<-fileiconcall>
=item B<-linkiconcall>
=item B<-msgimage>
=item B<-newfolderimage>
=item B<-reloadimage>
=item B<-warnimage>
=back
=head1 THE STRUCTURE OPTION
The I<-structure> option is a list that basically looks like:
[
$switch => $option,
$key => [$type, $label, @options],
...
]
B<SWITCHES>
$switch can have the following values:
=over 4
=item B<*page>
Creates a NoteBook widget if needed and adds a page with the name in $option.
=item B<*section>
Creates a new section with the name in $option as label on a L<Tk::LabFrame>.
You can create nested sections.
=item B<*frame>
Similar to B<*section> just does not take a name parameter. Useful for dividing
the space.
=item B<*column>
Starts a new set of colums at row 0; Does not take a parameter.
=item B<*end>
Ends current section. Does not take a parameter.
=item B<*expand>
Verically expands the next item in the list. Does not take a parameter.
=back
B<TYPES>
By default the following types are defined:
=over 4
=item B<boolean>
mycheck => ['boolean', 'My check', @options],
Creates a Checkbutton item.
=item B<color>
mycolor => ['color', 'My color', @options],
Creates an ColorEntry item. See L<Tk::ColorEntry>.
=item B<ext1>
myexternal => ['ext1', 'My external', 'Full::Class::Name', @options],
Adds an external class. See below at B<USER DEFINED TYPES>.
=item B<ext2>
myexternal => ['ext2', 'Full::Class::Name', @options],
Same as B<ext1> except it occupies all columns. See below at B<USER DEFINED TYPES>.
=item B<file>
myfile => ['file', 'My file', @options],
Creates an Entry item with a button initiating a file dialog.
=item B<float>
myfloat => ['float', 'My float', @options],
Creates an Entry item that validates a floating value.
=item B<folder>
mydir => ['folder', 'My folder', @options],
Creates an Entry item with a button initiating a folder dialog.
=item B<font>
myfont => ['font', 'My font', @options],
Creates an Entry item with a button initiating a font dialog.
=item B<integer>
myinteger => ['integer', 'My integer', @options],
Creates an Entry item that validates an integer value.
=item B<list>
mylist => ['list', 'My list', -values => \@values],
mylist => ['list', 'My list', -values => sub { return @values }],
Creates a ListEntry item. See L<Tk::ListEntry>.
=item B<radio>
myradio => ['radio', 'My radio', -values => \@values],
myradio => ['radio', 'My radio', -values => sub { return @values }],
Creates a row of radiobuttons.
=item B<scale>
myscale => ['scale', 'My scale', @options],
Creates a Scale item.
=item B<spin>
myspin => ['spin', 'My spin', @options],
Creates a Spinbutton item.
=item B<text>
mytext => ['text', 'My text', @options],
Creates an Entry item.
=back
B<USER DEFINED TYPES>
The B<-types> option lets you add your own item types.
Specify as follows:
$window->QuickForm(
-structure => [..],
-types => [
type1 => ['ClassName1', @options1]
type2 => ['ClassName2', @options2]
]
);
This may or make not make life easier.
You may prefer to use a class derived from L<Tk::QuickForm::CBaseClass>. However, any class will do as long as:
- It is a valid Tk megawidget
- It accepts a callback option B<-validatecall>,
- It has a B<put>, B<get>, and a B<validate> method.
This also applies to external classes in the B<ext1> and B<ext2> types.
=cut
sub
Populate {
my
(
$self
,
$args
) =
@_
;
my
$types
=
delete
$args
->{
'-types'
};
$self
->SUPER::Populate(
$args
);
$self
->{LABELS} = {};
$self
->{OPTIONS} = {};
$self
->{TYPES} = {
boolean
=> [
'Tk::QuickForm::CBooleanItem'
,
-onvalue
=> 1,
-offvalue
=> 0],
color
=> [
'Tk::QuickForm::CColorItem'
],
file
=> [
'Tk::QuickForm::CFileItem'
,
-image
=>
'-fileimage'
],
float
=> [
'Tk::QuickForm::CFloatItem'
, ],
folder
=> [
'Tk::QuickForm::CFolderItem'
,
-image
=>
'-folderimage'
],
font
=> [
'Tk::QuickForm::CFontItem'
,
-image
=>
'-fontimage'
],
'integer'
=> [
'Tk::QuickForm::CTextItem'
,
-regex
=>
'^-?\d+$'
],
list
=> [
'Tk::QuickForm::CListItem'
],
radio
=> [
'Tk::QuickForm::CRadioItem'
],
scale
=> [
'Tk::QuickForm::CScaleItem'
,
-from
=> 0,
-to
=> 100],
spin
=> [
'Tk::QuickForm::CSpinItem'
,
-from
=> 0,
-to
=> 100,
-regex
=>
'^-?\d+$'
],
text
=> [
'Tk::QuickForm::CTextItem'
],
};
if
(
defined
$types
) {
while
(
@$types
) {
my
$opt
=
shift
@$types
;
my
$val
=
shift
@$types
;
$self
->{TYPES}->{
$opt
} =
$val
;
}
}
$self
->gridColumnconfigure(0,
-weight
=> 1);
$self
->gridColumnconfigure(1,
-weight
=> 1);
my
$fil_icon
=
$self
->Pixmap(
-file
=> Tk->findINC(
'file.xpm'
));
my
$dir_icon
=
$self
->Pixmap(
-file
=> Tk->findINC(
'folder.xpm'
));
my
$fon_icon
=
$self
->Photo(
-file
=> Tk->findINC(
'font_icon.png'
),
-format
=>
'PNG'
);
$self
->ConfigSpecs(
-acceptempty
=> [
'PASSIVE'
,
undef
,
undef
, 0],
-autovalidate
=> [
'PASSIVE'
,
undef
,
undef
, 1],
-background
=> [
'SELF'
,
'DESCENDANTS'
],
-colorhistoryfile
=> [
'PASSIVE'
],
-diriconcall
=> [
'PASSIVE'
],
-fileiconcall
=> [
'PASSIVE'
],
-fileimage
=> [
'PASSIVE'
,
undef
,
undef
,
$fil_icon
],
-folderimage
=> [
'PASSIVE'
,
undef
,
undef
,
$dir_icon
],
-fontimage
=> [
'PASSIVE'
,
undef
,
undef
,
$fon_icon
],
-linkiconcall
=> [
'PASSIVE'
],
-listcall
=> [
'CALLBACK'
,
undef
,
undef
,
sub
{}],
-msgimage
=> [
'PASSIVE'
],
-newfolderimage
=> [
'PASSIVE'
],
-postvalidatecall
=> [
'CALLBACK'
,
undef
,
undef
,
sub
{}],
-reloadimage
=> [
'PASSIVE'
],
-structure
=> [
'PASSIVE'
,
undef
,
undef
, []],
-tabside
=> [
'PASSIVE'
,
undef
,
undef
,
'top'
],
-warnimage
=> [
'PASSIVE'
],
DEFAULT
=> [
'SELF'
],
);
}
=head1 METHODS
=over 4
=cut
sub
CreateClass {
my
$self
=
shift
;
my
$holder
=
shift
;
my
$class
=
shift
;
eval
"require $class"
;
while
(
$class
=~ s/.*[\:\:]//) {}
return
$holder
->
$class
(
@_
);
}
=item B<createForm>
Call this method after you created the B<Tk::QuickForm> widget.
It will create all the pages, sections and entries.
=cut
sub
createForm {
my
$self
=
shift
;
my
@holderstack
= ({
offset
=> 0,
holder
=>
$self
,
row
=> 0,
type
=>
'root'
,
});
my
$notebook
;
my
$popcolor
;
my
$structure
=
$self
->cget(
'-structure'
);
my
@options
=
@$structure
;
my
$labelwidth
= 0;
while
(
@options
) {
my
$key
=
shift
@options
;
if
((
$key
eq
'*page'
) or (
$key
eq
'*section'
)) {
shift
@options
;
next
;
}
next
if
(
$key
eq
'*end'
) or (
$key
eq
'*column'
) or (
$key
eq
'*expand'
) or (
$key
eq
'*frame'
);
my
$conf
=
shift
@options
;
my
$l
=
length
$conf
->[1];
unless
(
$conf
->[0] eq
'ext2'
) {
$labelwidth
=
$l
if
$l
>
$labelwidth
; }
}
my
%options
= ();
my
%labels
= ();
my
@padding
= (
-padx
=> 2,
-pady
=> 2);
@options
=
@$structure
;
while
(
@options
) {
my
$key
=
shift
@options
;
if
(
$key
eq
'*page'
) {
my
$label
=
shift
@options
;
my
$holder
=
$holderstack
[0]->{holder};
my
$row
=
$holderstack
[0]->{row};
unless
(
defined
$notebook
) {
$holder
->gridRowconfigure(
$row
,
-weight
=> 1);
$notebook
=
$holder
->NoteBook(
# -borderwidth => 1,
)->grid(
-column
=> 0,
-row
=>
$row
,
-columnspan
=> 2,
-sticky
=>
'nsew'
);
$self
->{NOTEBOOK} =
$notebook
;
}
my
$page
=
$notebook
->add(
$label
,
-label
=>
$label
);
my
$h
=
$page
->Frame->
pack
(
-fill
=>
'x'
);
$h
->gridColumnconfigure(1,
-weight
=> 1);
$holderstack
[0]->{row} ++;
unshift
@holderstack
, {
holder
=>
$h
,
offset
=> 0,
row
=> 0,
type
=>
'page'
,
};
}
elsif
(
$key
eq
'*section'
) {
my
$label
=
shift
@options
;
my
$h
=
$holderstack
[0];
my
$offset
=
$h
->{offset};
my
$lf
=
$h
->{holder}->LabFrame(
-label
=>
$label
,
-labelside
=>
'acrosstop'
,
)->grid(
@padding
,
-column
=> 0 +
$offset
,
-row
=>
$h
->{row},
-columnspan
=> 2,
-sticky
=>
'nsew'
);
$lf
->gridColumnconfigure(1,
-weight
=> 1);
$holderstack
[0]->{row} ++;
unshift
@holderstack
, {
holder
=>
$lf
,
offset
=> 0,
row
=> 0,
type
=>
'section'
,
};
}
elsif
(
$key
eq
'*frame'
) {
my
$h
=
$holderstack
[0];
my
$offset
=
$h
->{offset};
my
$f
=
$h
->{holder}->Frame->grid(
@padding
,
-column
=> 0 +
$offset
,
-row
=>
$h
->{row},
-columnspan
=> 2,
-sticky
=>
'nsew'
,
);
$f
->gridColumnconfigure(1,
-weight
=> 1);
$holderstack
[0]->{row} ++;
unshift
@holderstack
, {
holder
=>
$f
,
offset
=> 0,
row
=> 0,
type
=>
'frame'
,
};
}
elsif
(
$key
eq
'*column'
) {
my
$offset
=
$holderstack
[0]->{offset};
$offset
=
$offset
+ 2;
$holderstack
[0]->{offset} =
$offset
;
$holderstack
[0]->{row} = 0;
my
$h
=
$holderstack
[0]->{holder};
$h
->gridColumnconfigure(1 +
$offset
,
-weight
=> 1);
}
elsif
(
$key
eq
'*end'
) {
if
(
$holderstack
[0]->{type} eq
'page'
) {
$notebook
=
undef
}
if
(
@holderstack
> 1) {
shift
@holderstack
}
else
{
warn
"Holder stack is already empty"
}
}
elsif
(
$key
eq
'*expand'
) {
my
$holder
=
$holderstack
[0]->{holder};
my
$row
=
$holderstack
[0]->{row};
my
$count
= 0;
while
(
defined
$holderstack
[
$count
]) {
my
$htype
=
$holderstack
[
$count
]->{type};
if
(
$htype
eq
'page'
) {
my
$h
=
$holderstack
[
$count
]->{holder};
$h
->packForget;
$h
->
pack
(
-expand
=> 1,
-fill
=>
'both'
);
last
}
$count
++;
}
$holder
->gridRowconfigure(
$row
,
-weight
=> 1);
}
else
{
my
$conf
=
shift
@options
;
my
@opt
=
@$conf
;
my
$type
=
shift
@opt
;
my
$row
=
$holderstack
[0]->{row};
my
$holder
=
$holderstack
[0]->{holder};
my
$offset
=
$holderstack
[0]->{offset};
if
(
$type
eq
'color'
) {
my
$file
=
$self
->cget(
'-colorhistoryfile'
);
$popcolor
=
$self
->PopColor()
unless
defined
$popcolor
;
push
@opt
,
-popcolor
=>
$popcolor
,
-historyfile
=>
$file
;
}
if
(
$type
eq
'ext1'
) {
my
$label
=
shift
@opt
;
my
$class
=
shift
@opt
;
my
$l
=
$holder
->Label(
-width
=>
$labelwidth
,
-text
=>
$label
,
-anchor
=>
'e'
)->grid(
@padding
,
-column
=> 0 +
$offset
,
-row
=>
$row
,
-sticky
=>
'e'
);
my
$w
=
$self
->CreateClass(
$holder
,
$class
,
-quickform
=>
$self
,
-validatecall
=> [
'validate'
,
$self
],
@opt
);
$w
->grid(
-column
=> 1 +
$offset
,
-row
=>
$row
,
-sticky
=>
'nsew'
,
-padx
=> 2,
-pady
=> 2
);
$options
{
$key
} =
$w
;
$labels
{
$key
} =
$l
;
}
elsif
(
$type
eq
'ext2'
) {
# $label is now actually the reference to the external object
# $values is now actually a boolean scalar to instruct the widget to expand
my
$class
=
shift
@opt
;
my
$w
=
$self
->CreateClass(
$holder
,
$class
,
-validatecall
=> [
'validate'
,
$self
],
@opt
);
$w
->grid(
-column
=> 0,
-row
=>
$row
,
-columnspan
=> 2 +
$offset
,
-sticky
=>
'nsew'
,
-padx
=> 2,
-pady
=> 2
);
$options
{
$key
} =
$w
;
}
else
{
my
$label
=
shift
@opt
;
my
$l
=
$holder
->Label(
-width
=>
$labelwidth
,
-text
=>
$label
,
-anchor
=>
'e'
)->grid(
@padding
,
-column
=> 0 +
$offset
,
-row
=>
$row
,
-sticky
=>
'e'
);
my
$t
=
$self
->{TYPES}->{
$type
};
my
@o
=
@$t
;
my
$class
=
shift
@o
;
my
%opthash
= (
@o
);
while
(
@opt
) {
my
$key
=
shift
@opt
;
my
$value
=
shift
@opt
;
$opthash
{
$key
} =
$value
;
}
$opthash
{
'-validatecall'
} = [
'validate'
,
$self
];
my
$img
=
$opthash
{
'-image'
};
if
((
defined
$img
) and (
$img
=~ /^-/)) {
$opthash
{
'-image'
} =
$self
->cget(
$img
)
}
if
(
exists
$opthash
{
'-values'
}) {
my
$values
=
$opthash
{
'-values'
};
if
((
ref
$values
) and (
$values
=~/^CODE/)) {
my
@vals
=
&$values
;
$opthash
{
'-values'
} = \
@vals
;
}
}
my
$widg
=
$self
->CreateClass(
$holder
,
$class
,
%opthash
,
-quickform
=>
$self
)->grid(
-column
=> 1 +
$offset
,
-row
=>
$row
,
-sticky
=>
'nsew'
,
-padx
=> 2,
-pady
=> 2
);
$options
{
$key
} =
$widg
;
$labels
{
$key
} =
$l
;
$holderstack
[0]->{row} ++;
}
}
}
$self
->{OPTIONS} = \
%options
;
$self
->{LABELS} = \
%labels
;
$self
->{NOTEBOOK} =
$notebook
;
$self
->
after
(10, [
'validate'
,
$self
]);
return
$notebook
;
}
sub
DefineTypes {
my
$self
=
shift
;
while
(
@_
) {
my
$type
=
shift
;
my
$conf
=
shift
;
$self
->{TYPES}->{
$type
} =
$conf
;
}
}
=item B<get>I<(?$key?)>
Returns the value of $key. $key is the name of the item in the form.
Returns a hash with all values if $key is not specified.
=cut
sub
get {
my
(
$self
,
$key
) =
@_
;
my
$opt
=
$self
->{OPTIONS};
return
$opt
->{
$key
}->get
if
(
defined
$key
) and (
exists
$opt
->{
$key
});
if
(
defined
$key
) {
warn
"Invalid key $key"
;
return
}
my
@get
= ();
for
(
keys
%$opt
) {
push
@get
,
$_
,
$opt
->{
$_
}->get
}
return
@get
}
sub
getFilePicker {
my
$self
=
shift
;
my
$pick
=
$self
->Subwidget(
'picker'
);
return
$pick
if
defined
$pick
;
my
%options
= ();
for
(
'-diriconcall'
,
'-fileiconcall'
,
'-linkiconcall'
,
'-msgimage'
,
'-newfolderimage'
,
'-reloadimage'
,
'-warnimage'
) {
my
$opt
=
$self
->cget(
$_
);
$options
{
$_
} =
$opt
if
defined
$opt
}
$pick
=
$self
->FilePicker(
%options
);
$self
->Advertise(
'picker'
,
$pick
);
return
$pick
;
}
sub
getKeys {
my
$self
=
shift
;
my
$opt
=
$self
->{OPTIONS};
return
sort
keys
%$opt
;
}
sub
getLabel {
my
(
$self
,
$name
) =
@_
;
return
$self
->{LABELS}->{
$name
};
}
sub
getNotebook {
return
$_
[0]->{NOTEBOOK} }
sub
getWidget {
my
(
$self
,
$name
) =
@_
;
return
$self
->{OPTIONS}->{
$name
};
}
sub
pick {
my
$self
=
shift
;
my
$pick
=
$self
->getFilePicker;
$pick
->pick(
@_
);
}
sub
pickFile {
my
$self
=
shift
;
my
%args
=
@_
;
return
$self
->pick(
-checkoverwrite
=> 0,
-showfolders
=> 1,
-showfiles
=> 1,
-selectmode
=>
'single'
,
-selectstring
=>
'Select'
,
-title
=>
'Select file'
,
%args
,
);
}
sub
pickFolder {
my
$self
=
shift
;
my
%args
=
@_
;
return
$self
->pick(
-checkoverwrite
=> 0,
-showfolders
=> 1,
-showfiles
=> 0,
-selectmode
=>
'single'
,
-selectstring
=>
'Select'
,
-title
=>
'Select folder'
,
%args
,
);
}
=item B<put>(%values)
Sets the values in the tabbed form
=cut
sub
put {
my
$self
=
shift
;
my
$opt
=
$self
->{OPTIONS};
while
(
@_
) {
my
$key
=
shift
;
my
$value
=
shift
;
if
(
exists
$opt
->{
$key
}) {
$opt
->{
$key
}->put(
$value
)
}
else
{
warn
"Invalid key $key"
}
}
}
=item B<validate>
=over 4
validates all entries in the form and returns true if
all successfull.
=back
=cut
sub
validate {
my
(
$self
,
$key
) =
@_
;
my
$opt
=
$self
->{OPTIONS};
my
$valid
= 1;
for
(
keys
%$opt
) {
my
$w
=
$opt
->{
$_
};
next
if
(
$self
->cget(
'-acceptempty'
) and (
$w
->get eq
''
));
$valid
= 0
unless
$w
->validate;
}
$self
->Callback(
'-postvalidatecall'
,
$valid
);
return
$valid
}
=back
=head1 LICENSE
Same as Perl.
=head1 AUTHOR
Hans Jeuken (hanje at cpan dot org)
=head1 BUGS
Unknown. If you find any, please contact the author.
=head1 SEE ALSO
=over 4
=item L<Tk::QuickForm::CBaseClass>
=item L<Tk::QuickForm::CBooleanItem>
=item L<Tk::QuickForm::CColorItem>
=item L<Tk::QuickForm::CFileItem>
=item L<Tk::QuickForm::CFloatItem>
=item L<Tk::QuickForm::CFolderItem>
=item L<Tk::QuickForm::CFontItem>
=item L<Tk::QuickForm::CListItem>
=item L<Tk::QuickForm::CRadioItem>
=item L<Tk::QuickForm::CScaleItem>
=item L<Tk::QuickForm::CSpinItem>
=item L<Tk::QuickForm::CTextItem>
=back
=cut
1;
__END__