# -*- perl -*- # # $Id: Tree.pm,v 5.4 2007/05/10 20:11:09 eserte Exp $ # Author: Slaven Rezic # # Copyright (C) 2001,2004,2007 Slaven Rezic. All rights reserved. # This package is free software; you can redistribute it and/or # modify it under the same terms as Perl itself. # # Mail: slaven@rezic.de # WWW: http://www.rezic.de/eserte/ # package Tk::Pod::Tree; =head1 NAME Tk::Pod::Tree - list Pod file hierarchy =head1 SYNOPSIS use Tk::Pod::Tree; $parent->PodTree; =head1 WIDGET-SPECIFIC OPTIONS =over 4 =item Name: B<-showcommand> Specifies a callback for selecting a Pod module (Button-1 binding). =item Name: B<-showcommand2> Specifies a callback for selecting a Pod module in a different window (Button-2 binding). =item Name: B<-usecache> True, if a cache of Pod modules should be created and used. The default is true. =back =head1 DESCRIPTION The B widget shows all available Perl Pod documentation in a tree. =cut use strict; use vars qw($VERSION @ISA @POD %EXTRAPODDIR $FindPods $ExtraFindPods); $VERSION = sprintf("%d.%02d", q$Revision: 5.4 $ =~ /(\d+)\.(\d+)/); use base 'Tk::Tree'; use File::Spec; use Tk::Pod::FindPods; use Tk::ItemStyle; use Tk qw(Ev); Construct Tk::Widget 'PodTree'; my $search_history; use constant SEP => "/"; BEGIN { @POD = @INC } BEGIN { # Make a DEBUG constant very first thing... if(defined &DEBUG) { } elsif(($ENV{'TKPODDEBUG'} || '') =~ m/^(\d+)/) { # untaint my $debug = $1; *DEBUG = sub () { $debug }; } else { *DEBUG = sub () {0}; } } ###################################################################### use Class::Struct; struct '_PodEntry' => [ 'uri' => "\$", 'type' => "\$", 'name' => "\$", ]; sub _PodEntry::create { my $e = shift->new; $e->uri(shift); $e; } sub _PodEntry::file { my $uri = shift->uri; local $^W = 0; ($uri =~ /^file:(.*)/)[0]; } ###################################################################### sub Dir { my $class = shift; unshift @POD, @_; $EXTRAPODDIR{$_} = 1 for (@_); } sub ClassInit { my ($class,$mw) = @_; $class->SUPER::ClassInit($mw); $mw->bind($class, '<3>', ['PostPopupMenu', Ev('X'), Ev('Y')] ) if $Tk::VERSION > 800.014; my $set_anchor_and_sel = sub { my($w, $ent) = @_; $w->anchorSet($ent); $w->selectionClear; $w->selectionSet($ent); }; # Force callbacks to be treated as methods. This is done by putting # the $widget reference at the beginning of the Tk::Callback array my $inherited_cb = sub { my($w, $cb) = @_; if (UNIVERSAL::isa($cb, "Tk::Callback")) { my $new_cb = bless [$w, @$cb], 'Tk::Callback'; $new_cb->Call; } else { # XXX OK? $cb->($w); } }; # Add functionality to some callbacks: my $orig_home = $mw->bind($class, ""); $mw->bind($class, "" => sub { my $w = shift; $inherited_cb->($w, $orig_home); $set_anchor_and_sel->($w, ($w->infoChildren)[0]); }); my $orig_end = $mw->bind($class, ""); $mw->bind($class, "" => sub { my $w = shift; $inherited_cb->($w, $orig_end); # get last opened entry my $last = ($w->infoChildren)[-1]; while ($w->getmode($last) eq "close" && $w->infoChildren($last)) { $last = ($w->infoChildren($last))[-1]; } $set_anchor_and_sel->($w, $last); }); my $orig_prior = $mw->bind($class, ""); $mw->bind($class, "" => sub { my $w = shift; $inherited_cb->($w, $orig_prior); my $ent = $w->nearest(10); # XXX why 10? return if !defined $ent; $set_anchor_and_sel->($w, $ent); }); my $orig_next = $mw->bind($class, ""); $mw->bind($class, "" => sub { my $w = shift; $inherited_cb->($w, $orig_next); my $ent = $w->nearest($w->height - 10); # XXX why 10? return if !defined $ent; $set_anchor_and_sel->($w, $ent); }); } sub Populate { my($w,$args) = @_; $args->{-separator} = SEP; my $show_command = sub { my($w, $cmd, $ent) = @_; my $data = $w->info('data', $ent); if ($data) { $w->Callback($cmd, $w, $data); } }; my $show_command_mouse = sub { my $w = shift; my $cmd = shift || '-showcommand'; my $Ev = $w->XEvent; my $ent = $w->GetNearest($Ev->y, 1); return unless (defined $ent and length $ent); my @info = $w->info('item',$Ev->x, $Ev->y); if (defined $info[1] && $info[1] eq 'indicator') { $w->Callback(-indicatorcmd => $ent, ''); return; } $show_command->($w, $cmd, $ent); }; my $show_command_key = sub { my $w = shift; my $cmd = shift || '-showcommand'; my($ent) = $w->selectionGet; return unless (defined $ent and length $ent); if ($w->info('children', $ent)) { $w->open($ent); } $show_command->($w, $cmd, $ent); }; $w->bind("<1>" => sub { $show_command_mouse->(shift) }); foreach (qw/space Return/) { $w->bind("<$_>" => sub { $show_command_key->(shift) }); } foreach (qw/2 Shift-1/) { $w->bind("<$_>" => sub { $show_command_mouse->(shift, '-showcommand2') }); } $w->SUPER::Populate($args); $w->{Style} = {}; $w->{Style}{'core'} = $w->ItemStyle('imagetext', -foreground => '#006000', -selectforeground => '#006000', ); $w->{Style}{'site'} = $w->ItemStyle('imagetext', -foreground => '#702000', -selectforeground => '#702000', ); $w->{Style}{'vendor'} = $w->ItemStyle('imagetext', -foreground => '#856b48', -selectforeground => '#856b48', ); $w->{Style}{'cpan'} = $w->ItemStyle('imagetext', -foreground => '#000080', -selectforeground => '#000080', ); $w->{Style}{'folder'} = $w->ItemStyle('imagetext', -foreground => '#606060', -selectforeground => '#606060', ); my $m = $w->Menu(-tearoff => $Tk::platform ne 'MSWin32'); eval { $w->menu($m) }; warn $@ if $@; $m->command(-label => 'Reload', -command => sub { $w->toplevel->Busy(-recurse => 1); eval { $w->Fill(-nocache => 1); }; my $err = $@; $w->toplevel->Unbusy(-recurse => 1); die $err if $err; }); $m->command(-label => 'Search...', -command => [$w, 'search_dialog']); $w->ConfigSpecs( -showcommand => ['CALLBACK', undef, undef, undef], -showcommand2 => ['CALLBACK', undef, undef, undef], -usecache => ['PASSIVE', undef, undef, 1], ); } =head1 WIDGET METHODS =over 4 =item I<$tree>-EB(?I<-nocache =E 1>?) Find Pod modules and fill the tree widget. If I<-nocache> is specified, then no cache will be used for loading. A cache of Pod modules is written unless the B<-usecache> configuration option of the widget is set to false. =cut sub Fill { my $w = shift; my(%args) = @_; $w->delete("all"); my $usecache = ($w->cget('-usecache') && !$args{'-nocache'}); # fills %pods hash: $FindPods = Tk::Pod::FindPods->new unless $FindPods; my $pods = $FindPods->pod_find(-categorized => 1, -usecache => $usecache, ); if (keys %EXTRAPODDIR) { $ExtraFindPods = Tk::Pod::FindPods->new unless $ExtraFindPods; my $extra_pods = $ExtraFindPods->pod_find (-categorized => 0, -category => "local dirs", -directories => [keys %EXTRAPODDIR], -usecache => 0, ); while(my($k,$v) = each %$extra_pods) { $pods->{$k} = $v; } } my %category_seen; foreach (['perl', 'Perl language'], ['pragma', 'Pragmata'], ['mod', 'Modules'], ['script', 'Scripts'], keys(%$pods), ) { my($category, $title) = (ref $_ ? @$_ : ($_, $_)); next if $category_seen{$category}; $w->add($category, -text => $title); my $hash = $pods->{$category}; foreach my $pod (sort keys %$hash) { my $treepath = $category . SEP . $pod; (my $title = $pod) =~ s|/|::|g; $w->_add_parents($treepath); my $loc = Tk::Pod::FindPods::module_location($hash->{$pod}); my $is = $w->{Style}{$loc}; my @entry_args = ($treepath, -text => $title, -data => _PodEntry->create($hash->{$pod}), ($is ? (-style => $is) : ()), ); if ($w->info('exists', $treepath)) { $w->entryconfigure(@entry_args); } else { $w->add(@entry_args); } } $category_seen{$category}++; } for(my $entry = ($w->info('children'))[0]; defined $entry && $entry ne ""; $entry = $w->info('next', $entry)) { if ($w->info('children', $entry) || $w->entrycget($entry, -text) eq 'perlfunc') { $w->folderentry($entry); } else { $w->entryconfigure($entry, -image => $w->Getimage("file")); $w->hide('entry', $entry); } } if ($w->cget('-usecache') && !$FindPods->has_cache) { $FindPods->WriteCache; } $w->{Filled}++; } sub folderentry { my($w, $entry) = @_; $w->entryconfigure($entry, -image => $w->Getimage("folder")); $w->setmode($entry, 'open'); if ($entry =~ m|/|) { # XXX SEP? $w->hide('entry', $entry); } } sub Filled { shift->{Filled} } sub _add_parents { my($w, $entry) = @_; (my $parent = $entry) =~ s|/[^/]*$||; # XXX SEP? return if $parent eq ''; do{warn "XXX Should not happen: $entry eq $parent";return} if $parent eq $entry; return if $w->info('exists', $parent); my @parent = split SEP, $parent; my $title = join "::", @parent[1..$#parent]; $w->_add_parents($parent); $w->add($parent, -text => $title, ($w->{Style}{'folder'} ? (-style => $w->{Style}{'folder'}) : ())); } sub _open_parents { my($w, $entry) = @_; (my $parent = $entry) =~ s|/[^/]+$||; # XXX SEP? return if $parent eq '' || $parent eq $entry; $w->_open_parents($parent); $w->open($parent); } =item I<$tree>-EB($path) Move the anchor/selection and view to the given C<$path> and open subtrees to make the C<$path> visible, if necessary. =cut sub SeePath { my($w,$path) = @_; my $fs_case_tolerant = ($^O eq 'MSWin32' || $^O eq 'darwin' || # case_tolerant=0 here! (File::Spec->can("case_tolerant") && File::Spec->case_tolerant) ); if ($^O eq 'MSWin32') { $path =~ s/\\/\//g; } if ($fs_case_tolerant) { $path = lc $path; } DEBUG and warn "Call SeePath with $path\n"; return if !$w->Filled; # not yet filled return if !$FindPods; my $pods = $FindPods->pods; return if !$pods; my $see_treepath = sub { my $treepath = shift; $w->open($treepath); $w->_open_parents($treepath); $w->anchorSet($treepath); $w->selectionClear; $w->selectionSet($treepath); $w->see($treepath); }; foreach my $category (keys %$pods) { foreach my $pod (keys %{ $pods->{$category} }) { my $podpath = $pods->{$category}->{$pod}; $podpath = lc $podpath if $fs_case_tolerant; if ($path eq $podpath) { my $treepath = $category . SEP . $pod; $see_treepath->($treepath); return 1; } } } DEBUG and warn "SeePath: cannot find $path in tree\n"; 0; } sub GetCurrentPodPath { my $w = shift; my $sel_entry = ($w->selectionGet)[0]; if (defined $sel_entry) { my @c = split m{/}, $sel_entry; shift @c; my $pod = join "::", @c; return $pod; } } sub search_dialog { my($w) = @_; my $t = $w->Toplevel(-title => "Search"); $t->transient($w); $t->Label(-text => "Search module:")->pack(-side => "left"); my $term; my $Entry = 'Entry'; eval { require Tk::HistEntry; Tk::HistEntry->VERSION(0.40); $Entry = "HistEntry"; }; my $e = $t->$Entry(-textvariable => \$term)->pack(-side => "left"); if ($e->can('history') && $search_history) { $e->history($search_history); } $e->focus; $e->bind("" => sub { $t->destroy }); my $do_search = sub { if ($e->can('historyAdd')) { $e->historyAdd($term); $search_history = [ $e->history ]; } $w->search($term); }; $e->bind("" => $do_search); { my $f = $t->Frame->pack(-fill => "x"); Tk::grid($f->Button(-text => "Search", -command => $do_search, ), $f->Button(-text => "Close", -command => sub { $t->destroy }, ), -sticky => "ew"); } } sub search { my($w, $rx) = @_; return if $rx eq ''; my($entry) = ($w->info('selection'))[0]; if (!defined $entry) { $entry = ($w->info('children'))[0]; return if (!defined $entry); } my $wrapped = 0; while(1) { $entry = $w->info('next', $entry); if (!defined $entry) { if ($wrapped) { $w->bell; return; } $wrapped++; $entry = ($w->info('children'))[0]; } my $text = $w->entrycget($entry, '-text'); if ($text =~ /$rx/i) { my $p = $entry; while(1) { $p = $w->info('parent', $p); if (defined $p) { $w->open($p); } else { last; } } $w->selectionClear; $w->selectionSet($entry); $w->anchorSet($entry); $w->see($entry); return; } } } sub IndicatorCmd { my($w, $ent, $event) = @_; my $podentry = $w->entrycget($ent, "-data"); my $file = $podentry && $podentry->file; my $type = $podentry && $podentry->type; # Dynamically create children for perlfunc entry if (defined $type && $type =~ /^func_/ && !$w->info('children', $ent)) { require Pod::Functions; my $add_func = sub { my($ent, $func) = @_; my $podentry = _PodEntry->new; $podentry->type("func"); $podentry->name($func); (my $safe_name = $func) =~ s{[^a-zA-Z]}{_}g; $ent = $ent . SEP . $safe_name; $w->add($ent, -text => $func, -data => $podentry, -style => $w->{Style}{'core'}); }; if ($type eq 'func_alphabetically') { my $last_func; my @funcs = map { if (!defined $last_func || $last_func ne $_) { $last_func = $_; ($_); } else { $last_func = $_; (); } } sort map { @{ $Pod::Functions::Kinds{$_} } } keys %Pod::Functions::Kinds; for my $func (@funcs) { $add_func->($ent, $func); } } else { # by category for my $cat (sort keys %Pod::Functions::Kinds) { (my $safe_name = $cat) =~ s{[^a-zA-Z]}{_}g; my $ent = $ent . SEP . $safe_name; $w->add($ent, -text => $cat, -style => $w->{Style}{'folder'}); my $funcs = $Pod::Functions::Kinds{$cat}; for my $func (@$funcs) { $add_func->($ent, $func); } } } } elsif (defined $file && $file =~ /perlfunc\.pod$/ && !$w->info('children', $ent)) { my($treepath, $podentry); $treepath = $ent . SEP. "func_alphabetically"; $podentry = _PodEntry->new; $podentry->type("func_alphabetically"); $w->add($treepath, -text => "Alphabetically", -data => $podentry, -style => $w->{Style}{'folder'}); $w->folderentry($treepath); $treepath = $ent . SEP. "func_by_category"; $podentry = _PodEntry->new; $podentry->type("func_by_category"); $w->add($treepath, -text => "By category", -data => $podentry, -style => $w->{Style}{'folder'}); $w->folderentry($treepath); } $w->SUPER::IndicatorCmd($ent, $event); } 1; __END__ =back =head1 SEE ALSO L, L, L, L. =head1 AUTHOR Slaven ReziE<0x107> > Copyright (c) 2001,2004 Slaven ReziE<0x107>. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut