File Coverage

blib/lib/Tk/CanvasDirTree.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::CanvasDirTree;
2              
3             our $VERSION = '0.04';
4 1     1   20240 use warnings;
  1         3  
  1         28  
5 1     1   6 use strict;
  1         1  
  1         41  
6              
7 1     1   414 use Tk::widgets qw/Canvas/;
  0            
  0            
8             use base qw/Tk::Derived Tk::Canvas/;
9             use File::Spec;
10             use Tk::JPEG;
11             use Tk::PNG;
12              
13             Construct Tk::Widget 'CanvasDirTree';
14              
15             sub ClassInit
16             {
17             my ($class, $mw) = @_;
18             $class->SUPER::ClassInit($mw);
19             $mw->bind($class, "<1>" =>'pick_one' );
20             return $class;
21             }
22              
23             sub bind{
24             my $self = shift;
25             $self->CanvasBind(@_);
26             }
27              
28             sub ConfigChanged {
29             my ($self,$args)= @_;
30            
31             foreach my $opt (keys %{$args} ){
32            
33             if( $opt eq '-indfilla' ){
34             $self->{'indfilla'} = $args->{$opt};
35              
36             my @items = $self->find('withtag','open');
37             foreach my $item (@items){
38             $self->itemconfigure($item, -fill => $args->{$opt});
39             }
40             };
41              
42             if( $opt eq '-indfilln' ){
43             $self->{'indfilln'} = $args->{$opt};
44            
45             my @items = $self->find('withtag','ind');
46             foreach my $item (@items){
47             my @tags = $self->gettags($item);
48             if( grep {$_ eq 'open'} @tags ){next}
49             $self->itemconfigure($item, -fill => $args->{$opt});
50             }
51             };
52             #---------------------------------------------
53            
54             #----------- fontcolor updates--------------
55             if( $opt eq '-fontcolora' ){
56             $self->{'fontcolora'} = $args->{$opt};
57             $self->itemconfigure('list', -activefill => $args->{$opt});
58             };
59            
60             if( $opt eq '-fontcolorn' ){
61             $self->{'fontcolorn'} = $args->{$opt};
62             $self->itemconfigure('list', -fill => $args->{$opt});
63             };
64             #---------------------------------------------
65              
66             #----------- background image updates--------------
67             if(( $opt eq '-backimage' ) or ( $opt eq '-imx' ) or ( $opt eq '-imy' )){
68             my $chipped = $opt;
69             substr $chipped, 0, 1, '' ; #chip off - off of $opt
70             $self->{ $chipped } = $args->{$opt};
71             $self->set_background(
72             $self->{'backimage'} ,$self->{'imx'}, $self->{'imy'}
73             );
74             };
75             #---------------------------------------------
76             }
77            
78             $self->idletasks;
79              
80             } #end config changed
81            
82             #################################################################
83              
84             sub Populate {
85             my ($self, $args) = @_;
86             #-------------------------------------------------------------------
87             #take care of args which don't belong to the SUPER, see Tk::Derived
88             foreach my $extra ('backimage','imx','imy','font','indfilla',
89             'indfilln','fontcolorn','fontcolora','floatback') {
90             my $xtra_arg = delete $args->{ "-$extra" }; #delete and read same time
91             if( defined $xtra_arg ) { $self->{$extra} = $xtra_arg }
92             }
93             #-----------------------------------------------------------------
94              
95             $self->SUPER::Populate($args);
96              
97             $self->ConfigSpecs(
98             -indfilla => [ 'PASSIVE', undef, undef , undef], # need to set defaults
99             -indfilln => [ 'PASSIVE', undef, undef, undef], # below for unknown
100             -fontcolora => [ 'PASSIVE', undef, undef, undef], # reason ??
101             -fontcolorn => [ 'PASSIVE', undef, undef, undef], #
102             -backimage => [ 'PASSIVE', undef, undef, undef],
103             -imx => [ 'PASSIVE', undef, undef, undef],
104             -imy => [ 'PASSIVE', undef, undef, undef],
105             -font => [ 'PASSIVE', undef, undef, undef],
106             -floatback => [ 'PASSIVE', undef, undef, undef],
107             );
108            
109             #set some defaults
110             $self->{'indfilla'} ||= 'red';
111             $self->{'indfilln'} ||= 'blue';
112             $self->{'fontcolorn'} ||= 'black';
113             $self->{'fontcolora'} ||= 'red';
114             $self->{'backimage'} ||= '';
115             $self->{'imx'} ||= 0;
116             $self->{'imy'} ||= 0;
117             $self->{'font'} ||= 'system';
118             $self->{'floatback'} ||= 0;
119            
120             #---determine font spacing by making a capital W---
121             my $fonttest = $self->createText(0,0,
122             -fill => 'black',
123             -text => 'W',
124             -font => $self->{'font'},
125             );
126            
127             my ($bx,$by,$bx1,$by1) = $self->bbox($fonttest);
128             $self->{'f_width'} = $bx1 - $bx;
129             $self->{'f_height'} = $by1 - $by;
130             $self->delete($fonttest);
131             #--------------------------------------------------
132             $self->make_trunk('.', 0);
133              
134             $self->after(1,sub{ $self->_set_bars() });
135              
136             } # end Populate
137              
138             #######################################################################
139             sub _set_bars {
140             my $self = shift;
141             my $y = $self->parent->Subwidget('yscrollbar');
142             $self->{'real_can'} = $self->parent->Subwidget('scrolled');
143             $self->idletasks;
144             $y->configure( -command => [\&yscrollcallback,$self] );
145              
146             #account for any padding
147             $self->xviewMoveto(0);
148             $self->yviewMoveto(0);
149             $self->update;
150             }
151             ######################################################################
152             sub yscrollcallback{
153             #restore original function
154             my ($self, @set) = @_;
155             $self->yview(@set);
156              
157             #if you want the floating background
158             if( $self->{'floatback'} == 1 ){
159             my($z,$z1) = $self->yview;
160             my(undef,undef,undef,$sry) = $self->cget('scrollregion');
161              
162             my $real_can_h = $self->{'real_can'}->reqheight;
163             my $div = $sry/$real_can_h;
164            
165             $self->coords($self->{'background'},
166             $self->{'imx'},
167             $self->{'imy'} + $div *$z * $real_can_h );
168             $self->update;
169             }
170              
171             }
172             ########################################################
173             sub adjust_background{
174             my ($self, $photo_obj ) = @_;
175            
176             $self->delete( $self->{'background'} );
177            
178             $self->{'bimage'} = $photo_obj;
179             $self->{'bimg_w'} = $self->{'bimage'}->width;
180             $self->{'bimg_h'} = $self->{'bimage'}->height;
181              
182             $self->{'background'} = $self->createImage(
183             $self->{'imx'} , $self->{'imy'},
184             -anchor => 'nw',
185             -image => $self->{'bimage'},
186             );
187            
188             $self->lower($self->{'background'}, 'list');
189             $self->lower($self->{'background'}, 'ind');
190            
191             }
192             ############################################################
193             sub set_background{
194             my( $self, $image ,$xim, $yim) = @_;
195            
196             $self->{'backimage'} = $image;
197             $self->{'imx'} = $xim;
198             $self->{'imy'} = $yim;
199              
200             if( ref $image eq 'Tk::Photo'){
201             $self->adjust_background($image)
202             }else{
203             my $photo_obj = $self->Photo( -file => $self->{'backimage'} );
204             $self->adjust_background( $photo_obj );
205             }
206             }
207             ##############################################################
208             sub get_subdirs{
209             my ($self, $dir) = @_;
210              
211             my @subdirs;
212             opendir my $dh, $dir or warn $!;
213              
214             while ( my $file = readdir($dh) ) {
215             next if $file =~ m[^\.{1,2}$];
216             if(-d "$dir/$file"){
217             push @subdirs, $file;
218             }else{ next }
219             }
220              
221             return @subdirs;
222             }
223             ###########################################################
224             sub check_depth_2{
225             my ($self, $abs_path) = @_;
226            
227             my $put_ind = 0;
228             opendir my $dh, $abs_path or warn $!;
229             while ( my $file = readdir($dh) ) {
230             next if $file =~ m[^\.{1,2}$];
231             if(-d "$abs_path/$file"){
232             $put_ind = 1;
233             last;
234             }
235             }
236             return $put_ind;
237             }
238             #############################################################
239             sub make_trunk{
240             my ($self, $dir, $level) = @_;
241             my $x = 5; my $y = $self->{'f_height'};
242              
243             #make background image is needed
244             if( length $self->{'backimage'} > 0 ){
245             $self->set_background(
246             $self->{'backimage'},$self->{'imx'}, $self->{'imy'}
247             );
248             }
249              
250             my @subdirs = $self->get_subdirs( $dir );
251             my $abs_root = File::Spec->rel2abs( $dir );
252             #for windows compat
253             $abs_root =~ tr#\\#/#;
254              
255             #handle special case when toplevel is / or C:/, D:/, etc
256             if($abs_root eq '/'){$abs_root = ''}
257             #for windows compat
258             if ( $abs_root =~ m#^([ABCDEFGHIJKLMNOPQRSTUVWXYZ])\:\/$# )
259             {$abs_root = "$1:"}
260              
261             #add a static entry for the topdir
262             my $root_tag;
263             if($abs_root eq ''){$root_tag = '/'}else{ $root_tag = $abs_root }
264             my $root = $self->createLine(
265             $x , $y - .8 * $self->{'f_height'},
266             $x + $self->{'f_height'}, $y - .8 * $self->{'f_height'},
267             $x + $self->{'f_height'}, $y - .4 * $self->{'f_height'},
268             -width => int( $self->{'f_height'} / 6),
269             -fill => $self->{'fontcolora'},
270             -activefill => $self->{'fontcolora'},
271             -activewidth => int( $self->{'f_height'} / 6) + 1,
272             -arrow => 'last',
273             -arrowshape => [5,5,2],
274             -tags => ['list', $root_tag,],
275             );
276              
277             my $max = scalar (@subdirs);
278             my $count = 0;
279            
280             foreach my $subdir ( sort @subdirs ){
281             my $abs_path = "$abs_root/$subdir";
282             #see if any depth 2 subdir exists
283             my $put_ind = $self->check_depth_2($abs_path);
284            
285             #make open indicator if a dir --------------------------------------
286             if( $put_ind ){
287             my $ind = $self->createPolygon(
288             $x + .1 * $self->{'f_width'} , $y + $y * $count - .3 * $self->{'f_height'},
289             $x + .5 * $self->{'f_width'}, $y + $y * $count,
290             $x + .1 * $self->{'f_width'}, $y + $y * $count + .3 * $self->{'f_height'} ,
291            
292             -fill => $self->{'indfilln'},
293             -activefill => 'yellow',
294             -outline => 'black',
295             -width => 1,
296             -activewidth => 2,
297             -tags => ['ind', $abs_path],
298             );
299             }
300             #------------------------------------------------------------
301             my $id = $self->createText(
302             $x + .8 * $self->{'f_width'}, $y + $y * $count + (.5 *$self->{'f_height'}),
303             -fill => $self->{'fontcolorn'},
304             -activefill => $self->{'fontcolora'},
305             -text => $subdir,
306             -font => $self->{'font'},
307             -anchor => 'sw',
308             -tags => ['list', $abs_path],
309             );
310             $count++;
311             }
312              
313             my ($bx,$by,$bx1,$by1)= $self->bbox('all');
314             $self->configure(-scrollregion =>[0,0,$bx1,$by1] );
315            
316             } # end make_trunk
317             ############################################################################
318             sub pick_one {
319             my ($self) = @_;
320             my $item = $self->find('withtag','current'); #returns aref
321             my @tags = $self->gettags($item->[0]);
322             $item = $item->[0];
323              
324             $self->{'selected'} = ''; #default is no selection
325              
326             if( grep { $_ eq 'ind' } @tags ){
327             my $opened = 0;
328             if( grep { $_ eq 'open'} @tags){$opened = 1}
329             @tags = grep { $_ ne 'ind' and $_ ne 'current' and $_ ne 'open'} @tags;
330             my $dir = $tags[0];
331              
332             if( $opened ){
333             $self->dtag('current', 'open' );
334             $self->rotate_poly($item, -90, undef,undef);
335             $self->itemconfigure($item, 'fill' => $self->{'indfilln'} );
336             $self->idletasks;
337             $self->close_branch($dir,$item);
338             }else{
339             $self->addtag('open', 'withtag', 'current' );
340             $self->rotate_poly($item, 90, undef,undef);
341             $self->itemconfigure($item, 'fill' => $self->{'indfilla'} );
342             $self->idletasks;
343             $self->add_branch($dir);
344             }
345             }else{
346             #picked up an indicator click by this point
347             #clicks on list items will be handled by get_selected
348             @tags = grep { $_ ne 'list' and $_ ne 'current'} @tags;
349             $self->{'selected'} = $tags[0];
350             $self->{'selected'} ||= '';
351             }
352            
353             } # end pick_one
354             ####################################################################
355             sub get_selected{
356             my ($self) = @_;
357             return $self->{'selected'};
358             }
359             ###################################################################
360             sub add_branch{
361             my ($self, $abs_path) = @_;
362             $self->Busy;
363            
364             #for windows compat
365             $abs_path =~ tr#\\#/#;
366              
367             my $item;
368             foreach my $it( $self->find('withtag', $abs_path) ){
369             my @tags = $self->gettags($it);
370             if( grep { $_ eq 'list'} @tags ){ $item = $it }
371             }
372            
373             my ($bx,$by,$bx1,$by1)= $self->bbox($item);
374             my $x = $bx + $self->{'f_width'};
375             my $y_edge = ($by + $by1)/2;
376             my $y = $by1;
377             my $count = 0;
378              
379             my @subdirs = $self->get_subdirs( $abs_path );
380            
381             my $max = scalar @subdirs;
382             my $max_add = $max * $self->{'f_height'};
383              
384             $self->make_space($y_edge,$max_add);
385              
386             # add sub entries
387             foreach my $subdir (sort @subdirs ){
388             my $abs_path1 = File::Spec->rel2abs("$abs_path/$subdir");
389             #for windows compat
390             $abs_path1 =~ tr#\\#/#;
391             #see if any depth 2 subdir exists
392             my $put_ind = $self->check_depth_2($abs_path1);
393            
394             #make open indicator---------------------------------------------
395             if( $put_ind ){
396             my $ind = $self->createPolygon(
397             $x - .9 * $self->{'f_width'} , .5*$self->{'f_height'}+ $y + $self->{'f_height'}* $count - .3 * $self->{'f_height'},
398             $x - .5 * $self->{'f_width'}, .5*$self->{'f_height'}+ $y + $self->{'f_height'}* $count,
399             $x - .9 * $self->{'f_width'}, .5*$self->{'f_height'}+ $y + $self->{'f_height'}* $count + .3 * $self->{'f_height'} ,
400            
401             -fill => $self->{'indfilln'},
402             -activefill => 'yellow',
403             -outline => 'black',
404             -width => 1,
405             -activewidth => 2,
406             -tags => ['ind', $abs_path1],
407             );
408              
409             }
410             #------------------------------------------------------------
411             my $id = $self->createText(
412             $x , $y + $self->{'f_height'} * ($count + 1),
413             -fill => $self->{'fontcolorn'},
414             -activefill => $self->{'fontcolora'},
415             -text => $subdir,
416             -font => $self->{'font'},
417             -anchor => 'sw',
418             # -tags => ['list',$abs_path, $abs_path1],
419             -tags => ['list', $abs_path1],
420             );
421            
422             #add tag to upstream indicator
423            
424             $count++;
425             }
426              
427             $self->Unbusy;
428              
429             (undef,undef,undef,$by1)= $self->bbox('list'); # get y max
430             (undef,undef,$bx1,undef)= $self->bbox('all'); # get x max
431             $self->configure( -scrollregion =>[0,0,$bx1,$by1] );
432              
433             # a possible auto-scroll feature to open sub dirs
434             # $self->yviewMoveto( ($y_edge - .5 * $self->{'f_height'})/$by1 );
435              
436             $self->yscrollcallback(); #to keep background image aligned
437            
438             } # end add_branch
439             ############################################################################
440             sub close_branch{
441             my($self, $abs_path, $ind ) = @_;
442              
443             my @y; my $x;
444              
445             foreach my $it( $self->find('all') ){
446              
447             my @tags = $self->gettags($it);
448              
449             if( grep { $_ eq 'current'} @tags ){next}
450             if( grep { $_ eq $abs_path } @tags ){next}
451             if( grep { $_ =~ /^$abs_path(.*)/ } @tags ){
452             shift @tags; #shift off ind or list tag
453              
454             if(scalar @tags > 0 ){
455             my ($bx,$by,$bx1,$by1)= $self->bbox( $tags[0] );
456             push @y,$by;
457             push @y,$by1;
458             $self->delete($it);
459             }
460             }
461             }
462            
463             my @sorted = sort {$a<=>$b} @y ;
464             my $amount = $sorted[-1] - $sorted[0];
465             my ($bx,$by,$bx1,$by1)= $self->bbox('all');
466            
467             my @items = $self->find('enclosed',
468             $bx, $sorted[-1] - $self->{'f_height'} ,
469             $bx1, $by1 + $self->{'f_height'} );
470            
471             foreach my $move (@items){
472             $self->move($move,0, -$amount);
473             }
474              
475             #adjust scroll region
476             (undef,undef,undef,$by1)= $self->bbox('list'); # get y max
477             (undef,undef,$bx1,undef)= $self->bbox('all'); # get x max
478             $self->configure( -scrollregion =>[0,0,$bx1,$by1] );
479            
480             $self->yscrollcallback(); #to keep background image aligned
481            
482             }
483             ##############################################################################
484             sub make_space{
485             my ($self, $y, $amount) = @_;
486            
487             my ($bx,$by,$bx1,$by1)= $self->bbox('all');
488             my @items = $self->find('enclosed',$bx,$y,$bx1,$by1 + $self->{'f_height'});
489              
490             foreach my $move (@items){
491             $self->move($move,0,$amount);
492             }
493              
494             }
495             ##############################################################################
496              
497             sub rotate_poly {
498             my ($self, $id, $angle, $midx, $midy) = @_;
499            
500             # Get the old coordinates.
501             my @coords = $self->coords($id);
502              
503             # Get the center of the poly. We use this to translate the
504             # above coords back to the origin, and then rotate about
505             # the origin, then translate back. (old)
506              
507             ($midx, $midy) = _get_CM(@coords) unless defined $midx;
508              
509             my @new;
510              
511             # Precalculate the sin/cos of the angle, since we'll call
512             # them a few times.
513             my $rad = 3.1416*$angle/180;
514             my $sin = sin $rad;
515             my $cos = cos $rad;
516              
517             # Calculate the new coordinates of the line.
518             while (my ($x, $y) = splice @coords, 0, 2) {
519             my $x1 = $x - $midx;
520             my $y1 = $y - $midy;
521              
522             push @new => $midx + ($x1 * $cos - $y1 * $sin);
523             push @new => $midy + ($x1 * $sin + $y1 * $cos);
524             }
525              
526             # Redraw the poly.
527             $self->coords($id, @new);
528             }
529             #################################################################
530             # This sub finds the center of mass of a polygon.
531             # I grabbed the algorithm somewhere from the web.
532             # I grabbed it from Slaven Reszic's RotCanvas :-)
533             sub _get_CM {
534             my ($x, $y, $area);
535              
536             my $i = 0;
537              
538             while ($i < $#_) {
539             my $x0 = $_[$i];
540             my $y0 = $_[$i+1];
541              
542             my ($x1, $y1);
543             if ($i+2 > $#_) {
544             $x1 = $_[0];
545             $y1 = $_[1];
546             } else {
547             $x1 = $_[$i+2];
548             $y1 = $_[$i+3];
549             }
550              
551             $i += 2;
552              
553             my $a1 = 0.5*($x0 + $x1);
554             my $a2 = ($x0**2 + $x0*$x1 + $x1**2)/6;
555             my $a3 = ($x0*$y1 + $y0*$x1 + 2*($x1*$y1 + $x0*$y0))/6;
556             my $b0 = $y1 - $y0;
557              
558             $area += $a1 * $b0;
559             $x += $a2 * $b0;
560             $y += $a3 * $b0;
561             }
562              
563             return split ' ', sprintf "%.0f %0.f" => $x/$area, $y/$area;
564             }
565             ####################################################################
566             1;
567              
568             __END__