package Tkx::FindBar; use strict; use warnings; use Carp qw'carp'; use Tkx; use base qw(Tkx::widget Tkx::MegaConfig); our $VERSION = '0.07'; __PACKAGE__->_Mega("tkx_FindBar"); __PACKAGE__->_Config( -textwidget => ['METHOD'], -highlightcolor => ['METHOD'], ); my $initialized; my $tile_available; my %hidesub = ( pack => \&_hide_pack, grid => \&_hide_grid, place => \&_hide_place, ); my %showsub = ( pack => \&_show_pack, grid => \&_show_grid, place => \&_show_place, ); #------------------------------------------------------------------------------- # Subroutine : _ClassInit # Purpose : Perform class initialization. # Notes : #------------------------------------------------------------------------------- sub _ClassInit { # determine availability of themed widgets $tile_available = eval { Tkx::package_require('tile') }; # load images for toolbar buttons my $icofmt = eval { Tkx::package_require('img::png') } ? 'png' : 'gif89'; while () { next if /^#/; next if /^\s*$/; last if /^__END__/; chomp; my ($name, $fmt, $data) = (split /:/)[0,1,4]; next unless $fmt eq $icofmt; Tkx::image('create', 'photo', $name, -data => $data); } $initialized++; } #------------------------------------------------------------------------------- # Method : _Populate # Purpose : Create a new FindBar # Notes : #------------------------------------------------------------------------------- sub _Populate { my $class = shift; my $widget = shift; my $path = shift; my %opt = (-tile => 1, @_); # This doesn't get called automatically. Don't know who's to blame. _ClassInit() unless $initialized; # create the megawidget my $self = ($tile_available && $opt{-tile}) ? $class->new($path)->_parent->new_ttk__frame(-name => $path) : $class->new($path)->_parent->new_frame(-name => $path); $self->_class($class); # initialize instance data my $data = $self->_data(); $data->{-tile} = $tile_available && delete $opt{-tile}; $data->{-textwidget} = delete $opt{-textwidget}; $data->{-highlightcolor} = delete $opt{-highlightcolor} || '#80FF80'; $self->_set_hightlightcolor(); $data->{start} = '1.0'; # start index of found string $data->{what} = ''; # entry text $data->{case} = 0; # case-sensitive search $data->{regex} = 0; # regular expression search $data->{count} = 0; # number of chars in found string # populate the megawidget... $self->_button( -text => 'Close', -image => 'close16', -takefocus => 0, -command => [\&hide, $self], )->g_pack(-side => 'left', -anchor => 'w'); $self->_label( -name => 'lab', -text => 'Find:', )->g_pack(-side => 'left', -anchor => 'w'); $self->_entry( -name => 'e', -textvariable => \$data->{what}, -validate => 'key', -validatecommand => [\&_find, Tkx::Ev('%P'), $self, 'first'], )->g_pack(-side => 'left', -anchor => 'w'); $self->_button( -text => 'Next', -image => 'go-down16', -takefocus => 0, -command => [\&next, $self], )->g_pack(-side => 'left', -anchor => 'w'); $self->_button( -text => 'Previous', -image => 'go-up16', -takefocus => 0, -command => [\&previous, $self], )->g_pack(-side => 'left', -anchor => 'w'); $self->_checkbutton( -text => 'Match Case', -underline => 6, -variable => \$data->{case}, -command => [\&first, $self], )->g_pack(-side => 'left', -anchor => 'w'); $self->_checkbutton( -text => 'Regular Expression', -underline => 8, -variable => \$data->{regex}, -command => [\&first, $self], )->g_pack(-side => 'left', -anchor => 'w'); Tkx::bind("$self.e", '', sub { $data->{case} = ! $data->{case}; $self->first; }); Tkx::bind("$self.e", '', sub { $data->{regex} = ! $data->{regex}; $self->first; }); return $self; } #------------------------------------------------------------------------------- # Subroutine : _button # Purpose : Create a button widget using the tile setting. # Notes : #------------------------------------------------------------------------------- sub _button { my $self = shift; return $self->_data->{-tile} ? $self->new_ttk__button(-style => 'Toolbutton', @_) : $self->new_button(-relief => 'flat', @_); } #------------------------------------------------------------------------------- # Subroutine : _label # Purpose : Create a label widget using the tile setting. # Notes : #------------------------------------------------------------------------------- sub _label { my $self = shift; return $self->_data->{-tile} ? $self->new_ttk__label(@_) : $self->new_label(@_); } #------------------------------------------------------------------------------- # Subroutine : _entry # Purpose : Create an entry widget using the tile setting. # Notes : #------------------------------------------------------------------------------- sub _entry { my $self = shift; return $self->_data->{-tile} ? $self->new_ttk__entry(@_) : $self->new_entry(@_); } #------------------------------------------------------------------------------- # Subroutine : _checkbutton # Purpose : Create a checkbutton widget using the tile setting. # Notes : #------------------------------------------------------------------------------- sub _checkbutton { my $self = shift; return $self->_data->{-tile} ? $self->new_ttk__checkbutton(@_) : $self->new_checkbutton(@_); } #------------------------------------------------------------------------------- # Method : _config_textwidget # Purpose : Handler for configure(-textwidget => ) # Notes : #------------------------------------------------------------------------------- sub _config_textwidget { my $self = shift; my $text = shift; my $data = $self->_data(); if (defined $text) { # Remove tag from previous text widget $data->{-textwidget}->tag('delete', 'highlight') if $data->{-textwidget}; $data->{-textwidget} = $text; $self->_set_hightlightcolor(); } return $data->{-textwidget}; } #------------------------------------------------------------------------------- # Method : _config_highlightcolor # Purpose : Handler for configure(-highlightcolor => ) # Notes : #------------------------------------------------------------------------------- sub _config_highlightcolor { my $self = shift; my $color = shift; my $data = $self->_data(); if (defined $color) { $data->{-highlightcolor} = $color; $self->_set_hightlightcolor(); } return $data->{-highlightcolor}; } #------------------------------------------------------------------------------- # Method : _set_hightlightcolor # Purpose : # Notes : #------------------------------------------------------------------------------- sub _set_hightlightcolor { my $self = shift; my $data = $self->_data(); return unless $data->{-textwidget}; $data->{-textwidget}->tag('configure', 'highlight', -background => $data->{-highlightcolor}, ); } #------------------------------------------------------------------------------- # Method : _geometry_manager # Purpose : Determine the geometry manager used to manage widget # Notes : #------------------------------------------------------------------------------- sub _geometry_manager { my $self = shift; eval { Tkx::pack('info', $self) } and return 'pack'; eval { Tkx::grid('info', $self) } and return 'grid'; # eval { Tkx::place('info', $self) } and return 'place'; return; } #--------------------------------------------------------------------------- # Method : show # Purpose : Display the find toolbar # Notes : #--------------------------------------------------------------------------- sub show { my $self = shift; my $data = $self->_data(); # Display the find toolbar if it isn't already visible if ($data->{hidden}) { $showsub{$data->{gm}}->($self); $data->{hidden} = 0; } # Focus the entry widget and select the contents so the user can # start typing immediately Tkx::focus("$self.e"); Tkx::eval("$self.e", 'selection', 'range', 0, 'end'); $self->first(); } sub _show_pack { $_[0]->g_pack(Tkx::SplitList($_[0]->_data->{gminfo})) } sub _show_grid { $_[0]->g_grid() } sub _show_place { } #--------------------------------------------------------------------------- # Method : hide # Purpose : Hide the find toolbar # Notes : #--------------------------------------------------------------------------- sub hide { my $self = shift; my $data = $self->_data(); return if $data->{hidden}; # Clear any lingering highlights from found text $data->{-textwidget}->tag('remove', 'highlight', '0.0', 'end') if defined $data->{-textwidget}; my $gm = $self->_geometry_manager(); return unless $gm; # unsupported geometry manager! $data->{gm} = $gm; $hidesub{$gm}->($self); $data->{hidden} = 1; } sub _hide_pack { my $self = shift; my $data = $self->_data(); # Remember currrent pack options $data->{gminfo} = Tkx::pack('info', $self); # Remember current place in pack order # pack('info', ...) doesn't include -before or -after so we need to # synthesize them using the slave list from our pack master. my ($master) = $data->{gminfo} =~ /-in (\.\w*)/; my @slave = Tkx::SplitList(Tkx::pack('slaves', $master)); for (my $i = 0; $i < @slave; $i++) { next unless $slave[$i] eq $self->_mpath; if ($i > 0 ) { $data->{gminfo} .= " -after $slave[$i-1]" } elsif ($i < $#slave) { $data->{gminfo} .= " -before $slave[$i+1]" } # else it's the only thing in the slaves! last; } Tkx::pack('forget', $self); } # grid remove remembers options for later redisplay :D sub _hide_grid { Tkx::grid('remove', $_[0]) } sub _hide_place {} #--------------------------------------------------------------------------- # Method : first/next/previous # Purpose : public wrappers for specific searches # Notes : #--------------------------------------------------------------------------- sub first { _find($_[0]->_data->{what}, $_[0], 'first') } sub next { _find($_[0]->_data->{what}, $_[0], 'next' ) } sub previous { _find($_[0]->_data->{what}, $_[0], 'prev' ) } #------------------------------------------------------------------------------- # Method : add_bindings # Purpose : Sugar for bulk creation of bindings # Notes : #------------------------------------------------------------------------------- sub add_bindings { my $self = shift; my $widget = shift; my %binding = @_; while (my ($event, $method) = each %binding) { if (exists &$method) { Tkx::bind($widget, $event, sub { $self->$method }); } else { my $class = ref $self; carp "Class '$class' does not have a method named '$method'"; } } } #------------------------------------------------------------------------------- # Subroutine : _find # Purpose : Search in text widget # Notes : Private sub, NOT A METHOD because the text must be the first arg # for Tkx::Ev to work when binding. #------------------------------------------------------------------------------- sub _find { my $what = shift; # proposed text (Tcl %P) my $self = shift; # megawidget instance my $which = shift; # first|next|prev my $data = $self->_data(); # instance data my $where = $data->{-textwidget}; # where to search return 1 unless defined $where; # Restart new searches at the beginning. Advance the start position for # 'next' searches so we don't find the same text again. $data->{start} = '1.0' if $which eq 'first'; $data->{start} .= '+ 1 chars' if $which eq 'next'; # Build search options my @how = ('-count' => \$data->{count}); push @how, '-backwards' if $which eq 'prev'; push @how, '-regex' if $data->{regex}; push @how, '-nocase' if ! $data->{case}; # Clear any results from the last search $where->tag('remove', 'highlight', '0.0', 'end'); # Search for text # The eval{} is to catch exceptions caused by incomplete or invalid # regular expressions when the -regex option is used. Note that we can't # pre-check the regex because it's being evaluated by Tcl, not Perl, and # there are subtle syntax differences. my $i = eval { $where->search(@how, $what, $data->{start}) }; if ($@) { # invalid regex (presumably) Tkx::eval("$self.e", 'configure', -foreground => 'red'); } elsif ($i) { # text found / normal mode / no indication Tkx::eval("$self.e", 'configure', -foreground => 'black'); # Highlight the match, scroll to it, and reset the start # position for finding the prev/next instance $where->tag('add', 'highlight', $i, "$i + $data->{count} chars"); $where->see($i); $data->{start} = $i; } else { # text not found Tkx::eval("$self.e", 'configure', -foreground => '#808080'); } # We only wanted to search, not prevent text entry. return 1; } 1; __DATA__ #name:format:height:width:data close16:png:16:16:iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH1QwDARgOPQO6ugAAAZNJREFUOMudk71SGzEUhT/J9hriPEDGBTCTN2B4Axc0FHQ01KFPCbUzjtN7Jn3SU/IuTIoQ90S7lvfPq5Nis2vIuonV6M5I57v3niuZ2Xz6AfjKHsta+5HZfKp912w+Vb+hLZfL/8o+Ho8BaAEqinovSyhLqCoUAkgYa6HXg34f0+/DYNCCtgDva3FRQFliJIwECIwFa1BvUIuH0Q7AagVZBkXB8fk5AE/39wAcXV4C8OvhAUURGr0hhPAPYL0Gv8LmRUtvhO2dOIYowlqznUQTmDSF3zFyjp+LRce0p8UC4xzEMawzqqp6XQFpipK4biPPOwA9P6MowhweYkZv2xbaCvAekgQ5x8ntbQdwfHeHnCM4h9ae/G+SrQdZSogTzNq3oh/X14B4/+07AME5zGYDabpjCmmG/AolCY9XVyhN63ECjxcXmIMDzGgEIaAs2wHI8/otrGpI8L72QsI0vUtYa18BWg+Gp6eogUkgIYk6VHMAEsOzs24F7yYTmEw65jVuA2w2mzZ+aeLN5y+f9vrOwM0fpocVsnebVZ4AAAAASUVORK5CYII= close16:gif89:16:16:R0lGODlhEAAQAOYAANnZ2YiKhYqMh////+Pj4/39/eLd3eHY2OHT0+HQ0ODOzuDNzeHPz+HS0uHW1uLa2uLf3+DHx+DCwuHBweHAwOHAv+HDwuDGx+HLzODGxuC+vuC4uOGvr+K3t+G9veDIx/v7+9+8vN+xsOGqquGpqeK/vuCzs9+houCbm+CVleGVleKamuGhoeK1tvj4+OCqqt6UlN+MjN+FheCEhOCKiuGTk+Osrfb29uCfn96Hh959fd97e9+EhOKiou/v79+Xltx5et1vb9xbW9xbWt1sbN53d+KZmd6OjttubtthYdpWVtlNTdlISNpISNpLS9tTU9xeXt1qauGRkd2Hh9pjY9lVVdhISNY9PdY2NtY1Ndc7O9hERNlSUttgYOCJieS5udljY9dHR9Y8PNU1NdU0NdY6OtlRUdtfX+e9vfz8/Pr6+v///////////////////////////////////////////////////////////////////////////////////yH5BAEAAAAALAAAAAAQABAAAAfSgAGCg4SFAgGAA4KDhIQBAQOABIKDhIIFAQEDBgYHCAkKCwwNDg8QBQEBAw0LERITFBUTFhcYDQUBAQMZGhsDAxwcAwMdHh8gAQEDISIjgAOCgyQcJSABAQMmJygpgwMqKywtLgEBAy8wMTKDAzM0NTY3AQEDODk6g4M7PD0+AQEDP0BBgkJDgkRFRj4BAQNHSElKS0xNTk9QUVI+AQEDU1RVVldYWVpbXF1ePgEBA19gVWFiY2RlW2ZnaD4BAQOAaYKDampqNzc3PoABgoOEhYKBADs= go-down16:png:16:16:iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAI9SURBVDiNhZNNaBNBGIbfmezmD2vAYNJD/MGq9SIaj0m04MGL8SIULx4q4kEU6qFV8SoI0hU8qVCEnBQholJTW3Kx1lUItS0Ue6hVtFTNmq79C+kms7Ofh9Yamxjf4zffPPO+880wIkK14t1qHoQw6onB0HtEc3VJqWkihG9dSMN2BGxHYMkqYMkyAQB3H92oAdcCADDGkJt5jrJdQqE4g49zY2g/eLWuKV6vWB3Ko/jrbmwIABGIHBARFO5uCFASl1WdHMTW7btgOSS9kmxIsrFcNlG9Fu9S1w0yjjcKOUhFQi3Riyev+xSXCiLHK0mCmAS4RFEWoHo5uMJw/tQ1LwgQooJ0NlWaX5xLMSJColtNt0WPJ49Ek573+VeQXECijBV7EdPzORBoLRZH2/bTGJ0YKU9Ojfe97hHtfC1yx8uxjPHFmKJIcC+IVwCXjUJlGqqPwe3jUH0uRCNHYS6YNPlh3CDCmfVL1DVRBCH5cPCe5eVNCPiDKNFPlF2LcHs53D4Xtm3Zg5CvBdmhjEUOkromin9NQdfEREWsdD4YvFPaETgAm5WgelZP3rwpgP2hY+jLPi4JUenUNTFRd4y6JnpnjU8vhscGRGswAa4wKG6OQ+ETyL17W/lhfuvXNdHb8B2Qg46hkYHvprFAu5ti2OWPwfy6TLlRPe/I1dwNAbomiiSRzGSfWVuxD0GnFU/7n1iO/JO7Wmzjb/yteJd6LtK88zYAzOY/X9po/b8AADh8Rb0PAMM3xdl/9fwCc0oSKoZoHMsAAAAASUVORK5CYII= go-down16:gif89:16:16:R0lGODlhEAAQAOYAANnZ2Tp0BDpzBMfesMPcq7/aprHSkaPKfsjfsY2+X4a6VXGuN06aBsffsXiyQYy9XYW5VHy0Rzt1BDt2BMXdrYm8WYK4UG+tNFaJJ8Lbqsber8Lcqoi9VoK6TWOqH1OiCKXOf6TNfqTMfp/HeEuCGDt0BHqlT7jWm42+XYrAWIfAUX68QlqrDFmrC1mqClipDJfHaGWaNJm/danRg4nCUofDTWm2Hl+0DV6zDYnFToS3VUB4DbHTkJjMZX3CO2K5D2W8D2W9EHvFMp/OcT94CleMJrfblHnDMWfAEWrFEnPJH6jbeFKJHnqqTKDWbWnEEm7LFKXgbHKoPzx2BI2/XY3RTJDVTpLFX0J6DqLRdKPTdUJ7DlaLJP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////yH5BAEAAAAALAAAAAAQABAAAAe+gACCAAGAAoKDAYAAgoOCAgMEBQYHBwKEhAIICQoLDAcChIQCDQkKDgwHAoSEAgMPEBEMBwKEEgICAhMUFRYXDAcTAgICEgIYGRoUGxwdHh8gISIjJAIAJSYnKCkqKywtLi8wMSWCAgEyMzQ1Njc3ODk6EgKDAjs8PT4/QEFCQ0QChAACRUZHSEmASktMAoAAgoMAEk1OT1BRUlOEhAITVFVWV1MChIQAAlhZWlsChISDAlxcAoSEhABTU4SDgQA7 go-up16:png:16:16:iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAIeSURBVDiNlZNPaBNBFMa/N7sz2Y2FemhM/9GCBO3BQCqo0NWKaERiRS0UcmtKyUmQYhbBq8dSkaAXD0VPHqVC8eJNKl5EUNBDKbQqlZQ0iaaaJtnZGQ9RrCGp9h3f++bHfN+bIa012tXJm3weAJZmvel2GtZu4Lg83d8VSfZ1HUw6Lk/vCeC4PBoQ+7JXzkwGx0aTwYAIZh2XR/8L4Li8A4TFiXPT1paXx5a3ifjIJQuERcflHf8EEOHR6HCipzc0SJ9LH/Cx9B6dnZ0UGzrRDcLDXQGOy9P9ByKJs0fH+Ur+DQzGYTKOV2sLiA0dF6H9PYnmPNiOw1HB7Wwq4drr5WVo8iCEiTp9xzYV8S7/HBdOXw2apvgrD7bT99RYxlLkoSJLEAEBHuAoy3UI20BBrqKoVnH+1EWL2J882G/f8WPj3Yf6YpSvrMHkBjg3UFEFVI2vEDaDsBmWv71AuDdERw4Ph4k18qCRjJke6I5kb0zcsQUPQEHCh4fXuQW8LTyDZBUQIyQGMtBaQymgXq/h8dP5SrG0OWMSQ+pTbsWeuXe54Ymjevf6E6ssc/CNbXDBwEyCwQlzD25XZU1bv+wHiSFlLs16TtMmtNYKpfoXMINgcAYeaEBkTVsv5zza9R0AgNI+ftSLjSv7GrKuoFXrP2O2avpKolrbhjI1oBV8SfC9PQCkLxEfvAZiADECEfYAIGzcuj8ZbiUmwkZz7ycw98XbttCaIgAAAABJRU5ErkJggg== go-up16:gif89:16:16:R0lGODlhEAAQAOYAANnZ2Tt1BDpzBFeJKFaJJkF5D6fKh6LHfkF5Djp0BJa9cprFcpXCaoazWzt0BHilTrDSkX20SHaxP2qcO1eKKLvXoIy9X4C2TXqzRFyiGprEcU+EHUB4DbfUnKDJeou+Woa8UXS0OFOjCGyvLJfDbj53CqHEgLXVl5bFaZHFYIvDVmawHVmsC1mrC4XBTYS0VX+pVsffsKDLd5rKbJbKY4HBQ16zDV+1DV+0DV+yD5vOamecNViLKMrgtdDlvM7luMvls5fNY2i6GWS8D2W+EK/cg67bgqzZgqbTe0yEGDx4BMvmsonJS2W9EGnDEWvHErLhhD56BcrlsHjDMWfAEWzIE3HPFbTlhcjkrW29H2a/EGrFEm3JE7PjhMXiqWW2FmO6D2a+EGfBEbDeg8HfparVgavXga3agq3Zgv///////////////////////////////////////////////////////////////////////////////////////////yH5BAEAAAAALAAAAAAQABAAAAfBgACCgwABAYSEhAACAwQChISDAgUGBwgChIQAAgkKCwwNCQKEhA4PEBESDBMJhIMCFBUWFxgZGhsChAACHB0eHyAhIiMkJQKDAgkmJygpKissLS4vAQKCDjAxMjM0NTY3ODk6OwEAAjw9Pj9AQUJDREVGR0hJAgECAgJKS0xNToBPUFECAgIBAAAAAAJSU1RVVlcCgACCg4ICWFlaW1xdAoSEAl5fYGFiYwKEhAJkZWZHZ2gChIQJgAKCgw4AAAAAgQA7 __END__ =pod =head1 NAME Tkx::FindBar - Perl Tkx extension for an incremental search toolbar =head1 SYNOPSIS use Tkx; use Tkx::FindBar; my $mw = Tkx::widget->new('.'); my $text = $mw->new_text(); my $findbar = $mw->new_tkx_FindBar(-textwidget => $text); $text->g_pack(); $findbar->g_pack(); $findbar->hide(); # remove until requested by user # Bindings to display and hide toolbar and navigate matches. $findbar->add_bindings($mw, '' => 'show', '' => 'hide', '' => 'next', '' => 'previous', ); Tkx::MainLoop(); =head1 DESCRIPTION Tkx::FindBar is a Tkx megawidget that provides a toolbar for searching in a text widget. Using a toolbar for a search UI is much less obtrusive than a dialog box. The search is done incrementally (also known as "find as you type"). The toolbar may be hidden and shown as needed. Tkx::FindBar was inspired by the great find toolbar in Mozilla Firefox. =head1 WIDGET-SPECIFIC OPTIONS =head2 C<-textwidget =E I> Defines the widget to search in. This must be a text widget (or act like one). =head2 C<-highlightcolor =E I> Defines the background color used to highlight found text. The default value is #80FF80. =head2 C<-tile> If set to 1 (the default) and the Tk tile package is available, the FindBar will be drawn using themed widgets to acheive a platform native appearance. If set to 0 or tiling is not available the standard widgets will be used instead. =head1 METHODS =head2 C Hides the FindBar widget. =head2 C Shows the FindBar widget if hidden and focuses the entry subwidget. =head2 C Finds the first instance of the search text. =head2 C Finds the next instance of the search text. (Searches forwards.) =head2 C Finds the previous instance of the search text. (Searches backwards.) =head2 C Shortcut method for bulk addition of bindings (e.g. to create keygboard shortcuts). The first argument is the widget to bind to. The remaining arguments are bind tag/method name pairs. Multiple tags may be bound to the same method. =head1 LIMITATIONS The C and C methods don't work with the place geometry manager. (The pack and grid geometry managers are supported.) There's no support for configuring subwidgets. =head1 BUGS Please report any bugs or feature requests to C, or through the web interface at L. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes. =head1 SUPPORT You can find documentation for this module with the perldoc command. perldoc Tkx::FindBar You can also look for information at: =over 4 =item * RT: CPAN's request tracker L =item * AnnoCPAN: Annotated CPAN documentation L =item * CPAN Ratings L =item * Search CPAN L =back =head1 AUTHOR Michael J. Carman, C<< >> =head1 COPYRIGHT AND LICENSE Copyright (C) 2008-2009 by Michael J. Carman This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. The icons are Copyright (C) the Tango Desktop Project [L]. They are used under the terms of the Creative Commons Attribution-Share Alike License [L]. They're a heck of a lot better than anything I could come up with, so I'm grateful for being able to use them. Thanks, guys! =cut