File Coverage

blib/lib/Tk/PerlMethodList.pm
Criterion Covered Total %
statement 21 257 8.1
branch 0 54 0.0
condition 0 29 0.0
subroutine 7 37 18.9
pod n/a
total 28 377 7.4


line stmt bran cond sub pod time code
1             #! /usr/bin/perl
2              
3             package Tk::PerlMethodList;
4             our $VERSION = 0.07;
5              
6 1     1   1901 use warnings;
  1         3  
  1         36  
7 1     1   6 use strict;
  1         2  
  1         36  
8             #use Data::Dumper;
9 1     1   962 use File::Slurp qw /read_file/;
  1         13531  
  1         126  
10             require Tk;
11             require Tk::LabEntry;
12             require Tk::NumEntry;
13             require Tk::ROText;
14             require Class::Inspector;
15             require B ;
16 1     1   12 use MRO::Compat;
  1         3  
  1         29  
17 1     1   1754 use Devel::Peek qw(CvGV);
  1         546  
  1         8  
18             our @ISA = ('Tk::Toplevel');
19              
20             =head1 NAME
21              
22             Tk::PerlMethodList - query the Symbol-table for methods (subroutines) defined in a class (package) and its parents.
23              
24             =head1 SYNOPSIS
25              
26              
27             require Tk::PerlMethodList;
28              
29             my $instance = $main_window->PerlMethodList();
30              
31             =head1 DESCRIPTION
32              
33             Tk::PerlMethodList is a Tk::Toplevel-derived widget.
34              
35             The window contains entry fields for a classname and a regex. The list below displays the subroutine-names in the package(s) of the given classname and its parent classes. The list displays the sub-names present in the the symbol-table. In case of imported subs, the last field of a row contains the name of the aliased sub as reported by DevelPeek::CvGV. Tk::PerlMethodList will not show subs which can be - but have not yet been autoloaded. It will show declared subs though. The 'Filter' entry takes a regex to filter the returned List of sub/methodnames.
36              
37             If the file containing a subroutine definition can be found in %INC, a green mark will be displayed at the beginning of the line. The sourcecode will be displayed by clicking on the subs list-entry.
38              
39              
40             Method list and source window have Control-plus and Control-minus bindings to change fontsize.
41              
42              
43              
44             =head1 METHODS
45              
46             B supports the following methods:
47              
48             =over 4
49              
50             =item B'A::Class::Name'B<)>
51              
52             Set the classname-entry to 'A::Class::Name'.
53              
54             =item B'a_regex'B<)>
55              
56             Set the filter-entry to 'a_regex'.
57              
58             =item B
59              
60             Build the list for classname and filter present in the entry-fields.
61              
62             =back
63              
64             =head1 OPTIONS
65              
66             B supports the following options:
67              
68             =over 4
69              
70             =item B<-classname>
71              
72             $instance->configure(-classname =>'A::Class::Name')
73             Same as classname('A::Class::Name').
74              
75             =item B<-filter>
76              
77             $instance->configure(-filter =>'a_regex')
78             Same as filter('a_regex').
79              
80              
81             =back
82              
83             =head1 AUTHOR
84              
85             Christoph Lamprecht, ch.l.ngre@online.de
86              
87             =head1 COPYRIGHT AND LICENSE
88              
89             Copyright (C) 2006-2007 by Christoph Lamprecht
90              
91             This library is free software; you can redistribute it and/or modify
92             it under the same terms as Perl itself, either Perl version 5.8.7 or,
93             at your option, any later version of Perl 5 you may have available.
94              
95              
96             =cut
97              
98              
99             Tk::Widget->Construct('PerlMethodList');
100             unless (caller()) {
101             _test_();
102             }
103              
104             sub Populate{
105 0     0     my ($self,@args) = @_;
106 0           $self->SUPER::Populate(@args);
107 0           my $frame = $self -> Frame()->pack(-fill => 'x',
108             -padx => 20,
109             -pady => 4,
110             );
111 0           my $fr_left = $frame-> Frame()->pack(-side => 'left',
112             -fill => 'y');
113 0           my $fr_mid = $frame-> Frame(-relief => 'sunken',
114             -borderwidth => 2,
115             )->pack(-side => 'left',
116             -padx => 10);
117 0           my $fr_right = $frame-> Frame()->pack(-side => 'left',
118             -fill => 'y',
119             -padx => 20);
120              
121 0           my $fr_overr = $fr_left->Frame()->pack(-anchor => 'nw',
122             -pady => 1
123             );
124 0           my $fr_source= $fr_left->Frame()->pack(-anchor => 'nw',
125             -pady => 1,
126             );
127 0           $fr_overr->Label(-width => 1,
128             -bg => 'orange')->pack(-side => 'left');
129 0           $fr_overr->Label(-text => 'overridden if called as a method',
130             )->pack(-side => 'left');
131 0           $fr_source->Label(-width => 1,
132             -bg => 'green')->pack(-side => 'left');
133 0           $fr_source->Label(-text => 'sourcecode can be displayed',
134             )->pack(-side => 'left');
135 0   0       my @btn_data = (['Classname',\$self->{classname}],
136             ['Filter' ,\($self->{filter}||='')]);
137              
138 0           @$self{qw/entry_cl entry_f/}=
139 0           map {my $e = $fr_mid -> LabEntry(-label => $_->[0],
140             -textvariable=> $_->[1],
141             -labelPack => [-side=>'left'],
142             ) ->pack(-anchor => 'e');
143 0           $e->Subwidget('entry')->configure(-background => 'white');
144 0           $e;
145             } @btn_data;
146              
147              
148 0     0     my $btn = $fr_mid -> Button (-text => 'show methods',
149             -command=> sub{$self->show_methods}
150 0           )->pack;
151 0           my $text = $self -> Scrolled('ROText',
152             -wrap => 'none',
153             -insertontime => 0,
154             )->pack(-fill => 'both',
155             -expand => 1,
156             );
157 0           my $font = $self -> fontCreate(-family => 'Courier',
158             -size => 12,
159             );
160 0           $text->configure(-font=>$font);
161 0           $text->tagConfigure('overridden',-background => 'orange');
162 0           $text->tagConfigure('source_ok' ,-background => 'green');
163 0           $text->tagConfigure('white' ,-background => 'white');
164              
165 0           $text->menu(undef); #disable
166              
167 0           $self -> Label(-textvariable=>\$self->{status})->pack;
168              
169 0           $fr_right->Label(-text => 'Fontsize:',
170             )->pack(-side => 'left',
171             -padx => 10,
172             );
173 0           my $ne;
174             $ne = $fr_right->NumEntry(-minvalue => 8,
175             -maxvalue => 16,
176             -value => 12,
177             -width => 3,
178             -readonly => 1,
179             -browsecmd=> sub{
180 0     0     $self->_change_fontsize($ne->cget('-value'))
181             },
182 0           )->pack(-side => 'left');
183            
184 0     0     $text->bind('',sub{$ne->incdec(1)});
  0            
185 0     0     $text->bind('',sub{$ne->incdec(-1)});
  0            
186 0     0     $text->bind('<1>',sub{$self->_text_click});
  0            
187 0     0     $text->bind('',sub {$self->_adjust_selection});
  0            
188 0           for my $w (@$self{qw/entry_cl entry_f/}) {
189 0     0     $w->bind('',sub{$btn->Invoke});
  0            
190             }
191 0           $text->focus;
192              
193 0           @$self{qw/text font list/}= ($text,$font,[]);
194              
195 0           $self->ConfigSpecs(-background => [$text,'','','white'],
196             -classname => ['METHOD'],
197             -filter => ['METHOD'],
198             DEFAULT => ['SELF'],
199             );
200 0           return $self;
201             }
202              
203             sub _adjust_selection{
204 0     0     my $self = shift;
205 0           my $w = $self->{text};
206 0           $w->unselectAll;
207 0           $w->adjustSelect;
208 0           $w->selectLine;
209             }
210              
211             sub _change_fontsize{
212 0     0     my $self = shift;
213 0           my $size = $_[0];
214 0           my ($text,$font) = @$self{qw/text font/};
215 0           $text->fontConfigure($font,'-size',$size);
216             }
217              
218              
219             sub _text_click{
220 0     0     my $self = shift;
221 0           my $w = $self->{text};
222 0           my $position = $w->index('current');
223 0           my $line;
224 0 0         if ($position =~ m/^(\d+)\./) {
225 0           $line = $1;
226             } else {
227             return
228 0           }
229 0           my $idx = $line - 1; #line range starts at 1
230              
231 0           my $file = $self->{list}[$idx]{file};
232 0           my $methodname = $self->{list}[$idx]{sourcesymbol};
233 0           my $re = qq/sub\\s+$methodname(\\W.*)?\$/;
234 0           $self->_start_code_view($file,$re);
235             }
236              
237             sub _get_methods{
238 0     0     my $self = shift;
239 0           my $class_name = $self->{classname};
240 0           my $filter = $self->{filter};
241 0           my $regex = qr/$filter/i ;
242              
243 0           my @function_list;
244 0           my $classes = mro::get_linear_isa($class_name);
245 0           my %overridden;
246 0           foreach my $class (@$classes) {
247 1     1   1559 no strict 'refs';
  1         2  
  1         75  
248 0           my @list;
249 0           my $s_t_r = \%{$class."::"};
  0            
250 1     1   5 use strict ;
  1         2  
  1         2343  
251 0           foreach my $key ( keys %$s_t_r) {
252 0 0         next unless ($key =~ $regex);
253 0           my $var = \ ( $s_t_r->{$key} );
254 0           my $state;
255 0           ref $var eq 'GLOB' && *{$var}{CODE}
  0            
256             && ($state = 'declared')
257 0 0 0       && defined &{*{$var}{CODE}} && ($state = 'defined');
  0   0        
      0        
258              
259 0 0 0       ref $var eq 'SCALAR' && $$var == -1 && ($state = 'declared');
260            
261 0 0         if ($state) {
262 0   0       my $overridden = $overridden{$key} || 0;
263 0           my $definition = '';
264 0           my $file = '';
265 0 0         if ($state eq 'defined'){
266 0           $definition .= CvGV(*{$var}{CODE});
  0            
267 0           my $o = B::svref_2object(*{$var}{CODE});
  0            
268 0           $file = $o->FILE;# to do: fix .al
269             }
270 0           $overridden{$key} = 1;
271 0           push @list , {symbol => $key,
272             state => $state,
273             package => $class,
274             overridden => $overridden,
275             defined_as => $definition,
276             file => $file,
277             };
278             }
279             }
280 0           @list = sort {lc $a->{symbol}cmp lc $b->{symbol}} @list;
  0            
281 0           push @function_list,@list;
282             }
283 0           $self->{list} = \@function_list;
284 0           return $self;
285             }
286              
287             sub _grep_sources{
288 0     0     my $self = shift;
289 0           my $list = $self->{list};
290 0           $self->_set_source_fields;
291 0           my $last_filename = '';
292 0           my $module_source = '';
293 0           for my $element (@$list) {
294              
295 0           my $converted = $self-> _convert_filename($element->{file});
296 0 0         $element->{file} = $converted if $converted;
297 0 0         unless ($element->{file}){
298             # fallback: check package file for autosplit defs
299 0           $element->{file}
300             = $self-> _convert_packagename($element->{package});
301             }
302 0           my $filename = $element->{file};
303 0 0         next unless $filename;
304 0 0 0       if ($filename && ($filename ne $last_filename)){
305 0   0       $module_source = read_file($filename, err_mode=>'quiet') || '';
306 0           $last_filename = $filename;
307             }
308 0           my $symbol = $element->{sourcesymbol};
309 0 0         $element->{source_avail}
310             = ($module_source =~/sub\s+$symbol(\W.*)?$/m)?
311             1 : 0;
312            
313             }
314 0           return $self;
315             }
316              
317             sub _set_source_fields{
318 0     0     my $self = shift;
319 0           my $list = $self->{list};
320 0           for my $element (@$list) {
321 0 0         if ($element->{defined_as} =~ /\*(.*)::(.*)$/){
322 0           $element->{sourcepackage} = $1;
323 0           $element->{sourcesymbol} = $2;
324 0           $element->{defined_as} =~ s/^\*/alias to: /;
325             }
326 0           my $is_alias = 0;
327 0           for (qw/symbol package/){
328 0   0       $element->{"source$_"}||= $element->{$_};
329 0 0         unless($element->{$_} eq $element->{"source$_"}){
330             # $defined_as = $element->{defined_as};
331 0           $is_alias = 1;
332 0           last;
333             }
334             }
335 0 0         $element->{defined_as} = '' unless $is_alias;
336             }
337             }
338              
339              
340             sub show_methods{
341 0     0     my $self = shift;
342 0           my ($text,$classname) = @$self{qw/text classname/};
343 0           $text->delete('1.0','end');
344 0           $self->{indexmap} = [];
345              
346 0           eval "require $classname";
347             # now check if package $classname is loaded -
348             # package $classname needn't be defined in the required file...
349              
350              
351 0 0         unless (Class::Inspector->loaded($classname)) {
352 0           $self->{list}= [];
353 0           $self->{status}="Error: package '$classname' not loaded!";
354 0           return;
355             }
356              
357 0           $self->{status}="Showing methods for '$classname'";
358              
359 0           $self->{inc_files} = {map {$INC{$_}, 1} keys(%INC)};
  0            
360              
361 0           $self->_get_methods
362             ->_grep_sources;
363 0           my $list = $self->{list};
364 0           my %max_width = ( symbol => 0,
365             package => 0,
366             defined_as => 0,
367             file => 0,
368             );
369 0           for my $element (@$list) {
370 0           map {my $length = length($element->{$_})+2;
  0            
371 0 0         $max_width{$_} = $length if $length > $max_width{$_};
372             } qw/symbol package defined_as file/;
373             }
374 0           for my $element (@$list) {
375 0           my $line = sprintf( '%-'.$max_width{package}.'s'
376             .'%-'.$max_width{symbol}.'s'
377             .'%-'.$max_width{file}.'s'
378             .'%-12s'
379             .'%-'.$max_width{defined_as}.'s',
380              
381             $element->{package},
382             $element->{symbol} ,
383             $element->{file},
384             $element->{state},
385             $element->{defined_as},
386             )."\n";
387 0 0         $text->insert('end',# provide pairs of content, tag:
    0          
388             ' ',
389             $element->{overridden} ? 'overridden': 'white',# tag
390             ' ',
391             $element->{source_avail}? 'source_ok': 'white',# tag
392             $line, '');
393             }
394 0           return $self;
395             }
396              
397             sub _convert_filename{
398 0     0     my ($self,$filename) = @_;
399 0           my $inc_files = $self->{inc_files};
400              
401 0 0         my $path_name = exists ($inc_files->{$filename})? $filename : '';
402             # If $filename is not in $inc_files, it might be a .al file:
403 0 0         unless ($path_name){
404 0 0         if ($filename =~ m|autosplit into .*lib.auto.(.*\.al)|){
405 0           my $seg = $1;
406 0           $seg =~ y|\\|/|;
407 0           for (keys %$inc_files){
408 0 0         if ($_ =~ /$seg/){
409 0           $path_name = $_;
410 0           last;
411             }
412             }
413             }
414             }
415 0           return $path_name;
416             }
417             sub _convert_packagename{
418 0     0     my ($self,$package) = @_;
419 0           $package =~ s#::#/#g;
420 0           $package.='.pm';
421 0   0       return $INC{$package}||'';
422             }
423             sub classname{
424 0     0     my ($self,$classname) = @_;
425 0 0         $self->{classname} = $classname if $classname;
426 0           $self->{classname};
427             }
428             sub filter{
429 0     0     my ($self,$filter) = @_;
430 0           $self->{filter} = $filter;
431 0           $filter;
432             }
433              
434             sub _start_code_view{
435 0     0     my $self = shift;
436 0           my ($filename,$regex)=@_;
437 0 0         return unless $filename;
438 0           my $c_v = $self->{c_v};
439 0           $self->{c_v_entry_filter}= $regex;
440 0 0 0       unless ($c_v && $c_v->Exists){
441 0           $self->_code_view_init_top();
442 0           $c_v = $self->{c_v};
443             } else {
444 0           $c_v->deiconify;
445 0           $c_v->raise;
446             }
447 0           my $text = $self->{c_v_text};
448 0           $text->delete('0.0','end');
449              
450 0           my $content = read_file($filename,
451             err_mode=> 'quiet',
452             );
453 0 0         unless ($content){
454 0           $self->messageBox(-message => "No file '$filename' found",
455             # -font => 'Helvetica 14',
456             -title => 'Error',
457             );
458 0           $c_v->withdraw;
459 0           return;
460             }
461 0           $c_v->configure(-title=>$filename);
462 0           $text->insert('end',$content);
463 0           $c_v->focus();
464 0 0         $self->_c_v_filter_changed() if $regex;
465             }
466             sub _code_view_init_top{
467 0     0     my $self = shift;
468 0           my $c_v = $self->Toplevel();
469 0           my $top_fr = $c_v->Frame()->pack;
470 0           my $frame = $top_fr->Frame()->pack;
471 0           my $text = $c_v->Scrolled('ROText',
472             -wrap => 'none',
473             -bg => 'white',
474             )->pack(-fill => 'both',
475             -expand => 1,
476             );
477 0   0       my $entry = $frame ->LabEntry(-label => 'Filter',
478             -labelPack => [-side=>'left'],
479             -textvariable=>\($self->{c_v_entry_filter}||=''),
480             -bg =>'white'
481             )->pack(-side => 'left',
482             );
483 0           my $font = $self -> fontCreate(-family => 'Courier',
484             -size => 12,
485             );
486              
487 0           $text->configure(-font => $font);
488              
489 0     0     $entry->bind('',sub {$self->_c_v_filter_changed});
  0            
490              
491 0     0     $frame->Button(-text =>'Find Next',
492             -command => sub{$self->_c_v_filter_changed},
493 0           )->pack(-side => 'left',
494             -padx => 10);
495 0           $frame->Label(-text => 'Fontsize:')->pack(-side => 'left',
496             -padx => 10);
497 0           my $ne;
498             $ne = $frame->NumEntry(-minvalue => 8,
499             -maxvalue => 16,
500             -value => 12,
501             -width => 3,
502             -readonly => 1,
503             -browsecmd=> sub{
504 0     0     $self->_c_v_change_fontsize(
505             $ne->cget('-value'))
506             },
507 0           )->pack(-side => 'left');
508              
509 0     0     $text->bind('',sub{$ne->incdec(1)});
  0            
510 0     0     $text->bind('',sub{$ne->incdec(-1)});
  0            
511              
512 0           @$self{qw/c_v c_v_text c_v_font/} = ($c_v,$text,$font);
513             #allow one code_view window only:
514 0     0     $c_v->protocol("WM_DELETE_WINDOW",sub{$c_v->withdraw});
  0            
515             }
516             sub _c_v_filter_changed{
517 0     0     my $self = shift;
518 0           my $text = $self->{c_v_text};
519 0           $text->focus;
520 0           $text->FindNext(-forward=>'-regex','-case',$self->{c_v_entry_filter});
521             }
522              
523             sub _c_v_change_fontsize{
524 0     0     my $self = shift;
525 0           my $size = $_[0];
526 0           my ($text,$font) = @$self{qw/c_v_text c_v_font/};
527 0           $text->fontConfigure($font,'-size',$size);
528             }
529              
530             sub _test_{
531 0     0     my $mw = Tk::tkinit();
532 0           $mw->PerlMethodList(-classname=>'Tk::MainWindow')->show_methods;
533              
534 0           Tk::MainLoop();
535             }
536             1;