The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
#!/usr/bin/perl -w
# -*- perl -*-

#
# $Id: tkxmlview,v 1.14 2006/09/01 20:14:34 eserte Exp $
# Author: Slaven Rezic
#
# Copyright (C) 2000, 2003, 2004, 2006 Slaven Rezic. All rights reserved.
# This program is free software; you can redistribute it and/or
# modify it under the same terms as Perl itself.
#
# Mail: srezic@cpan.org
# WWW:  http://www.sourceforge.net/projects/srezic
#

use Tk;
use Tk::XMLViewer;
use Getopt::Long;
use strict;

my $indent;
my $mainbg;
my $ua;
my $do_loaddtd;
my $show_xpath;

my $top = new MainWindow;

GetOptions("indent=i" => \$indent,
	   "mainbg=s" => \$mainbg,
	   "loaddtd!" => \$do_loaddtd,
	   "showxpath!" => \$show_xpath,
	  ) or die <<EOF;
usage: $0 [-indent level] [-mainbg color] [-[no]loaddtd]
	  [-showxpath] [X11 options] xmlfile
EOF

my $string;
my @url_history;
my @overall_history;
my $file = shift;
my $xmlviewer = $top->Scrolled("XMLViewer", -scrollbars => "osw"
			      )->pack(-expand => 1, -fill => "both");
if ($show_xpath) {
    require Tk::Balloon;
    my $bln = $top->Balloon(-balloonposition => 'mouse');
    $bln->attach($xmlviewer->Subwidget("scrolled"));
}
$xmlviewer->configure(-background => $mainbg) if defined $mainbg;
$xmlviewer->SetIndent($indent) if defined $indent;
$xmlviewer->XMLMenu;
my $filemenu;
if ($xmlviewer->can("menu")) {
    my $textmenu = $xmlviewer->menu;
    if ($textmenu) {
	$filemenu = $textmenu->entrycget(0, -menu);
	# Reverse order, and assume there's no tear-off
	$filemenu->insert(0, "separator");
	$filemenu->insert(0, "command",
			  -label => 'Re-Open',
			  -command => sub {
			      if ($xmlviewer->SourceType eq 'file'){
				  open_file_or_url();
			      }
			  });
	if (eval { require LWP::UserAgent }) {
	    $filemenu->insert(0, "command",
			      -label => "Open URL",
			      -command => sub { guiopenurl() });
	}
	$filemenu->insert(0, "command",
			  -label => 'Open',
			  -command => sub { openxml(); viewxml() });

	# for the history list:
	$filemenu->separator;

	my $editmenu = $textmenu->entrycget(1, -menu);
	$editmenu->separator;
	$editmenu->command
	    (-label => 'Edit file...',
	     -command => sub {
		 require Tk::TextEdit;
		 package Tk::TextEdit;
		 sub Save {
		     my $self = shift;
		     my $r = $self->SUPER::Save(@_);
		     main::viewxml();
		     $r;
		 }

		 package main;
		 my $top_editor = $top->Toplevel;
		 $top_editor->title("Edit $file");
		 my $editor = $top_editor->Scrolled
		     ('TextEdit',
		      -scrollbars => "osoe",
		      -wrap => "none",
		     )->pack(qw(-fill both -expand 1));
		 # XXX workaround bug in 3.004
		 $editor->SetGUICallbacks([]);
		 $editor->FileName($file);
		 $editor->Load;
	     });

	my $helpmenu = $textmenu->cascade(-tearoff => 0,
					  -label => 'Help');
	$helpmenu->command(-label => 'About',
			   -command => sub {
			       $top->messageBox
				   (-title   => 'About tkxmlview',
				    -message => "An XML viewer for Perl/Tk\n" .
				                "(c) 2000, 2003, 2004, 2006 by Slaven Rezic",
				    -type    => 'OK');
			   });
	$helpmenu->command(-label => 'Tk::XMLViewer POD',
			   -command => sub {
			       require Tk::Pod;
			       $top->Pod(-file => 'Tk/XMLViewer',
					 -title => 'Tk::XMLViewer POD');
			   });
    }

    $top->configure(-menu => $textmenu);
}

if (!defined $file) {
    openxml(); viewxml();
} else {
    open_file_or_url();
}

MainLoop;

sub open_file_or_url {
    # guess if it's an URL
    if ($file =~ /^(http|https|file|ftp):/) {
	openandviewurl();
    } else {
	viewxml();
    }
}

sub openxml {
    my $dir;
    if(defined $file && (index($file, "/") >= 0) ) {
	$dir = substr($file, 0, rindex($file, "/"));
    }
    $file = $top->getOpenFile
	(-filetypes => [['Generic XML Files', '*.xml'],
			['XSLT Files', ['*.xslt','.xsl']],
			['RSS Files', '*.rss'],
			['RDF Files', '*.rdf'],
			['WML Files', '*.wml'],
			['All Files', '*']],
	 (defined $dir ? (-initialdir => $dir) : ()),
	);
    if (defined $file) {
	add_overall_history("file://$file");
    }
    $file;
}

sub guiopenurl {
    # XXX Dialog?
    my $t = $top->Toplevel(-title => "Open URL");
    $t->transient($top);
    my $e;
    my $continue = 0;
    my $url;
    my $Entry = "Entry";
    eval {
        # try loading the module, otherwise $Entry is left to the value "Entry"
        require Tk::HistEntry;
        $Entry = "SimpleHistEntry";
    };
    Tk::grid(
	     $t->Label(-text => "URL:"),
	     $e = $t->$Entry(-textvariable => \$url),
	     $t->Button(-text => "OK",
			-command => sub { $continue = 1 }),
	     $t->Button(-text => "Cancel",
			-command => sub { $continue = -1 }),
	    );
    if ($e->can('history')) {
	$e->history(\@url_history);
    }
    $e->bind("<Return>" => sub { $continue = 1 });
    $e->bind("<Escape>" => sub { $continue = -1 });
    $e->focus;
    $t->waitVariable(\$continue);
    $t->destroy if Tk::Exists($t);
    if ($continue == 1) {
	$file = $url;
	return openandviewurl();
    } else {
	0;
    }
}

sub viewxml {
    if (defined $file) {
	my $fname;
	if(length $file > 40) {
	    if(rindex($file, "/") >= 0 ) {
		$fname = substr($file, rindex($file, "/")+1);
	    } else {
		$fname = $file;
	    }
	    if (length $fname > 40) {
		$fname = substr($fname, -38);
	    }
	    $fname = "... " .$fname;
	} else {
	    $fname = $file;
	}
	$top->title("XMLView: ".$fname);
	$xmlviewer->delete("1.0", "end");
	$xmlviewer->insertXML(-file => $file,
			      _add_insertxml_param(),
			     );
    }
}

sub openandviewurl {
    my($url) = $file;

    push @url_history, $url;
    add_overall_history($url);

    if (!$ua) {
	$ua = LWP::UserAgent->new(env_proxy => 1);
    }
    my $resp = $ua->get($url);
    if (!$resp->is_success) {
	$top->messageBox(-title => "Error",
			 -message => "GET was not successful: " . $resp->code,
			 -icon => "error",
			);
	return 0;
    }

    $top->title("XMLView: ".$url);
    $xmlviewer->delete("1.0", "end");
    $xmlviewer->insertXML(-text => $resp->content,
			  _add_insertxml_param(),
			 );

    1;
}

sub _add_insertxml_param {
    (-xmlparserargs => { ParseParamEnt => $do_loaddtd });
}

sub add_overall_history {
    my($url) = @_;
    unshift @overall_history, $url;
    my @new_overall_history;
    my %seen;
    for my $history_element (@overall_history) {
	next if $seen{$history_element};
	$seen{$history_element} = 1;
	push @new_overall_history, $history_element;
	last if @new_overall_history >= 10;
    }
    @overall_history = @new_overall_history;

    create_menu_last_projects();
}

# taken from tktimex
sub create_menu_last_projects {
    return if !$filemenu;
    # find last separator
    my $end = $filemenu->index('end');
    my $i = $end;
 LOOP: {
	while ($i >= 0) {
	    last LOOP if ($filemenu->type($i) eq 'separator');
	    $i--;
	}
	warn "SHOULD NOT HAPPEN: Separator in Menu File not found";
	return;
    }
    # delete anything from the item after the separator to the end
    if ($i < $end) {
	$filemenu->delete($i+1, 'end');
    }
    # insert last_projects
    $i = -1;
    foreach my $url (@overall_history) {
	$i++;
	if ($url =~ m{^file://(.*)}) {
	    my $path = $1;
	    $filemenu->command(-label => $path,
			       -command => sub {
				   $file = $path;
				   viewxml();
				   add_overall_history($url);
			       });
	} else {
	    $filemenu->command(-label => $url,
			       -command => sub {
				   openandviewurl($url);
				   add_overall_history($url);
			       });
	}
    }
}

__END__

=head1 NAME

tkxmlview - a simple Tk based XML viewer

=head1 SYNOPSIS

    tkxmlview [-indent amount] [-mainbg color] [-loaddtd] file_or_url

=head1 DESCRIPTION

B<tkxmlview> displays the XML from the specified file or URL as a tree.

=head2 OPTIONS

=over

=item B<-indent> I<amount>

Set the indentation width in pixels. The default is taken from
L<Tk::XMLViewer>.

=item B<-mainbg> I<color>

Set the background color of the main window. The I<color> is any valid
Tk color name.

=item B<-loaddtd>

Causes tkxmlview to load any external DTD specified in the XML file.

=back

=head1 AUTHOR

Slaven Rezic

=head1 SEE ALSO

L<Tk::XMLViewer>.

=cut