The Perl and Raku Conference 2025: Greenville, South Carolina - June 27-29 Learn more

=head1 NAME
Tk::ListBrowser - Tk::IconList like mega widget.
=cut
use strict;
use Carp;
use vars qw($VERSION);
$VERSION = '0.03';
use base qw(Tk::Derived Tk::Frame);
Construct Tk::Widget 'ListBrowser';
use Tk;
#used in formatText
my $dlmreg = qr/\.|\(|\)|\:|\!|\+|\,|\-|\<|\=|\>|\%|\&|\*|\"|\'|\/|\;|\?|\[|\]|\^|\{|\||\}|\~|\\|\$|\@|\#|\`|\s/;
#available refresh handlers
my %handlers = (
bar => 'Bar',
column => 'Column',
list => 'List',
row => 'Row',
);
=head1 SYNOPSIS
require Tk::ListBrowser;
my $ib= $window->ListBrowser(@options)->pack;
$ib->add('item_1', -image => $image1, -text => $text1);
$ib->add('item_2', -image => $image2, -text => $text2);
$ib->refresh;
=head1 DESCRIPTION
B<Tk::ListBrowser> is inspired on L<Tk::IconList> but with added features
like row as well as column, list and bar oriented display. There are plenty
of options to set the presentation of text items.
Scrollbars are automatically shown when needed.
=head1 OPTIONS
Tk::ListBrowser uses the following standardoptions: B<-background>, B<-font>,
B<-foreground>, B<-selectbackground>, B<-selectforeground>. In addition the
following options are supported.
=over 4
=item Switch B<-arrange>
Default value I<row>. This option specifies the layout of your list. If you change this
option on the fly you have to call I<refresh> to see the changes. You can set the
following values:
=over 4
=item B<bar>
Presents a one row, horizontal list.
=item B<column>
Presents a column centered two dimensional list.
=item B<list>
Presents a one column vertical list.
=item B<row>
Presents a row centered two dimensional list.
=back
=item Switch B<-browsecmd>
Callback, called when an is selected. Gets the selection list as parameters.
=item Switch B<-command>
Callback, called when an entry is double clicked or return was pressed. Gets the selection list as parameters.
=item Name B<filterDelay>
=item Class B<FilterDelay>
=item Switch B<-filterdelay>
Default value 300 miliseconds. When the filter bar is active this is the wait time
between a keystroke and a corresponding filter action. If a key is pressed before
time out, the timer is reset.
=item Switch B<-filterfield>
Default value I<name>. Possible values are I<name>, I<text> and I<data>.
Specifies on what data the filter should work.
=item Switch B<-filteron>
Default value I<false>. If set the filter entry will allways be visible.
=item Switch B<-itemtype>
Default value I<imagetext>. Can be I<image>, I<imagetext> or I<text>.
=item Switch B<-motionselect>
Default value I<false>. Only works when I<-selectmode> is set to single.
Automatically selects an entry when the pointer is hovering over it.
=item Switch B<-nofilter>
Default value I<false>. If set the filter entry is not available.
This option supercedes the I<-filteron> option.
Only available at create time.
=item Switch B<-selectmode>
Default value I<single>. Can either be I<single> or I<multiple>.
In single mode only one entry in the list can be selected at all times.
In multiple mode more than one entry can be selected.
=item Switch B<-textanchor>
Default value empty string. This value centers the text in it's cativy.
Besides an empty string the possible values are I<n>, I<ne>, I<nw>, I<s>, I<se>, I<sw>, I<e> and I<w>.
The letters stand for the first letter of the wind directions. They tell where to position the text in
it's cavity.
=item Switch B<-textjustify>
Default value I<center>. Can be I<center>, I<left> or I<right>. Specifies
how multiline or wrapped text is justified.
=item Switch B<-textside>
Default value I<bottom>. Possible values are I<bottom>, I<top>, I<left>, I<right>,
or I<none>. It sets at which side the text should appear next to the icon.
If you change this option on the fly you have to call I<refresh>
to see the changes. This option only has meaning when I<-itemtype> is set to 'imagetext'.
=item Switch B<-wraplength>
Default value 0. You can set it to a positive integer of at least 40 to invoke word or character wrapping.
The value determines the maximum width in pixels of a text block. It will attempt a word
wrap and will do a character wrap if that fails.
If you change this option on the fly you have to call I<refresh> to see the changes.
If you try to set it to a lower value than 40, you will be overruled.
This is to prevent the wrap length to become smaller that the with of one character, which
would freeze you application. This also means watching out for using very big fonts.
=back
=cut
=head1 METHODS
=over 4
=cut
sub Populate {
my ($self,$args) = @_;
my $nofilter = delete $args->{'-nofilter'};
$nofilter = '' unless defined $nofilter;
$self->SUPER::Populate($args);
my $canv = $self->Scrolled('LBCanvas',
-keycall => ['KeyPress', $self],
-scrollbars => 'osoe',
)->pack(-expand => 1, -fill => 'both');
my $c = $canv->Subwidget('scrolled');
$c->configure(-takefocus => 1);
$self->Advertise('Canvas', $c);
$self->bind('<Configure>', [ $self, 'refresh' ]);
#mouse bindings
$c->Tk::bind('<Button-1>', [ $self, 'Button1', Ev('x'), Ev('y') ]);
$c->Tk::bind('<Control-Button-1>', [ $self, 'Button1Control', Ev('x'), Ev('y') ]);
$c->Tk::bind('<Double-Button-1>', [ $self, 'Button1Double', Ev('x'), Ev('y') ]);
$c->Tk::bind('<Shift-Button-1>', [ $self, 'Button1Shift', Ev('x'), Ev('y') ]);
$c->Tk::bind('<Control-f>', [$self, 'filterFlip']);
$c->Tk::bind('<Button-2>', [ $self, 'Button2', Ev('x'), Ev('y') ]);
$c->Tk::bind('<B2-Motion>', [ $self, 'Button2Motion', Ev('x'), Ev('y') ]);
$c->Tk::bind('<ButtonRelease-2>', [ $self, 'Button2Release', Ev('x'), Ev('y') ]);
$c->Tk::bind('<Motion>', [ $self, 'Motion', Ev('x'), Ev('y') ]);
#setting up the filter
unless ($nofilter) {
my $filter = '';
$self->Advertise('Filter', \$filter);
my $fframe = $self->Frame;
$self->Advertise('FilterFrame', $fframe);
my $fentry = $fframe->Entry(
-textvariable => \$filter,
)->pack(-side => 'left', -pady => 2, -expand => 1, -fill => 'x');
$self->Advertise('FilterEntry', $fentry);
$fentry->bind('<Control-f>', [$self, 'filterFlip']);
$fentry->bind('<Escape>', [$self, 'filterFlip']);
$fentry->bind('<Button-1>', [$self, 'filterClick']);
$fentry->bind('<KeyRelease>', [$self, 'filterActivate']);
}
$self->{ARRANGE} = undef;
$self->{HANDLER} = undef;
$self->{POOL} = [];
$self->{ROWS} = 0;
$self->{WRAPLENGTH} = 0;
$self->ConfigSpecs(
-arrange => ['METHOD', undef, undef, 'row'],
-background => [$c, 'background', 'Background', '#E8E8E8'],
-browsecmd => ['CALLBACK'],
-command => ['CALLBACK'],
-filterdelay => ['PASSIVE', 'filterDelay', 'FilterDelay', 300],
-filterfield => ['PASSIVE', undef, undef, 'name'],
-filteron => ['PASSIVE', undef, undef, ''],
-font => ['PASSIVE', 'font', 'Font', 'Monotype 10'],
-foreground => ['PASSIVE', 'foreground', 'foreground', '#3C3C3C'],
-itemtype => ['PASSIVE', undef, undef, 'imagetext'],
-motionselect => ['PASSIVE', undef, undef, ''],
-selectbackground => ['PASSIVE', 'selectBackground', 'SelectBackground', '#A0A0FF'],
-selectforeground => ['PASSIVE', 'selectForeground', 'SelectForeground', '#FAF9EA'],
-selectmode => ['PASSIVE', undef, undef, 'single'],
-textanchor => ['PASSIVE', undef, undef, ''],
-textjustify => ['PASSIVE', undef, undef, 'center'],
-textside => ['PASSIVE', undef, undef, 'bottom'],
-wraplength => ['METHOD', undef, undef, 0],
DEFAULT => [ $c ],
);
$self->Delegates(
CanvasFocus => $c,
canvasx => $c,
canvasy => $c,
createImage => $c,
createRectangle => $c,
createText => $c,
xview => $c,
yview => $c,
xviewScroll => $c,
yviewScroll => $c,
DEFAULT => $self,
);
$self->after(10, sub { $self->filterFlip if $self->cget('-filteron') });
# $c->CanvasFocus
}
sub _handler { return $_[0]->{HANDLER} }
=item B<add>I<(?$name?, %options)>
Adds I<$name> to the list with I<%options>. I<$name> must not yet exist.
Possible options are:
=over 4
=item B<-after>
Insert I<$name> after the entry name held by this option.
=item B<-before>
Insert I<$name> before the entry name held by this option.
=item B<-data>
Assign an arbitray scalar value to this entry.
=item B<-hidden>
Tag the entry as hidden. If you do not specify this option
the entry is shown after I<refresh>
=item B<-image>
A valid L<Tk::Image> class assigned to this entry.
=item B<-text>
Text assigned to this entry.
=back
After a call to I<add> you must call I<refresh> to see your changes.
Returns a reference to the created item object.
=cut
sub add {
my ($self, $name, %options) = @_;
if ($self->infoExists($name)) {
croak "Entry '$name' already exists";
return
}
my $after = delete $options{'-after'};
my $before = delete $options{'-before'};
my $item = new Tk::ListBrowser::Item(
%options,
-canvas => $self,
-name => $name,
);
my $pool = $self->pool;
if (defined $after) {
my $index = $self->index($after);
splice(@$pool, $index + 1, 0, $item) if defined $index;
croak "Entry for -after '$after' not found" unless defined $index;
} elsif (defined $before) {
my $index = $self->index($before);
splice(@$pool, $index, 0, $item) if defined $index;
croak "Entry for -before '$before' not found" unless defined $index;
} else {
push @$pool, $item
}
return $item
}
=item B<anchorClear>
Clears the keyboard anchor.
=cut
sub anchorClear {
my $self = shift;
my $pool = $self->pool;
for (@$pool) {
$_->anchor(0)
}
}
=item B<anchorGet>
Returns a reference to the L<Tk::ImageBrowser::Icon> object
that currently holds the anchor.
=cut
sub anchorGet {
my $self = shift;
my $pool = $self->pool;
for (0 .. @$pool - 1) {
my $obj = $pool->[$_];
return $obj if $obj->anchored
}
return undef
}
sub anchorInitialize {
my $self = shift;
my $i = $self->anchorGet;
unless (defined $i) {
my $name = $self->infoFirstVisible;
$self->anchorSet($name) unless defined $self->anchorGet;
$self->see($name);
return 1
}
return ''
}
=item B<anchorSet>I<($name)>
Sets the keyboard anchor to I<$name>
=cut
sub anchorSet {
my ($self, $name) = @_;
my $item = $self->get($name);
if ((defined $item) and (not $item->hidden)) {
$self->anchorClear;
$item->anchor(1);
# $self->anchor($item);
return 1
}
return ''
}
=item B<anchorSetColumnRow>I<($column, $row)>
Sets the anchor in $column and $row
=cut
sub anchorSetColumnRow {
my ($self, $column, $row) = @_;
my $pool = $self->pool;
my $index = $self->indexColumnRow($column, $row);
if (defined $index) {
return $self->anchorSet($pool->[$index]->name);
}
return ''
}
sub arrange {
my $self = shift;
if (@_) {
my $arr = shift;
my $mod = $handlers{$arr};
unless (defined $mod) {
croak "Invalid handler $arr";
return
}
$self->{ARRANGE} = $arr;
my $modname = "Tk::ListBrowser::$mod";
my $error = '';
eval "use $modname;";
$error = $@;
unless ($error) {
$self->clear;
my $h = $modname->new($self);
$self->{HANDLER} = $h;
} else {
croak $error
}
}
return $self->{ARRANGE}
}
sub Button1 {
my ($self, $x, $y) = @_;
my $item = $self->initem($x, $y);
if (defined $item) {
$self->selectionClear;
$item->select(1);
$self->anchorSet($item->name);
$self->Callback('-browsecmd', $item->name);
} else {
$self->selectionClear;
}
}
sub Button1Control {
my ($self, $x, $y) = @_;
return $self->Button1($x, $y) if $self->cget('-selectmode') eq 'single';
my $item = $self->initem($x, $y);
if (defined $item) {
if ($item->selected) {
$item->select(0)
} else {
$item->select(1)
}
} else {
$self->selectionClear;
}
}
sub Button1Double {
my ($self, $x, $y) = @_;
my $item = $self->initem($x, $y);
if (defined $item) {
$self->Callback('-command', $item->name);
} else {
$self->selectionClear;
}
}
sub Button1Shift {
my ($self, $x, $y) = @_;
return $self->Button1($x, $y) if $self->cget('-selectmode') eq 'single';
my $item = $self->initem($x, $y);
if (defined $item) {
my $pool = $self->pool;
my @sel = $self->selectionGet;
unless (@sel) {
my $start = $pool->[0]->name;
$self->selectionSet($pool->[0]->name, $item->name);
return
}
if ($self->index($item->name) < $self->index($sel[0])) {
$self->selectionSet($item->name, $sel[0]);
return
}
if ($self->index($item->name) > $self->index($sel[@sel - 1])) {
$self->selectionSet($sel[@sel - 1], $item->name);
return
}
$self->selectionClear;
}
}
sub Button2 {
my ($self, $x, $y) = @_;
$self->configure(-cursor => 'fleur');
$self->{'mouse_pos'} = [$x, $y];
}
sub Button2Motion {
my ($self, $x, $y) = @_;
my $mousepos = $self->{'mouse_pos'};
my ($mx, $my) = @$mousepos;
$self->{'mouse_pos'} = [$x, $y];
my $dx = $mx - $x;
my $dy = $my - $y;
$self->xviewScroll(-$dx, 'units') if $self->_handler->scroll eq 'horizontal';
$self->yviewScroll(-$dy, 'units') if $self->_handler->scroll eq 'vertical';
}
sub Button2Release {
my $self = shift;
$self->configure(-cursor => 'arrow');
delete $self->{'mouse_pos'};
}
=item B<canvasSize>
Returns the available width and height of the Danvas widget.
=cut
sub canvasSize {
my $self = shift;
my $c = $self->Subwidget('Canvas');
my $offset = $c->cget('-highlightthickness') + $c->cget('-borderwidth');
return ($c->width - $offset, $c->height - $offset);
}
=item B<clear>
Clears the canvas.
=cut
sub clear {
my $self = shift;
$self->anchorClear;
$self->selectionClear;
my $pool = $self->pool;
grep { $_->clear } @$pool;
my $c = $self->Subwidget('Canvas');
$c->xview(moveto => 0);
$c->yview(moveto => 0);
$c->configure(-scrollregion => [0, 0, 0, 0]);
}
=item B<delete>I<(?$name?)>
Deletes entry I<$name>. You must call I<refresh> to see your changes.
=cut
sub delete {
my ($self, $name) = @_;
my $pool = $self->pool;
my $index = $self->index($name);
if (defined $index) {
my ($del) = splice(@$pool, $index, 1);
$del->clear;
return
}
croak "Entry '$name' not found"
}
=item B<deleteAll>
Deletes all entries. You must call refresh to see your changes.
=cut
sub deleteAll {
my $self = shift;
my $pool = $self->pool;
grep { $self->delete($_->name) } @$pool;
$self->clear;
}
my %validconfigs = (
-data => 1,
-hidden => 1,
-image => 1,
-text => 1
);
=item B<entryCget>I<($name, $option)>
Returns the value of I<$option> held by $name. Valid
options are I<-data>, I<-hidden>, I<-image> and I<-text>.
=cut
sub entryCget {
my ($self, $name, $option) = @_;
my $i = $self->get($name);
unless (defined $i) {
croak "Entry '$name' not found";
return
}
unless (exists $validconfigs{$option}) {
croak "Invalid option '$option'";
return
}
$option =~ s/^\-//;
return $i->$option
}
=item B<entryConfigure>I<($name, %options)>
Sets the value of I<%options> held by $name. Valid
options are I<-data>, I<-hidden>, I<-image> and I<-text>.
You can specify multiple options.
=cut
sub entryConfigure {
my $self = shift;
my $name = shift;
my $i = $self->get($name);
unless (defined $i) {
croak "Entry '$name' not found";
return
}
while (@_) {
my $option = shift;
my $value = shift;
unless (exists $validconfigs{$option}) {
croak "Invalid option '$option'";
return
}
$option =~ s/^\-//;
$i->$option($value)
}
}
sub filter {
my ($self, $filter, $value) = @_;
return 1 if $filter eq '';
$filter = quotemeta($filter);
return 1 if $value eq '';
return $value =~ /$filter/i;
}
sub filterClick {
my $self = shift;
my $e = $self->Subwidget('FilterEntry');
my $text = $e->get;
$e->delete(0, 'end') if $text eq 'Filter';
}
sub filterActivate {
my $self = shift;
my $filter_id = $self->{'filter_id'};
if (defined $filter_id) {
$self->afterCancel($filter_id);
}
$filter_id = $self->after($self->cget('-filterdelay'), ['filterRefresh', $self]);
$self->{'filter_id'} = $filter_id;
}
=item B<filterFlip>
Hides the filter bar if it is shown. Shows it if it is hidden.
=cut
sub filterFlip {
my $self = shift;
my $f = $self->Subwidget('FilterFrame');
if (defined $f) {
my $e = $self->Subwidget('FilterEntry');
if ($f->ismapped) {
unless ($self->cget('-filteron')) {
$f->packForget;
$e->delete(0, 'end');
$self->CanvasFocus;
}
} else {
$e->insert('end', 'Filter');
$f->pack(-fill => 'x');
}
}
}
sub filterRefresh {
my $self = shift;
my $pool = $self->pool;
my $filter = $self->Subwidget('FilterEntry')->get;
my $filterfield = $self->cget('-filterfield');
for (@$pool) {
if ($self->filter($filter, $_->$filterfield)) {
$_->hidden('')
} else {
$_->hidden(1)
}
}
delete $self->{'filter_id'};
$self->refresh;
}
sub focus { $_[0]->CanvasFocus }
=item B<get>I<(?$name?)>
Returns a reference to the L<Tk::ListBrowser::Icon> object of I<$name>.
=cut
sub get {
my ($self, $name) = @_;
my $pool = $self->pool;
my @hit = grep { $_->name eq $name } @$pool;
return $hit[0]
}
=item B<get>I<(?$name?)>
Returns a list of all L<Tk::ListBrowser::Icon> objects.
=cut
sub getAll {
my $self = shift;
my $pool = $self->pool;
return @$pool
}
=item B<getColumn>I<($column)>
Returns a list of referencec to all L<Tk::ListBrowser::Icon> objects in column I<$column>.
=cut
sub getColumn {
my ($self, $col) = @_;
my $pool = $self->pool;
my @hits = grep { (defined $_->column) and ($_->column eq $col) } @$pool;
return @hits
}
=item B<getIndex>I<($index)>
Returns a reference to the L<Tk::ListBrowser::Icon> object at index I<$index>
=cut
sub getIndex {
my ($self, $index) = @_;
return undef unless defined $index;
my $pool = $self->pool;
if (($index < 0) or ($index > @$pool - 1)) {
croak "Index '$index' out of range";
return undef ;
}
return $pool->[$index];
}
=item B<getRow>I<($row)>
Returns a list of referencec to all L<Tk::ListBrowser::Icon> objects in row I<$row>.
=cut
sub getRow {
my ($self, $row) = @_;
my $pool = $self->pool;
my @hits = grep { (defined $_->row ) and ($_->row eq $row) } @$pool;
return @hits
}
=item B<hide>I<($name)>
Hides entry I<$name>. Call I<refresh> to see changes.
=cut
sub hide {
my ($self, $name) = @_;
my $a = $self->get($name);
$a->hidden(1) if defined $a
}
=item B<index>
Returns the numerical index of entry I<$name>.
=cut
sub index {
my ($self, $name) = @_;
my $pool = $self->pool;
my ($index) = grep { $pool->[$_]->name eq $name } 0 .. @$pool - 1;
return $index
}
=item B<indexColumnRow>I<($column, $row)>
Returns the numerical index of the entry at I<$column>, I<$row>.
=cut
sub indexColumnRow {
my ($self, $column, $row) = @_;
my $pool = $self->pool;
my ($index) = grep {
(defined $pool->[$_]->column) and
(defined $pool->[$_]->row) and
($pool->[$_]->column eq $column) and
($pool->[$_]->row eq $row)
} 0 .. @$pool - 1;
return $index
}
=item B<indexLast>
Returns the numerical index of the last entry in the list.
=cut
sub indexLast {
my $self = shift;
my $pool = $self->pool;
my $last = @$pool - 1;
return $last
}
=item B<infoAnchor>
Returns the name of the entry that holds the anchor.
Returns undef if the anchor is not held.
=cut
sub infoAnchor {
my $self = shift;
my $a = $self->anchorGet;
return $a->name if defined $a;
return undef
}
=item B<infoData>I<($name)>
Returns the data associated with entry I<$name>
=cut
sub infoData {
my ($self, $name) = @_;
my $a = $self->get($name);
return $a->data if defined $a;
croak "Entry '$name' not found";
return undef
}
=item B<infoExists>I<($name)>
Returns a boolean value indicating if entry I<$name> exists.
=cut
sub infoExists {
my ($self, $name) = @_;
my $a = $self->get($name);
return defined $a;
}
=item B<infoFirst>
Returns the name of the first entry in the list.
=cut
sub infoFirst {
my $self = shift;
my $pool = $self->pool;
return undef unless @$pool;
return $pool->[0]->name
}
=item B<infoFirstVisible>
Returns the name of the first entry in the list that is not hidden.
=cut
sub infoFirstVisible {
my $self = shift;
my $pool = $self->pool;
for (@$pool) {
return $_->name unless $_->hidden
}
}
=item B<infoHidden>I<($name)>
Returns the boolean hidden state of entry I<$name>.
=cut
sub infoHidden {
my ($self, $name) = @_;
my $a = $self->get($name);
if (defined $a) {
my $flag = $a->hidden;
$flag = '' if $flag eq 0;
return $flag
}
croak "Entry '$name' not found";
return undef
}
=item B<infoLast>
Returns the name of the last entry in the list.
=cut
sub infoLast {
my $self = shift;
my $pool = $self->pool;
return undef unless @$pool;
return $pool->[@$pool - 1]->name
}
=item B<infoLastVisible>
Returns the name of the last entry in the list that is not hidden.
=cut
sub infoLastVisible {
my $self = shift;
my $pool = $self->pool;
for (reverse @$pool) {
return $_->name unless $_->hidden
}
}
=item B<infoList>
Returns a list of all entry names in the list.
=cut
sub infoList {
my $self = shift;
my $pool = $self->pool;
my @list;
for (@$pool) { push @list, $_->name }
return @list
}
=item B<infoNext>I<($name)>
Returns the name of the next entry of I<$name>.
Returns undef if I<$name> is the last entry in the list.
=cut
sub infoNext {
my ($self, $name) = @_;
my $pool = $self->pool;
my $a = $self->index($name);
unless (defined $a) {
croak "Entry '$name' not found";
return
}
return undef if $a eq @$pool - 1;
return $pool->[$a + 1]->name;
}
=item B<infoNextVisible>I<($name)>
Returns the name of the first next entry of I<$name> that is not hidden.
Returns undef if I<$name> is the last entry in the list.
=cut
sub infoNextVisible {
my ($self, $name) = @_;
my $pool = $self->pool;
my $a = $self->index($name);
unless (defined $a) {
croak "Entry '$name' not found";
return
}
for ($a .. @$pool - 1) {
return $pool->[$_]->name unless $pool->[$_]->hidden
}
}
=item B<infoPev>I<($name)>
Returns the name of the previous entry of I<$name>.
Returns undef if I<$name> is the first entry in the list.
=cut
sub infoPrev {
my ($self, $name) = @_;
my $pool = $self->pool;
my $a = $self->index($name);
unless (defined $a) {
croak "Entry '$name' not found";
return
}
return undef if $a eq 0;
return $pool->[$a - 1]->name;
}
=item B<infoPrevVisible>I<($name)>
Returns the name of the first previous entry of I<$name> that is not hidden.
Returns undef if I<$name> is the first entry in the list.
=cut
sub infoPrevVisible {
my ($self, $name) = @_;
my $pool = $self->pool;
my $a = $self->index($name);
unless (defined $a) {
croak "Entry '$name' not found";
return
}
for (reverse 0 .. $a) {
return $pool->[$_]->name unless $pool->[$_]->hidden
}
}
=item B<infoSelection>
Same as I<selectionGet>.
=cut
sub infoSelection { return $_[0]->selectionGet }
sub initem {
my ($self, $x, $y) = @_;
$self->CanvasFocus;
$x = int($self->canvasx($x));
$y = int($self->canvasy($y));
my $pool = $self->pool;
for (@$pool) {
if ($_->inregion($x, $y)) {
return $_;
}
}
return undef
}
sub KeyArrowNavig {
my ($self, $dcol, $drow) = @_;
return undef if $self->anchorInitialize;
my $pool = $self->pool;
my $i = $self->anchorGet;
my $target;
if ($drow eq 0) { #horizontal move
my $rown = $i->row;
my @row = $self->getRow($rown);
if (($dcol > 0) and ($i->column >= @row - 1)) {
$target = $self->moveRow(1);
} elsif (($dcol < 0) and ($i->column <= 0)) {
$target = $self->moveRow(-1);
} else {
my $ti = $self->indexColumnRow($i->column + $dcol, $rown);
$target = $self->getIndex($ti) if defined $ti;
}
} else { #vertical move
my $coln = $i->column;
my @column = $self->getColumn($coln);
if (($drow > 0) and ($i->row >= @column - 1)) {
$target = $self->moveColumn(1);
} elsif (($drow < 0) and ($i->row <= 0)) {
$target = $self->moveColumn(-1);
} else {
my $ti = $self->indexColumnRow($coln, $i->row + $drow);
$target = $self->getIndex($ti) if defined $ti;
}
}
if (defined $target) {
my $name = $target->name;
$self->anchorSet($name);
$self->see($name);
return 1
}
return ''
}
sub KeyArrowSelect {
my ($self, $dcol, $drow) = @_;
return if $self->anchorInitialize;
my $p = $self->anchorGet;
if ($self->KeyArrowNavig($dcol, $drow)) {
my $new = $self->anchorGet->name;
if ($p->selected) {
$self->selectionSet($new)
} else {
$self->selectionUnSet($new)
}
}
}
sub KeyLastColumn {
my $self = shift;
return if $self->anchorInitialize;
my $i = $self->anchorGet;
my $row = $i->row;
my $col = $self->lastColumnInRow($row);
unless ($self->anchorSetColumnRow($col, $row)) {
my $flag = '';
while ((not $flag) and ($col >= 0)) {
$col --;
my $index = $self->indexColumnRow($col, $row);
my $name = $self->pool->[$index]->name;
$flag = $self->anchorSet($name);
$self->see($name) if $flag;
}
}
}
sub KeyPress {
my ($self, $key) = @_;
my $pool = $self->pool;
my $h = $self->_handler;
return unless @$pool;
my @sel = $self->selectionGet;
if ($key eq 'Return') {
return if $self->anchorInitialize;
my $i = $self->anchorGet;
my $name = $i->name;
$self->selectionSet($name);
$self->Callback('-command', $name);
return
}
if ($key eq 'Escape') {
if ($self->Subwidget('FilterEntry')->ismapped) {
$self->filterFlip
} else {
$self->selectionClear;
$self->anchorClear;
}
return
}
#keyboard navigation
if ($key eq 'Down') {
$self->KeyArrowNavig(0, 1);
return
}
if ($key eq 'End') {
$self->KeyLastColumn;
return
}
if ($key eq 'Control-End') {
my $name = $self->infoLastVisible;
$self->see($name);
$self->after(50, sub { $self->anchorSet($name) });
return
}
if ($key eq 'Home') {
return if $self->anchorInitialize;
my $i = $self->anchorGet;
my $row = $i->row;
my $index = $self->indexColumnRow(0, $row);
my $name = $pool->[$index]->name;
$self->anchorSet($name);
$self->see($name);
return
}
if ($key eq 'Control-Home') {
my $name = $self->infoFirstVisible;
$self->anchorSet($name);
$self->see($name);
return
}
if ($key eq 'Left') {
$self->KeyArrowNavig(-1, 0);
return
}
if ($key eq 'Right') {
$self->KeyArrowNavig(1, 0);
return
}
if ($key eq 'Up') {
$self->KeyArrowNavig(0, -1);
return
}
#manipulating selections
if ($key eq 'space') {
return if $self->anchorInitialize;
my $i = $self->anchorGet;
my $name = $i->name;
$self->selectionFlip($name);
$self->Callback('-browsecmd', $name) if $i->selected;
return
}
if ($key eq 'Shift-Down') {
return $self->KeyArrowSelect(0, 1)
}
if ($key eq 'Shift-End') {
return if $self->anchorInitialize;
my $i = $self->anchorGet;
my $column = $i->column;
my $row = $i->row;
my @items = $self->getRow($row);
if ($self->KeyLastColumn) {
for (@items) {
if ($self->cget('-selectmode') eq 'multiple') {
$self->selectionFlip($_->name)
}
}
}
}
if ($key eq 'Control-Shift-End') {
return if $self->anchorInitialize;
my $begin = $self->anchorGet;
my $name = $self->infoLastVisible;
if ($self->anchorSet($name)) {
my $end = $self->anchorGet;
if ($begin->selected) {
$self->selectionClear if $self->cget('-selectmode') eq 'single';
$self->selectionSet($begin->name, $end->name);
} else {
$self->selectionClear if $self->cget('-selectmode') eq 'single';
$self->selectionUnSet($begin->name, $end->name);
}
$self->see($name);
}
}
if ($key eq 'Shift-Home') {
return if $self->anchorInitialize;
my $i = $self->anchorGet;
my $column = $i->column;
my $row = $i->row;
my @items = $self->getRow($row);
if ($self->anchorSetColumnRow(0, $row)) {
for (@items) {
$self->selectionFlip($_->name)
}
}
return
}
if ($key eq 'Control-Shift-Home') {
return if $self->anchorInitialize;
my $begin = $self->anchorGet;
if ($self->anchorSet($self->infoFirstVisible)) {
my $end = $self->anchorGet;
if ($begin->selected) {
$self->selectionSet($begin->name, $end->name);
} else {
$self->selectionUnSet($begin->name, $end->name);
}
}
return
}
if ($key eq 'Shift-Left') {
return $self->KeyArrowSelect(-1, 0)
}
if ($key eq 'Shift-Right') {
return $self->KeyArrowSelect(1, 0)
}
if ($key eq 'Shift-Up') {
return $self->KeyArrowSelect(0, -1)
}
}
=item B<lastColumnInRow>I<($row)>
Returns the number of the last column in I<$row>.
=cut
sub lastColumnInRow {
my ($self, $row) = @_;
my $pool = $self->pool;
my @row = $self->getRow($row);
return $row[@row - 1]->column;
}
=item B<lastRowInColumn>I<($column)>
Returns the number of the last row in I<$column>.
=cut
sub lastRowInColumn {
my ($self, $column) = @_;
my $pool = $self->pool;
my @column = $self->getColumn($column);
return $column[@column - 1]->row;
}
sub Motion {
my ($self, $x, $y) = @_;
return unless $self->cget('-selectmode') eq 'single';
return unless $self->cget('-motionselect');
my $item = $self->initem($x, $y);
if (defined $item) {
$self->selectionSet($item->name);
}
}
sub moveColumn {
my ($self, $delta) = @_;
my $i = $self->anchorGet;
my $column = $i->column;
my $row = $i->row;
my @c = $self->getColumn($column);
my $lastrow = @c - 1;
$row = $row + $delta;
if ($row >= $lastrow) {
$column ++;
$row = 0;
} elsif ($column <= 0) {
$column --;
my @nc = $self->getColumn($column);
$row = @nc - 1;
}
my $target;
my $index = $self->indexColumnRow($column, $row);
$target = $self->getIndex($index) if defined $index;
return $target;
}
sub moveRow {
my ($self, $delta) = @_;
my $i = $self->anchorGet;
my $column = $i->column;
my $row = $i->row;
my @r = $self->getRow($row);
my $lastcolumn = @r - 1;
$column = $column + $delta;
if ($column >= $lastcolumn) {
$column = 0;
$row ++;
} elsif ($column <= 0) {
$row --;
my @nr = $self->getRow($row);
$column = @nr - 1;
}
my $target;
my $index = $self->indexColumnRow($column, $row);
$target = $self->getIndex($index) if defined $index;
return $target;
}
sub pool { return $_[0]->{POOL} }
sub refreshTimer {
my $self = shift;
delete $self->{'timer_id'};
$self->refresh(1);
}
=item B<refresh>
Clears the canvas and rebuilds it. Call this method after you are done making changes.
=cut
sub refresh {
my ($self, $timer) = @_;
if (my $id = $self->{'timer_id'}) {
$self->afterCancel($id);
my $nid = $self->after(50, ['refreshTimer', $self]);
$self->{'timer_id'} = $nid;
}
unless (defined $timer) {
my $id = $self->after(50, ['refreshTimer', $self]);
$self->{'timer_id'} = $id;
return
}
$self->_handler->refresh;
}
=item B<see>I<($name)>
Scrolls the canvas to make I<$name> visible if it is not, or not completely, visible.
=cut
sub see {
my ($self, $name) = @_;
my $scrollregion = $self->cget('-scrollregion');
return unless @$scrollregion;
my ($cx1, $cy1, $cx2, $cy2) = @$scrollregion; #the canvas
my $i = $self->get($name);
my ($cwidth, $cheight) = $self->canvasSize;
my ($ix1, $iy1, $ix2, $iy2) = $i->region;
my $h = $self->_handler;
#horizontal
if ($h->scroll eq 'horizontal') {
my ($vl, $vr) = $self->xview;
my $div = $cx2 - $cx1;
if (($div > 0) and ($ix1/$div < $vl)) { #going to the left
$self->xview(moveto => $ix1/$div);
} elsif (($div > 0) and ($ix2/$div > $vr)) { #going to the right.
my $mr = ($ix2 - $cwidth + 2)/$div;
$self->xview(moveto => $mr);
}
}
#vertical
if ($h->scroll eq 'vertical') {
my ($vt, $vb) = $self->yview;
my $div = $cy2 - $cy1;
if (($div > 0) and ($iy1/$div < $vt)) { #going up
$self->yview(moveto => $iy1/$div);
} elsif (($div > 0) and ($iy2/$div > $vb)){ #going down.
my $mr = ($iy2 - $cheight + 2)/$div;
$self->yview(moveto => $mr);
}
}
}
=item B<selectAll>
Selects all entries.
=cut
sub selectAll {
my $self = shift;
return if $self->cget('-selectmode') eq 'single';
my $pool = $self->pool;
grep { $_->select } @$pool;
}
=item B<selectionClear>
Clears the entire selection.
=cut
sub selectionClear {
my $self = shift;
my $pool = $self->pool;
grep { $_->select(0) } @$pool;
}
sub selectionFlip {
my ($self, $begin, $end) = @_;
($begin, $end) = $self->selectionIndex($begin, $end);
my $pool = $self->pool;
for ($begin .. $end) {
my $i = $pool->[$_];
if ($i->selected) {
$self->selectionClear if $self->cget('-selectmode') eq 'single';
$i->select(0);
} else {
$self->selectionClear if $self->cget('-selectmode') eq 'single';
$i->select;
}
}
}
=item B<selectionGet>
Returns a list of entry names contained in the selection.
=cut
sub selectionGet {
my $self = shift;
my @list;
my $pool = $self->pool;
for (@$pool) { push @list, $_->name if $_->selected }
return @list;
}
sub selectionIndex {
my ($self, $begin, $end) = @_;
$end = $begin unless defined $end;
$begin = $self->index($begin);
$end = $self->index($end);
if ($begin > $end) {
my $t = $begin;
$begin = $end;
$end = $t;
}
return ($begin, $end)
}
=item B<selectionSet>I<($begin, ?$end?)>
Selects entry I<$begin>. If you specify I<$end> the
range from I<$begin> to I<$end> will be selected.
=cut
sub selectionSet {
my ($self, $begin, $end) = @_;
($begin, $end) = $self->selectionIndex($begin, $end);
my $pool = $self->pool;
for ($begin .. $end) {
my $i = $pool->[$_];
unless ($i->hidden) {
$self->selectionClear if $self->cget('-selectmode') eq 'single';
$i->select #unless $i->selected;
}
}
}
=item B<selectionUnSet>I<($begin, $end)>
Clears the selection of entry I<$begin>. If you specify I<$end> the
range from I<$begin> to I<$end> will be cleared from the selection.
=cut
sub selectionUnSet {
my ($self, $begin, $end) = @_;
$end = $begin unless defined $end;
($begin, $end) = $self->selectionIndex($begin, $end);
my $pool = $self->pool;
for ($begin .. $end) {
my $i = $pool->[$_];
$i->select(0) unless $i->hidden;
}
}
=item B<show>I<($name)>
Shows entry I<$name>. Call I<refresh> to see changes.
=cut
sub show {
my ($self, $name) = @_;
my $a = $self->get($name);
$a->hidden(0) if defined $a
}
=item B<textFormat>I<($text)>
Formats, basically wraps, I<$text> taking the option I<-wraplength> into account.
I<$text> can be a multi line string.
=cut
sub textFormat {
my ($self, $text) = @_;
my $wraplength = $self->cget('-wraplength');
my $font = $self->cget('-font');
return $text if $wraplength <= 0;
my @lines = split (/\n/, $text);
my @out;
for (@lines) {
my $line = $_;
my $length = $self->fontMeasure($font, $line);
if ($length > $wraplength) {
my $res = $length / length($line);
my $oklength = int($wraplength/$res);
while (length($line) > $oklength) {
my $t = substr($line, 0, $oklength, '');
if ($t =~ s/([$dlmreg])([^$dlmreg]+$)//) {
$line = "$2$line";
$t = "$t$1";
}
push @out, $t;
}
push @out, $line;
} else {
push @out, $line;
}
}
my $result = '';
while (@out) {
$result = $result . shift @out;
$result = "$result\n" if @out
}
return $result
}
=item B<textHeight>I<($text)>
Returns the display height of I<$text> in pixels.
I<$text> can be a multi line string.
=cut
sub textHeight {
my ($self, $text) = @_;
return 0 if $text eq '';
my $height = 1;
while ($text =~ /\n/g) { $height ++ }
my $font = $self->cget('-font');
return ($height * $self->fontMetrics($font, '-linespace')) #+ $self->fontMetrics($font, '-descent');;
}
=item B<textWidth>I<($text)>
Returns the display width of I<$text> in pixels.
I<$text> can be a multi line string.
=cut
sub textWidth {
my ($self, $text) = @_;
return $self->fontMeasure($self->cget('-font'), $text) unless $text =~ /\n/;
my $width = 0;
while ($text =~ s/^([^\n]*)\n//) {
my $w = $self->fontMeasure($self->cget('-font'), $1);
$width = $w if $w > $width;
}
if ($text ne '') {
my $w = $self->fontMeasure($self->cget('-font'), $text);
$width = $w if $w > $width;
}
return $width
}
sub wraplength {
my $self = shift;
if (@_) {
my $l = shift;
if ($l > 0) {
$l = 40 if $l < 40;
}
$self->{WRAPLENGTH} = $l;
}
return $self->{WRAPLENGTH}
}
=back
=head1 USING THE KEYBOARD
Before you can manipulate the list using the keyboard, the anchor must be initialized first. You do
that by pressing any of the keys below. After that you can start navigating and manipulate selections.
The spacebar selects or deselects the entry that is currently held by the anchor. The I<-browsecmd>
callback is called if the entry is selected.
The return key selects the entry and invokes the I<-command> callback.
You can navigate the list using the arrow keys and the the Home, Control-Home, End and Control-End keys.
Holding shift while pressing these keys manipulates the selection.
The escape key clears the selection and anchor or hides the filter entry if it is visible.
Control-f pops a filter entry. Clicking Control-f again hides it. Filtering is done instantly upon entering
text in. This is influenced by the I<-filteron> and I<-nofilter> options.
=head1 USING THE MOUSE
Clicking an entry with the left button selects it and assigns the anchor to it. Shift-click manipulates
the selection of the range from the entry that holds the anchor to the one you click. Control-click
selects or deselects the entry.
You can drag-scroll the list by moving you mouse while holding the middle button pressed.
=head1 LICENSE
Same as Perl.
=head1 AUTHOR
Hans Jeuken (hanje at cpan dot org)
=head1 TODO
Work out Tk::ListBrowser::Hlist and Tk::ListBrowser::Tree addons. Add side columns.
Add headers.
=head1 BUGS AND CAVEATS
If you find any bugs, please report them here: L<https://github.com/haje61/Tk-ListBrowser/issues>.
=head1 SEE ALSO
=over 4
=item L<Tk::ListBrowser::Bar>
=item L<Tk::ListBrowser::Column>
=item L<Tk::ListBrowser::Item>
=item L<Tk::ListBrowser::List>
=item L<Tk::ListBrowser::Row>
=back
=cut
1;