File Coverage

blib/lib/Tk/VisualBrowser.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Tk::VisualBrowser;
2            
3             $VERSION = "0.14";
4            
5             # TODO Font, anchor für Label per option
6             #
7 2     2   49747 use Carp;
  2         5  
  2         162  
8 2     2   10 use File::Basename;
  2         3  
  2         178  
9 2     2   806 use Tk;
  0            
  0            
10             use Tk::Event;
11             use Tk::Balloon;
12             #use Tk::ErrorDialog;
13             use Tk::XPMs qw(:arrows);
14            
15             require Tk::Frame;
16             use base qw(Tk::Frame);
17            
18             use strict;
19             use constant NORMAL => 0;
20             use constant MOVE => 1;
21             my $state = NORMAL;
22             my $save_cursor;
23             my $cursor;
24            
25             my $do_scroll = 1;
26            
27             # PDO {{{
28            
29            
30             =head1 NAME
31            
32             Tk::VisualBrowser - Visual Browser for image directories
33            
34             =head1 SYNOPSIS
35            
36             use Tk;
37             use Tk::VisualBrowser;
38            
39             my $top = MainWindow->new();
40            
41             my $vsb = $top->VisualBrowser;
42            
43             my @PICTURES = qw( f1.jpg f2.jpg f3.gif);
44            
45             $vsb->configure(
46             -rows => 5, -cols => 6,
47             -pictures => \@PICTURES,
48             -thumbnail => \&thumbnail_handler,
49             -special_color => \&special_color_handler,
50             -b1_handler => \&my_b1_handler,
51             -b2_handler => \&my_b2_handler,
52             -b3_handler => \&my_b3_handler,
53             -double_b1_handler => \&my_bdouble_1_handler,
54             -double_b2_handler => \&my_bdouble_2_handler,
55             -double_b3_handler => \&my_bdouble_3_handler,
56             );
57            
58             $vsb->scroll(0); # scroll to first picture
59             # this will implicitely load the pictures
60            
61            
62             =head1 DESCRIPTION
63            
64             C is a megawidget which displays a matrix of
65             (C<-rows>) x (C<-cols>) Labels with thumbnail images. It can be used,
66             for example, to create a visual directory browser for image directories
67             or an interactive program for sorting images (dia-sorter.pl).
68            
69             The application program must provide a reference to a list of image
70             filenames and a handler which returns the filename of a corresponding
71             thumbnail GIF image for a given image filename.
72             C displays the thumbnail pictures and provides some
73             navigation buttons for scrolling linewise or pagewise through the list.
74             A scrollbar is also attached to the widget.
75            
76             It is possible to select thumbnails with the left moust button or to
77             select ranges of thumbnails with shift-click (as you would select files in
78             normal file browser). Ctrl-click allows adding or removing single thumbnails
79             from a selection.
80            
81             The selected thumbnails may be moved around with the left mouse button.
82             The cursor image changes and all thumbnails which are currently under the
83             mouse will be highlighted while moving around. Releasing the mouse button
84             inserts the selected thumbnails before the current position.
85            
86             When moving around, an automatic scroll up or down is triggered when the
87             mouse comes close to the upper or lower margin of the C.
88             But only one linewise scroll is triggered at a time (in order to avoid the
89             scrollbar from running away). Try going back and forth with the mouse
90             to trigger further scrolls as needed.
91            
92             =head1 CONFIGURATION
93            
94             There are the following possibilities for configuring the C:
95            
96             =head2 Rows and Columns
97            
98             Use C<-rows> and C<-cols> to specify the number of rows and columns
99             of the C
100            
101             $vsb->configure(-rows => 4, -cols => 8);
102            
103             NOTE: C<-cols> and/or C<-rows> B be configured in order to
104             get the C up and running: Only when configuring
105             columns or rows the C will be (re-)built.
106            
107             =head2 List of Images
108            
109             The list of images to be displayed is passed as a reference via
110             the C<-pictures> option:
111            
112             $vsb->configure(-pictures => \@PICTURES);
113            
114             The C needs GIF images for each image filename in the list.
115             To this end a handler is specified which returns the name of the
116             corresponding GIF image when fed with an image filename:
117            
118             $vsb->configure(-thumbnail => \&thumbnail_handler);
119            
120             sub thumbnail_handler {
121             my ($image_filename) = @_;
122            
123             # for example: (assuming that the thumbnails are
124             # in the same directory but with .gif extension):
125            
126             $image_filename =~ s/\.jpg/.gif/i;
127             return $image_filename;
128             }
129            
130             It could also be arranged that the thumbnail_handler creates the GIF
131             images when they do not yet exist. So the viewing of an image directory
132             would automatically create the thumbnails (with Image::Magick, for example).
133            
134             NOTE: The names in the @PICTURES array need not be valid filenames,
135             although they normally are. The names of the GIF files
136             provided by the thumbnail_handler must be valid filenames,
137             either relative to the current working directory or absolute
138             pathnames.
139            
140             =head2 Handlers for Mouse Button Events
141            
142             The application can specify its own handlers for mousebutton events, e. g.:
143            
144             $vsb->configure(-doubel_b1_handler => \&my_double_1);
145            
146             sub my_double_1 {
147             my ($image_filename) = @_;
148            
149             # display $image_filename in a Toplevel Window:
150             require Tk::JPEG;
151             my $show = $top->Toplevel();
152             my $image = $top->Photo('-format' => "jpeg",
153             -file => $image_filename);
154             $show->Label(-image => $image)->pack;
155             }
156            
157             =head2 Colors
158            
159             The following table shows the possible color options:
160            
161             -highlight => "#rrggbb" color for moving around
162             -active_color => "#rrggbb" color for selected thumbs
163             -bg_color1 => "#rrggbb" background color for plane
164             -bg_color => "#rrggbb" background color for thumbs
165             -cursor_bg => "#rrggbb" background color for cursor
166             -cursor_fg => "#rrggbb" foreground color for cursor
167            
168             When you have selected some thumbnails, they are colored with the
169             C<-active_color> option. Moving them around will highlight the
170             thumbnail under the cursor with C<-highlight> color to indicate the
171             current insert position.
172            
173             NOTE: Color options must be specified at the very beginning, when the
174             C is instantiated. Later reconfigurations may have no effect.
175            
176             It is possible to provide a handler which makes sure that certain images
177             get a different background color (for example to indicate that these
178             images have been changed recently):
179            
180             $vsb->configure(-special_color => \&my_color_hdlr);
181            
182             sub my_color_hdlr {
183             my ($image_filename) = @_;
184            
185             # decide if $image_filname needs to be displayed with a different
186             # background color:
187             if ( -M $image_filename < 7 ) {
188             return "#cc2222"; # use special bg color
189             }
190            
191             return 0; # no special color
192             }
193            
194             =head2 Labels and Balloons
195            
196             It is possible to use Labels for each image and to have balloon messages on each image (i. e.
197             a small window with text pops up when the cursor hovers over an image). In order to activate this
198             features use the following options:
199            
200             -use_labels => 1
201             -use_balloons => 1
202            
203             The default text for labels and balloons are the basenames of the image filenames. You can, however,
204             set the labels and balloon texts indiviually by passing references to corresponding arrays the the
205             VisualBrowser:
206            
207             -balloon_texts => \@Array_with_balloon_texts
208             -label_texts => \@Array_with_label_texts
209            
210             This may be used, for example, to prepare an array with text for each image which contains the filename
211             and EXIF information for the image.
212            
213             =head1 METHODS
214            
215             The following methods are available:
216            
217             =cut
218            
219             # }}}
220            
221             Construct Tk::Widget 'VisualBrowser';
222            
223             # Public Methods
224             # ==============
225            
226             sub get_selected { # {{{
227            
228             =head2 my @SELECTED = $vsb->get_selected;
229            
230             Returns the list of currently selected images. The list contains the
231             filenames of the selected pictures. This might be useful for the
232             creation of a slideshow control file with the names of the selected
233             images.
234            
235             =cut
236            
237             my ($w) = @_;
238             my @LIST = ();
239             for (my $i=0; $i < @{$w->{SEL}}; $i++){
240             push @LIST, $w->{pictures}[$i] if $w->{SEL}[$i];
241             } # for $i
242             return @LIST;
243             } # get_selected }}}
244            
245             sub get_selected_idx{ # {{{
246            
247             =head2 my @SELECTED = $vsb->get_selected_idx;
248            
249             Returns the list of currently selected images. The list contains the index numbers,
250             not the filenames.
251            
252             =cut
253            
254             my ($w) = @_;
255             my @LIST = ();
256             for (my $i=0; $i < @{$w->{SEL}}; $i++){
257             push @LIST, $i if $w->{SEL}[$i];
258             } # for $i
259             return @LIST;
260             } # get_selected_idx }}}
261            
262             sub select { # {{{
263            
264             =head2 $vsb->select($idx);
265            
266             Select specified picture with index $idx. Note that other pictures are not
267             deselected automatically.
268            
269             =cut
270            
271             my ($w, $z) = @_;
272             $w->{SEL}[$z] = 1;
273             _select_pic($w, $z, 1);
274             } # select }}}
275            
276             sub select_all { # {{{
277            
278             =head2 $vsb->select_all;
279            
280             Selectes all pictures together.
281            
282             =cut
283            
284             my ($w) = @_;
285             for ( my $z = 0; $z <= $#{$w->{pictures}}; $z++ ){
286             $w->{SEL}[$z] = 1;
287             _select_pic($w, $z, 1);
288             }
289             } # select_all }}}
290            
291             sub deselect_all { # {{{
292            
293             =head2 $vsb->deselect_all;
294            
295             Deselectes all pictures.
296            
297             =cut
298            
299             my ($w) = @_;
300             for ( my $z = 0; $z <= $#{$w->{pictures}}; $z++ ){
301             $w->{SEL}[$z] = 0;
302             _select_pic($w, $z, 0);
303             }
304             } # deselect_all }}}
305            
306             sub remove_selected { # {{{
307            
308             =head2 $vsb->remove_selected;
309            
310             This command removes the selected images from the list of pictures.
311             Note that the original list is changed because you passed a reference to
312             this list via C<-pictures>.
313            
314             =cut
315            
316             my ($w) = @_;
317             # delete all selected pictures from list
318             # when there are labels and/or balloons: delete from theses lists also
319             for (my $i = @{$w->{SEL}} -1; $i>=0; $i--){
320             if ($w->{SEL}[$i]) {
321             splice( @{ $w->cget('-pictures') }, $i, 1) ;
322             my $lref = $w->cget('-label_texts');
323             if ($lref and ref($lref) eq 'ARRAY' and @$lref) {
324             splice( @{ $w->cget('-label_texts') }, $i, 1) ;
325             }
326             my $bref = $w->cget('-balloon_texts');
327             if ($bref and ref($bref) eq 'ARRAY' and @$bref and $bref != $lref) {
328             splice( @{ $w->cget('-balloon_texts') }, $i, 1) ;
329             }
330             }
331             }
332             @{$w->{SEL}} = map {0} @{$w->cget('-pictures')};
333            
334             scroll($w, $w->{posi});
335            
336             }# remove_selected }}}
337            
338             sub swap_selected { # {{{
339            
340             =head2 $vsb->swap_selected;
341            
342             Swaps two selected pictures. Returns 1 in case of success and 0 otherwise.
343             NOTE: The user must have selected exactly two pictures.
344            
345             =cut
346            
347             my ($w) = @_;
348             my @SL; # indices of selected pics
349            
350             # Ermitteln, welche beiden Bilder selektiert sind.
351             for (my $i=0; $i < @{$w->{SEL}}; $i++){
352             push @SL, $i if $w->{SEL}[$i];
353             } # for $i
354            
355             if (scalar(@SL) != 2){
356             return 0; # not ok, need exactly two selected images
357             }
358            
359             # ok: swap pics and display again
360             my $pref = $w->cget('-pictures');
361             ($$pref[ $SL[0] ], $$pref[ $SL[1] ]) =
362             ($$pref[ $SL[1] ], $$pref[ $SL[0] ]);
363            
364             # if we have labels and/or ballons: swap also:
365             my $lref = $w->cget('-label_texts');
366             if ($lref and ref($lref) eq 'ARRAY' and @$lref) {
367             ($$lref[ $SL[0] ], $$lref[ $SL[1] ]) =
368             ($$lref[ $SL[1] ], $$lref[ $SL[0] ]);
369             }
370             my $bref = $w->cget('-balloon_texts');
371             if ($bref and ref($bref) eq 'ARRAY' and @$bref and $bref != $lref) {
372             ($$bref[ $SL[0] ], $$bref[ $SL[1] ]) =
373             ($$bref[ $SL[1] ], $$bref[ $SL[0] ]);
374             }
375             $w->{SEL}[ $SL[0] ] = 0; # deselect ...
376             $w->{SEL}[ $SL[1] ] = 0; # deselect ...
377            
378             scroll($w, $w->{posi});
379             return 1; # ok
380             } # swap_selected }}}
381            
382             sub scroll { # {{{
383            
384             =head2 $vsb->scroll();
385            
386             Scrolls the C to the specified position.
387             may have the following values:
388            
389             adjust the view so that the image with index
390             appears in the upper left corner.
391             "p" go back one line (previous line)
392             "pp" go back one page (previous page)
393             "n" scroll forward one line (next line)
394             "nn" scroll forward one page (next page)
395             "l" scroll to last image
396            
397             In order to go to the first image, you should use the numeric value 0.
398            
399             =cut
400            
401            
402             # Scroll to absolute position or scroll page wise or line wise.
403            
404             my ($w, $pos) = @_;
405             return unless $do_scroll;
406             my $thmb;
407             my $k = 0;
408             my ($r,$c) = ($w->cget('-rows'), $w->cget('-cols'));
409             return unless defined $w->{Photo}[0][0];
410             return unless defined $c;
411             return unless defined $r;
412             return unless defined $w->cget("-pictures");
413            
414             # print " scroll: pos: $pos\n";
415             my $ps = $w->{posi};
416             my $picref = $w->cget('-pictures');
417             my $max = $#{$picref};
418             my $blnref = $w->cget('-balloon_texts');
419             my $lblref = $w->cget('-label_texts');
420            
421             my $anz = $r * $c;
422             if ($pos =~ /^\d+$/){ # absolute
423             $k = trim_pos($w, $pos);
424             } elsif ( $pos eq "p") { # prev line
425             $k = trim_pos($w, $ps -$c);
426             } elsif ( $pos eq "pp") { # prev page
427             $k = trim_pos($w, $ps-$anz);
428             } elsif ( $pos eq "n") { # next line
429             $k = trim_pos($w, $ps +$c);
430             } elsif ( $pos eq "nn") { # next page
431             $k = trim_pos($w, $ps+$anz);
432             } elsif ( $pos eq "l") { # last page
433             $k = trim_pos($w, $max+1-$anz);
434             } else {
435             }
436             $w ->{posi} = $k;
437            
438             # Picture with index $k is placed in upper left corner
439             my ($color, $relief) = ("#CCCCCC", "flat");
440             $do_scroll = 0;
441             my $use_balloon = $w->cget('-use_balloons');
442             my $use_labels = $w->cget('-use_labels');
443             for (my $i = 0; $i < $r; $i++){
444             for (my $j = 0; $j < $c; $j++){
445             if ( $k <= $max and $k >= 0 ){
446             my $special_color = $w->Callback(-special_color => $$picref[$k]) || $w->cget("-bg_color");;
447             $relief = $w->{SEL}[$k] ? "groove" : "flat";
448             $color = $w->{SEL}[$k] ? $w->cget("-active_color") : $special_color;
449             $thmb = $w->Callback( -thumbnail => $$picref[$k]);
450             if (! -e $thmb){
451             $thmb = $w->{pic_path}."/vis-dummy.gif";
452             }
453             my $name = basename($$picref[$k]);
454            
455             $w->{Photo}[$i][$j] -> configure( -file => $thmb );
456             if ($use_labels) {
457             if ( @{ $w->cget('-label_texts')} ) {
458             $name = $$lblref[$k];
459             }
460             $w->{Label}[$i][$j] = $name;
461             }
462            
463             if ($use_balloon) {
464             if ( @{ $w->cget('-balloon_texts')} ) {
465             $name = $$blnref[$k];
466             }
467             $w->{bln}->detach( $w->{Thmb}[$i][$j]);
468             $w->{bln}->attach( $w->{Thmb}[$i][$j], -balloonmsg => "$name");
469             }
470             $w->{Thmb}[$i][$j] -> configure(
471             -width => 80,
472             -height => 80,
473             -background =>$color,
474             -relief => $relief,
475             -image => $w->{Photo}[$i][$j]
476             );
477             } else { # empty pictures after the end of our list
478             $thmb = $w->{pic_path}."/vis-empty.gif";
479             if ($use_labels) {
480             $w->{Label}[$i][$j] = "";
481             }
482             if ($use_balloon) {
483             $w->{bln}->detach( $w->{Thmb}[$i][$j]);
484             }
485             $w->{Photo}[$i][$j] -> configure( -file => $thmb );
486             $w->{Thmb}[$i][$j] -> configure(
487             -width => 80,
488             -height => 80,
489             -background => $w->cget("-bg_color"),
490             -relief => "flat",
491             -image => $w->{Photo}[$i][$j]
492             );
493             }
494             $k++; # next picture
495             #$w->MainWindow->update;
496             #$w->{Thmb}[$i][$j]->update; # same effect
497            
498             # ACHTUNG: Unter Windows:
499             # wenn update Aktiv ist, tritt derselbe Effekt auf, wie unter Linux ....
500             # Beim Klick auf Scrollbar-Pfeil läuft der Rollbalken weg (Dauerscroll ...)
501             } # $i
502             } # $j
503             # print " end\n";
504             $do_scroll = 1;
505             } # scroll }}}
506            
507             # Private Methods
508             # ===============
509            
510             sub Populate { # {{{
511             my ($w, $args) = @_;
512             $w->SUPER::Populate($args);
513            
514             $w->{posi} = 0;
515             $w->{state} = NORMAL;
516             $w->{pic_path} = $INC{"Tk/VisualBrowser.pm"};
517             $w->{pic_path} =~ s/VisualBrowser.pm//;
518            
519             $w->ConfigSpecs(
520             -cols => [METHOD => undef, undef, 5],
521             -rows => [METHOD => undef, undef, 4],
522             -b1_handler => [CALLBACK => undef, undef, undef],
523             -b2_handler => [CALLBACK => undef, undef, undef],
524             -b3_handler => [CALLBACK => undef, undef, undef],
525             -double_b1_handler => [CALLBACK => undef, undef, undef],
526             -double_b2_handler => [CALLBACK => undef, undef, undef],
527             -double_b3_handler => [CALLBACK => undef, undef, undef],
528             -pictures => [METHOD => undef, undef, []],
529             -thumbnail => [CALLBACK => undef, undef, sub{ return "nix is" }],
530             -special_color => [CALLBACK => undef, undef, sub{ return 0 }],
531             -highlight => [PASSIVE => undef, undef, "#3F8856"],
532             -active_color => [PASSIVE => undef, undef, "#2222CC"],
533             -bg_color => [PASSIVE => undef, undef, "#CCCCCC"],
534             -bg_color1 => [PASSIVE => undef, undef, "#BBBBBB"],
535             -cursor_fg => [PASSIVE => undef, undef, "white"],
536             -cursor_bg => [PASSIVE => undef, undef, "brown"],
537             -use_labels => [PASSIVE => undef, undef, 0],
538             -use_balloons => [PASSIVE => undef, undef, 0],
539             -balloon_texts => [METHOD => undef, undef, []],
540             -label_texts => [METHOD => undef, undef, []],
541             );
542            
543             } # Populate }}}
544            
545             sub rebuild { # {{{
546             my ($w, $rows_old, $cols_old) = @_;
547            
548             my $cols = $w->cget("-cols");
549             my $rows = $w->cget("-rows");
550            
551             # print "---- rebuild $rows, $cols\n";
552             return unless defined $rows_old;
553             return unless defined $cols_old;
554             return unless defined $rows;
555             return unless defined $cols;
556            
557            
558             # is it really necessary?
559             if ($cols_old == $cols and $rows_old == $rows) {
560             return ;
561             }
562            
563             # remove all buttons and labels
564             $w->{ysb}->destroy if defined $w->{ysb};
565             # scrollbar must be destroyed before all other objects
566             # because its enclosing frame $frm_pan is handled in the following list
567            
568             foreach my $obj ( @{ $w->{OBJECTS} } ){
569             $obj->destroy;
570             }
571             undef $w->{OBJECTS};
572            
573             # free Photo Objects
574             for (my $i = 0; $i < $rows_old; $i++){
575             for (my $j = 0; $j < $cols_old; $j++){
576             undef $w->{Photo}[$i][$j];
577             }
578             }
579            
580             # rebuild all:
581             my $pfeil_first = $w->Pixmap(-data => arrow_first_xpm);
582             my $pfeil_last = $w->Pixmap(-data => arrow_last_xpm);
583             my $pfeil_ll = $w->Pixmap(-data => arrow_ppage_xpm);
584             my $pfeil_nn = $w->Pixmap(-data => arrow_npage_xpm);
585             my $pfeil_l = $w->Pixmap(-data => arrow_prev_xpm);
586             my $pfeil_n = $w->Pixmap(-data => arrow_next_xpm);
587            
588             my $frm_but = $w->Frame()->pack;
589            
590             if ($w->cget('-use_balloons')) {
591             $w->{bln} = $w->Balloon;
592             }
593            
594             my $mm = $rows * $cols;
595             my $b_fst = $frm_but->Button(#-text => "|<",
596             -image => $pfeil_first,
597             -command => sub { scroll($w, 0);
598             set_sb($w, 0, $mm);
599             }
600             )->pack(-side => "left");
601             push @{ $w->{OBJECTS} }, $b_fst;
602            
603             my $b_pp = $frm_but->Button(#-text => "<<",
604             -image => $pfeil_ll,
605             -command => sub { scroll($w, "pp");
606             set_sb($w, $w->{posi}, $mm);
607             }
608             )->pack(-side => "left");
609             push @{ $w->{OBJECTS} }, $b_pp;
610            
611             my $b_p = $frm_but->Button(#-text => "<",
612             -image => $pfeil_l,
613             -command => sub { scroll($w, "p");
614             set_sb($w, $w->{posi}, $mm);
615             }
616             )->pack(-side => "left");
617             push @{ $w->{OBJECTS} }, $b_p;
618             my $b_n = $frm_but->Button(#-text => ">",
619             -image => $pfeil_n,
620             -command => sub { scroll($w, "n");
621             set_sb($w, $w->{posi}, $mm);
622             }
623             )->pack(-side => "left");
624             push @{ $w->{OBJECTS} }, $b_n;
625             my $b_nn = $frm_but->Button(#-text => ">>",
626             -image => $pfeil_nn,
627             -command => sub { scroll($w, "nn");
628             set_sb($w, $w->{posi}, $mm);
629             }
630             )->pack(-side => "left");
631             push @{ $w->{OBJECTS} }, $b_nn;
632             my $b_lst = $frm_but->Button(#-text => ">|",
633             -image => $pfeil_last,
634             -command => sub { scroll($w, "l");
635             my $picref = $w->cget('-pictures');
636             my $max = $#{$picref};
637             set_sb($w, $max-$mm, $mm);
638             }
639             )->pack(-side => "left");
640             push @{ $w->{OBJECTS} }, $b_lst;
641            
642             push @{ $w->{OBJECTS} }, $frm_but;
643             # push frames after their widgets so that destroy is applied
644             # in reverse order ...
645            
646             my $frm_pan = $w->Frame()->pack;
647             my $frm_pic = $frm_pan->Frame(-bg => $w->cget(-bg_color1) )->pack(-side => "left");
648            
649             $w->{ysb} = $frm_pan->Scrollbar( -command => [yview=>$w], );
650             $w->{ysb} -> pack(-side => 'left', -fill => 'y');
651             my $use_labels = $w->cget('-use_labels');
652             my $row_fakt = $use_labels ? 2 : 1;
653            
654             # print " === rows: $rows, cols: $cols\n";
655            
656             for (my $i = 0; $i < $rows; $i++){
657             for (my $j = 0; $j < $cols; $j++){
658             # push @{ $w->{OBJECTS} },
659             $w->{Photo}->[$i][$j] = $w->Photo(-file => $w->{pic_path}."/vis-empty.gif");
660             push @{ $w->{OBJECTS} },
661             $w->{Thmb} ->[$i][$j] = $frm_pic->Label(
662             -width => 80,
663             -height => 80,
664             -background => $w->cget("-bg_color"),
665             -image => $w->{Photo}[$i][$j],
666             ) -> grid( -column => $j, -row => $i*$row_fakt,
667             -sticky => "w", -padx => 3, -pady => 3);
668            
669             if ($use_labels ) {
670             $w->{Label}->[$i][$j] = "$i $j";
671             push @{ $w->{OBJECTS} },
672             $w->{Lbl} ->[$i][$j] = $frm_pic->Label(
673             -width => 12,
674             -anchor => "center",
675             -background => $w->cget("-bg_color"),
676             -textvariable => \$w->{Label}[$i][$j],
677             ) -> grid( -column => $j, -row => $i*2 + 1,
678             -sticky => "w", -padx => 3, -pady => 3);
679            
680             }
681            
682            
683             my $kx = $i*($cols) + $j;
684             my ($ii, $jj) = ($i, $j);
685             $w->{Thmb}[$i][$j] ->bind("", sub{b1($w, $kx, 1)});
686             $w->{Thmb}[$i][$j] ->bind("", sub{b1($w, $kx, 2)});
687             $w->{Thmb}[$i][$j] ->bind("", sub{dbl_b1($w, $kx)});
688             $w->{Thmb}[$i][$j] ->bind("", sub{dbl_b2($w, $kx)});
689             $w->{Thmb}[$i][$j] ->bind("", sub{dbl_b3($w, $kx)});
690             $w->{Thmb}[$i][$j] ->bind("", sub{b1($w, $kx)});
691             $w->{Thmb}[$i][$j] ->bind("", sub{b2($w, $kx)});
692             $w->{Thmb}[$i][$j] ->bind("", sub{b3($w, $kx)});
693            
694             $w->{Thmb}[$i][$j] ->bind("", [\&b1_release, $w, $ii, $jj]);
695             # first parameter for b1_release is the widget handle of the thumbnail:
696             # $w->{Thmb}[$i][$j]
697            
698             $w->{Thmb}[$i][$j] ->bind("", [\&b1_motion, $w, $ii, $jj]);
699             }
700             }
701             push @{ $w->{OBJECTS} }, $frm_pic;
702             push @{ $w->{OBJECTS} }, $frm_pan;
703             scroll($w, 0); # loads the pictures
704            
705             } # rebuild }}}
706            
707             sub _move_selected { # {{{
708             my ($w, $pos) = @_;
709             # print "move to pos $pos ...\n";
710            
711             # first of all: remove selected pics from array and save to a new array
712             # calculate the insert position during this action.
713             # Then insert new list at insert position.
714             #
715             my @MOVE_PICS;
716             my $pos_back = $pos;
717            
718             # handle label texts {{{
719             @MOVE_PICS = ();
720             $pos = $pos_back;
721             my $lref = $w->cget('-label_texts');
722             if ($lref and ref($lref) eq 'ARRAY' and @$lref) {
723             for (my $i = @{$w->{SEL}} -1; $i>=0; $i--){
724             if ($w->{SEL}[$i]) {
725             push @MOVE_PICS, splice( @{ $w->cget('-label_texts') }, $i, 1) ;
726             $pos -- if $pos ne "end" and $pos > $i;
727             }
728             }
729             if ($pos eq "end"){
730             push @{ $w->cget('-label_texts') }, reverse @MOVE_PICS;
731             } else {
732             splice @{ $w->cget('-label_texts') }, $pos, 0, reverse @MOVE_PICS;
733             }
734             } # }}}
735            
736             # handle balloon texts {{{
737             @MOVE_PICS = ();
738             $pos = $pos_back;
739             my $bref = $w->cget('-balloon_texts');
740             if ($bref and ref($bref) eq 'ARRAY' and @$bref and $bref != $lref) {
741             for (my $i = @{$w->{SEL}} -1; $i>=0; $i--){
742             if ($w->{SEL}[$i]) {
743             push @MOVE_PICS, splice( @{ $w->cget('-balloon_texts') }, $i, 1) ;
744             $pos -- if $pos ne "end" and $pos > $i;
745             }
746             }
747             if ($pos eq "end"){
748             push @{ $w->cget('-balloon_texts') }, reverse @MOVE_PICS;
749             } else {
750             splice @{ $w->cget('-balloon_texts') }, $pos, 0, reverse @MOVE_PICS;
751             }
752             } # }}}
753            
754             # the same procedure has to be done for the pictures
755             @MOVE_PICS = ();
756             $pos = $pos_back;
757             for (my $i = @{$w->{SEL}} -1; $i>=0; $i--){
758             if ($w->{SEL}[$i]) {
759             push @MOVE_PICS, splice( @{ $w->cget('-pictures') }, $i, 1) ;
760             $pos -- if $pos ne "end" and $pos > $i;
761             }
762             }
763             if ($pos eq "end"){
764             push @{ $w->cget('-pictures') }, reverse @MOVE_PICS;
765             scroll($w, $w->{posi});
766             } else {
767             splice @{ $w->cget('-pictures') }, $pos, 0, reverse @MOVE_PICS;
768             scroll($w, $w->{posi});
769             }
770            
771             deselect_all($w);
772            
773             }# _move_selected }}}
774            
775             # scrollbar handling
776            
777             sub yview { # {{{
778             # print "yview call: @_\n";
779             my $w = shift;
780             my $dir = shift;
781            
782            
783             my ($r,$c) = ($w->cget('-rows'), $w->cget('-cols'));
784             my $mm = $r * $c;
785             my $picref = $w->cget('-pictures');
786             my $mmax = scalar(@{$picref});
787            
788             my $n;
789             my $unit;
790             if ($dir eq "moveto") {
791             $n = shift;
792             # print " moveto --> $n\n";
793             my $pos = int($n*$mmax);
794             $pos = 0 if $pos < 0;
795             $pos = $mmax if $pos > $mmax;
796             scroll($w, $pos);
797             set_sb($w, $pos, $mm);
798             } elsif ($dir eq "scroll") {
799             $n = shift;
800             $unit = shift;
801             # print " scroll --> $n $unit\n";
802             if ($n == 1){
803             if ($unit eq "pages"){
804             scroll($w, "nn");
805             set_sb($w, $w->{posi}, $mm);
806             } else {
807             scroll($w, "n");
808             set_sb($w, $w->{posi}, $mm);
809             }
810             } else {
811             if ($unit eq "pages"){
812             scroll($w, "pp");
813             set_sb($w, $w->{posi}, $mm);
814             } else {
815             scroll($w, "p");
816             set_sb($w, $w->{posi}, $mm);
817             }
818             }
819             }
820             } # yview }}}
821            
822             sub set_sb { # {{{
823             my $w = shift;
824             return unless defined $w->{ysb};
825             my $val = shift;
826             my $mm = shift;
827             my $picref = $w->cget('-pictures');
828             my $mmax = scalar(@{$picref}) || 1;
829             $w->{ysb}->set( $val/$mmax, ($val + $mm)/$mmax);
830             } # set_sb }}}
831            
832             # option handlers
833            
834             sub pictures { # {{{
835             my ($w, $ref) = @_;
836            
837             if ($#_ > 0){ # configure
838             @{$w->{SEL}} = map {0} @$ref;
839             $w->{pictures} = $ref;
840             set_sb($w, 0, $w->cget("-cols") * $w->cget("-rows"));
841             scroll($w, 0);
842             } else { # cget request
843             $w->{pictures}
844             }
845             } # pictures }}}
846            
847             sub balloon_texts { # {{{
848             my ($w, $ref) = @_;
849            
850             if ($#_ > 0){ # configure
851             $w->{balloon_texts} = $ref;
852             } else { # cget request
853             $w->{balloon_texts}
854             }
855             } # balloon_texts }}}
856            
857             sub label_texts { # {{{
858             my ($w, $ref) = @_;
859            
860             if ($#_ > 0){ # configure
861             $w->{label_texts} = $ref;
862             } else { # cget request
863             $w->{label_texts}
864             }
865             } # label_texts }}}
866            
867             sub rows { # {{{
868             my ($w, $r) = @_;
869            
870             if ($#_ > 0){ # configure
871             croak "number of rows must be greater 0\n" unless $r > 0;
872             my $c_old = $w->{cols};
873             my $r_old = $w->{rows};
874             $w->{rows} = $r;
875             rebuild($w, $r_old, $c_old);
876             set_sb($w, 0, $w->cget("-cols") * $w->cget("-rows")) if defined $w->{pictures};
877             } else { # cget request
878             $w->{rows}
879             }
880             } # rows }}}
881            
882             sub cols { # {{{
883             my ($w, $c) = @_;
884            
885             if ($#_ > 0){ # configure
886             croak "number of columns must be greater 0\n" unless $c > 0;
887             my $c_old = $w->{cols};
888             my $r_old = $w->{rows};
889             $w->{cols} = $c;
890             rebuild($w, $r_old, $c_old);
891             set_sb($w, 0, $w->cget("-cols") * $w->cget("-rows")) if defined $w->{pictures};
892             } else { # cget request
893             $w->{cols}
894             }
895             } # cols }}}
896            
897             # mouse button handlers
898            
899            
900             # Button Events:
901             sub b1 { # {{{
902             my ($w, $pos, $sh) = @_;
903             # $w Object Handle
904             # $pos Position in Thumbs-Matrix: 0, 1, ..., cols*rows-1
905             # $sh Shift-Button pressed
906             #
907             # select/deselect current picture
908             my ($c, $r);
909             $r = int($pos/$w->cget("-cols")); # current row
910             $c = $pos%$w->cget("-cols"); # current column
911             # print " ---- b1: \n";
912            
913             # print "shift-" if defined $sh;
914             # print "b1 pos: $pos $c, $r\n";
915            
916             my $idx = list_index($w, $pos); # click position in PICS array
917            
918             my $sel = 0;
919             # Shift-Klick
920             # ===========
921             if (defined $sh and $sh == 1){ # select area
922             # ersten und letzten selection index ermitteln:
923             $w->{SEL}[$idx] = 1;
924             my ($i1, $i2) = (9999999, -1);
925             for ( my $z = 0; $z <= $#{$w->{pictures}}; $z++ ){
926             if ( $w->{SEL}[$z]){
927             $i1 = $z; last;
928             }
929             }
930             for ( my $z = $#{$w->{pictures}}; $z >=0; $z-- ){
931             if ( $w->{SEL}[$z]){
932             $i2 = $z; last;
933             }
934             }
935             # print "**1 $i1 bis $i2\n";
936             if ($idx < $i1) {
937             $i1 = $idx;
938             }
939             if ($idx > $i1) {
940             $i2 = $idx;
941             }
942             # print "**2 $i1 bis $i2\n";
943            
944             # erst mal alle deselektieren
945             for ( my $z = 0; $z <= $#{$w->{pictures}}; $z++ ){
946             $w->{SEL}[$z] = 0;
947             _select_pic($w, $z, 0);
948             }
949             # dann den Bereich selektieren
950             for ( my $z = $i1; $z <= $i2; $z++ ){
951             $w->{SEL}[$z] = 1;
952             _select_pic($w, $z, 1);
953             }
954            
955             # Ctrl-Klick
956             # ==========
957             } elsif (defined $sh and $sh == 2){ # ctrl B1, add/remove
958             # print "##### ctrl \n";
959             $w->{SEL}[$idx] = 1 - $w->{SEL}[$idx] if $idx > -1;
960             my $relief = _is_selected($w, $pos) ? "groove" : "flat";
961             my $picref = $w->cget('-pictures');
962             my $special_color = $w->Callback(-special_color => $$picref[$idx]) || $w->cget("-bg_color");;
963             my $color = _is_selected($w, $pos) ? $w->cget("-active_color") : $special_color;
964             $w->{Thmb}[$r][$c] ->configure(
965             -relief =>$relief,
966             -background => $color,
967             );
968            
969             # Button-1
970             # ========
971             } else { # single select
972             # wenn man in ein nicht selektierte Bils kilckt:
973             # neues Bild wird als einziges selektiert
974             if (! $w->{SEL}[$idx]) {
975             for ( my $z = 0; $z <= $#{$w->{pictures}}; $z++ ){
976             $w->{SEL}[$z] = 0;
977             _select_pic($w, $z, 0);
978             }
979             _select_pic($w, $idx, 1);
980             $w->{SEL}[$idx] = 1;
981             } else {
982             # andernfalls: klick auf selektiertes Bild:
983             # gehe in den MOVE-Zustand: Aktuelle Auswahl wird via
984             # B1-Motion bewegt:
985             $state = MOVE;
986             # Cursor ändern:
987             $save_cursor = $w->MainWindow->cget('-cursor');
988             $cursor = 'mouse';
989             if ($^O !~ /Win/i){
990             if (scalar get_selected($w) == 1){
991             $cursor = ['@'. $w->{pic_path} ."/move1.xbm" ,
992             $w->{pic_path} ."move1_mask.xbm", $w->cget(-cursor_bg), $w->cget(-cursor_fg)];
993             } else {
994             $cursor = ['@'. $w->{pic_path} ."/move.xbm" ,
995             $w->{pic_path} ."move_mask.xbm", $w->cget(-cursor_bg), $w->cget(-cursor_fg)];
996             }
997             }
998             $w->MainWindow->configure(-cursor => $cursor);
999             }
1000             }
1001            
1002             # Call user's b1 handler if applicable:
1003             my $jpg = ${$w->{pictures}}[$idx];
1004             $w->Callback( -b1_handler => $jpg);
1005             } # b1 }}}
1006            
1007             sub b1_motion { # {{{
1008            
1009             my ($thb, $w, $ii, $jj) = @_;
1010             return unless $state == MOVE; # only then ...
1011            
1012             my $rows = $w->cget('-rows');
1013             my $cols = $w->cget('-cols');
1014             my $e = $thb->XEvent; # coordinates relativ to Thmb Label !!
1015            
1016            
1017             THMB:
1018             for (my $i = 0; $i < $rows; $i++){
1019             for (my $j = 0; $j < $cols; $j++){
1020             my $idx = $w->{posi} + $i * $cols + $j;
1021             my $upper_left_x = $w->{Thmb}[$i][$j]->x;
1022             my $upper_left_y = $w->{Thmb}[$i][$j]->y;
1023             my $width = $w->{Thmb}[$i][$j]->width;
1024             my $height = $w->{Thmb}[$i][$j]->height;
1025             if (_enclosed($upper_left_x, $upper_left_y,
1026             $width, $height,
1027             $e->x + $jj * $width, # auf linkes oberes Label beziehen ...
1028             $e->y + $ii * $height)) # daher Korrektursummanden ...
1029             {
1030             # highlight background
1031             $w->{Thmb}[$i][$j] -> configure(
1032             -background =>$w->cget("-highlight"),
1033             -relief => "sunken",
1034             );
1035             } elsif ($w->{SEL}[$idx] ) {
1036             # selection background for thumbs which are selected
1037             $w->{Thmb}[$i][$j] -> configure(
1038             -background => $w->cget("-active_color"),
1039             -relief => "groove",
1040             );
1041             } else {
1042             # normal background for thumbs which are not selected
1043             my $picref = $w->cget('-pictures');
1044             my $special_color = $w->Callback(-special_color => $$picref[$idx]) || $w->cget("-bg_color");;
1045             $w->{Thmb}[$i][$j] -> configure(
1046             -background => $special_color,
1047             -relief => "flat",
1048             );
1049             }
1050             }
1051             }
1052            
1053            
1054             # scroll when we approche the lower margin
1055             #
1056             my $mm = $rows * $cols;
1057             my $height =$thb->height;
1058             my $y_pos = $e->y + $ii*$height;
1059             if ( $y_pos < $height/2 ){
1060             # print " <<<<<<\n";
1061             if ($w->{up}) {
1062             $w->scroll("p");
1063             set_sb($w, $w->{posi}, $mm);
1064             $w->{up} = 0;
1065             }
1066             } elsif ($y_pos > $height*0.55) { # Hysterese
1067             $w->{up} = 1;
1068             }
1069             if ( $y_pos > $rows * $height - $height/2 ){
1070             # print " >>>>>>\n";
1071             if ($w->{down}) {
1072             $w->scroll("n");
1073             set_sb($w, $w->{posi}, $mm);
1074             $w->{down} = 0;
1075             }
1076             } elsif ( $y_pos < $rows*$height - 0.55*$height) { # Hysterese
1077             $w->{down} = 1;
1078             }
1079            
1080             # update cursor image
1081             $w->MainWindow->configure(-cursor => $cursor);
1082             } # b1_motion }}}
1083            
1084             sub b1_release { # {{{
1085             my ($thb, $w, $ii, $jj) = @_;
1086             if ($state == MOVE) {
1087             # Versuche herauszubekommen, über welchem Label sich der
1088             # Cursor gerade befindet:
1089             my $e = $thb->XEvent; # Koordinaten relativ zum Thumb Label !!
1090             # print "x: ", $e->x, " y: ", $e->y, "\n";
1091             # ok, soweit so gut. Jetzt muss man die Koordiaten mit den umfassenden
1092             # Rechtecken aller Thmb Labels vergleichen und daraus die Release-Position
1093             # eritteln:
1094             my $rows = $w->cget('-rows');
1095             my $cols = $w->cget('-cols');
1096             THMB:
1097             for (my $i = 0; $i < $rows; $i++){
1098             for (my $j = 0; $j < $cols; $j++){
1099             my $upper_left_x = $w->{Thmb}[$i][$j]->x;
1100             my $upper_left_y = $w->{Thmb}[$i][$j]->y;
1101             my $width = $w->{Thmb}[$i][$j]->width;
1102             my $height = $w->{Thmb}[$i][$j]->height;
1103             # print " ux $upper_left_x, uy $upper_left_y\n";
1104             my $kx = $cols * $i + $j;
1105             if (_enclosed($upper_left_x, $upper_left_y,
1106             $width, $height,
1107             $e->x + $jj * $width, # auf linkes oberes Label beziehen ...
1108             $e->y + $ii * $height)) # daher Korrektursummanden ...
1109             {
1110             # print " #### $kx\n" ;
1111             my $idx = list_index($w, $kx); # click position in PICS array
1112             _move_selected($w, $idx);
1113             last THMB;
1114             }
1115             }
1116             }
1117             }
1118             $state = NORMAL;
1119             $w->MainWindow->configure(-cursor => $save_cursor);
1120             } # b1_release }}}
1121            
1122             sub _enclosed { # {{{
1123             # check, if ($x, $y) is within the rectangle
1124             my ($ulx, $uly, $width, $height, $x, $y) = @_;
1125             return 1 if
1126             $ulx <= $x and $x <= $ulx + $width and
1127             $uly <= $y and $y <= $uly + $height;
1128             return 0;
1129             } # _enclosed }}}
1130            
1131             sub b2 { # {{{
1132             my ($w, $pos) = @_;
1133             my $idx = list_index($w, $pos); # click position in PICS array
1134             # Call user's b1 handler if applicable:
1135             my $jpg = ${$w->{pictures}}[$idx];
1136             # print " ---- b2: $jpg\n";
1137             $w->Callback( -b2_handler => $jpg);
1138             } # b2 }}}
1139            
1140             sub b3 { # {{{
1141             my ($w, $pos) = @_;
1142             my $idx = list_index($w, $pos); # click position in PICS array
1143             # Call user's b1 handler if applicable:
1144             my $jpg = ${$w->{pictures}}[$idx];
1145             # print " ---- b3: $jpg\n";
1146             $w->Callback( -b3_handler => $jpg);
1147             } # b3 }}}
1148            
1149             sub dbl_b1 { # {{{
1150             my ($w, $pos) = @_;
1151             my $idx = list_index($w, $pos); # click position in PICS array
1152             my $jpg = ${$w->cget("-pictures")}[$idx];
1153             # print " ---- dbl_b1: $jpg\n";
1154            
1155             # select only current picture:
1156             _select_only($w, $pos);
1157            
1158             # Call user's double-b1 handler if applicable:
1159             # $jpg = ${$w->{pictures}}[$idx];
1160             $w->Callback( -double_b1_handler => $jpg);
1161             } # dbl_b1 }}}
1162            
1163             sub dbl_b2 { # {{{
1164             my ($w, $pos) = @_;
1165             my $idx = list_index($w, $pos); # click position in PICS array
1166             my $jpg = ${$w->{pictures}}[$idx];
1167             # print " ---- dbl_b2: $jpg\n";
1168            
1169             # select only current picture:
1170             _select_only($w, $pos);
1171            
1172             # Call user's double-b2 handler if applicable:
1173             $w->Callback( -double_b2_handler => $jpg);
1174             } # dbl_b2 }}}
1175            
1176             sub dbl_b3 { # {{{
1177             my ($w, $pos) = @_;
1178             my $idx = list_index($w, $pos); # click position in PICS array
1179             my $jpg = ${$w->{pictures}}[$idx];
1180             # print " ---- dbl_b3: $jpg\n";
1181            
1182             # select only current picture:
1183             _select_only($w, $pos);
1184            
1185             # Call user's double-b3 handler if applicable:
1186             $w->Callback( -double_b3_handler => $jpg);
1187             } # dbl_b3 }}}
1188            
1189             # auxiliary functions
1190            
1191             sub _is_selected { # {{{
1192             my ($w, $pos) = @_;
1193             my $idx = list_index($w, $pos);
1194             return 0 if $idx < 0;
1195             return $w->{SEL}[$idx];
1196             } # _is_selected }}}
1197            
1198             sub _select_pic { # {{{
1199             my ($w, $z, $sel) = @_;
1200             # $z position in PICs array
1201             # $sel select/deselect
1202             return if $z < $w->{posi}
1203             or $z > $w->{posi}+$w->cget("-rows")*$w->cget("-cols")-1;
1204            
1205             my $pos = $z - $w->{posi}; # position in thumbs matrix
1206             my ($c, $r);
1207             $r = int($pos/$w->cget("-cols")); # current row
1208             $c = $pos%$w->cget("-cols"); # current column
1209             # print "_select_pic: $r, $c z: $z pos: $pos\n";
1210             my $relief = $sel ? "groove" : "flat";
1211             my $picref = $w->cget('-pictures');
1212             my $special_color = $w->Callback(-special_color => $$picref[$z]) || $w->cget("-bg_color");;
1213             my $color = $sel ? $w->cget("-active_color") : $special_color;
1214             return unless defined $w->{Thmb}[$r][$c];
1215             $w->{Thmb}[$r][$c] ->configure(
1216             -relief => $relief,
1217             -background => $color,
1218             );
1219             } # _select_pic }}}
1220            
1221             sub _select_only { # {{{
1222             my ($w, $pos) = @_;
1223             # select only current picture:
1224             for ( my $z = 0; $z <= $#{$w->{pictures}}; $z++ ){
1225             $w->{SEL}[$z] = 0;
1226             _select_pic($w, $z, 0);
1227             }
1228             _select_pic($w, $pos, 1);
1229             $w->{SEL}[$pos] = 1;
1230             } # _select_only }}}
1231            
1232             sub trim_pos{ # {{{
1233             # calculate position in PICS array, check boundaries
1234             my ($w, $pos) = @_;
1235             return 0 if $pos < 0;
1236             my $picref = $w->cget('-pictures');
1237             my $max = scalar(@{$picref});
1238             return $max if $pos > $max;
1239             return $pos;
1240             } # trim_pos }}}
1241            
1242             sub list_index { # {{{
1243             # Position of current pic in list PICS
1244             my ($w, $pos) = @_;
1245             my $idx = $w->{posi}+$pos;
1246             my $picref = $w->cget('-pictures');
1247             my $max = scalar(@{$picref});
1248             return -1 if $idx > $max;
1249             return $idx;
1250             } # list_index }}}
1251            
1252             1;
1253            
1254             __END__