#!/usr/bin/perl
# $Id: podbrowser.pl,v 1.20 2004/09/18 13:32:10 jodrell Exp $
# Copyright (c) 2004 Gavin Brown. All rights reserved. This program is free
# software; you can redistribute it and/or modify it under the same terms
# as Perl itself.
use Gtk2 -init;
use Gtk2::GladeXML 1.001;
use Gtk2::SimpleList;
use Gtk2::PodViewer 0.08;
use Gtk2::PodViewer::Parser qw(decode_entities);
use Gnome2;
use Locale::gettext;
use POSIX qw(setlocale);
use strict;

### set up global variables:
my $NAME		= 'PodBrowser';
my $VERSION		= '0.03';
my $PREFIX		= '/usr';
my $GLADE_FILE		= (-d $PREFIX ? sprintf('%s/share/%s', $PREFIX, lc($NAME)) : $ENV{PWD}).sprintf('/%s.glade', lc($NAME));
my $LOCALE_DIR		= (-d $PREFIX ? "PREFIX/share/locale" : $ENV{PWD}.'/locale');

my $SEARCH_OFFSET	= 0;
my $RCFILE		= sprintf('%s/.%src', $ENV{HOME}, lc($NAME));
my $MAXIMIZED		= 0;
my $FULLSCREEN		= 0;
my $OPTIONS		= load_config();
my @HISTORY		= split(/\|/, $OPTIONS->{history});
my @BOOKMARKS		= split(/\|/, $OPTIONS->{bookmarks});
my $BOOKMARK_ITEMS	= {};
my @FORWARD;
my @BACK;
my $CURRENT_DOCUMENT;
my $LAST_SEARCH_STR;

### set up l10n support:
setlocale(LC_ALL, $ENV{LANG});
bindtextdomain(lc($NAME), $LOCALE_DIR);
textdomain(lc($NAME));

### bits we'll be reusing:
chomp(my $OPENER	= `which gnome-open 2> /dev/null`);
my $APP			= Gtk2::GladeXML->new($GLADE_FILE);
my $THEME		= get_an_icon_theme();
my $TIPS		= Gtk2::Tooltips->new;
my $IDX_PBF		= $APP->get_widget('main_window')->render_icon('gtk-jump-to', 'menu');
my $PAGE_PBF		= Gtk2::Gdk::Pixbuf->new_from_file($THEME->lookup_icon('gnome-mime-text', 16, 'force-svg')->get_filename)->scale_simple(16, 16, 'bilinear');
my $FOLDER_PBF		= Gtk2::Gdk::Pixbuf->new_from_file($THEME->lookup_icon('gnome-fs-directory', 16, 'force-svg')->get_filename)->scale_simple(16, 16, 'bilinear');
my $NORMAL_CURSOR	= Gtk2::Gdk::Cursor->new('left_ptr');
my $BUSY_CURSOR		= Gtk2::Gdk::Cursor->new('watch');
my $ITEMS		= {};
my %categories		= (
	funcs		=> gettext('Functions'),
	modules		=> gettext('Modules'),
	pragma		=> gettext('Pragmas'),
	pods		=> gettext('POD Documents'),
);
my $CATEGORY_PBFS	= {};

### start building the UI:

$APP->signal_autoconnect_from_package(__PACKAGE__);
$APP->get_widget('location')->disable_activate;
$APP->get_widget('open_dialog_location')->disable_activate;

my $viewer = Gtk2::PodViewer->new;
$viewer->signal_connect('link_clicked' => \&link_clicked);
$viewer->signal_connect('link_enter', sub { set_status($_[1]) });
$viewer->signal_connect('link_leave', sub { set_status('') });
$APP->get_widget('viewer_scrwin')->add($viewer);
$viewer->show;

### build a SimpleList from the glade widget for the document index:
my $page_index	= Gtk2::SimpleList->new_from_treeview(
	$APP->get_widget('document_index'),
	icon	=> 'pixbuf',
	mark	=> 'text',
	link	=> 'hidden',
);

$page_index->get_selection->signal_connect('changed', sub {
	my $idx = ($page_index->get_selected_indices)[0];
	my $mark = $page_index->{data}[$idx][2];
	$viewer->jump_to($mark);
	return 1;
});

### build a tree widget for the full index:
my $model = Gtk2::TreeStore->new(qw/Gtk2::Gdk::Pixbuf Glib::String/);
$APP->get_widget('index')->set_model($model);
$APP->get_widget('index')->insert_column_with_attributes(
	0,
	'pixbuf',
	Gtk2::CellRendererPixbuf->new,
	pixbuf => 0,
);
$APP->get_widget('index')->insert_column_with_attributes(
	1,
	'document',
	Gtk2::CellRendererText->new,
	text => 1,
);

Glib::Timeout->add(500, sub {
	$ITEMS = generate_index();
	### populate the tree:
	foreach my $category (sort keys %{$ITEMS}) {
		if ($category ne '') {
			my $parent_iter = $model->append(undef);
			$model->set($parent_iter, 0, (defined($CATEGORY_PBFS->{$category}) ? $CATEGORY_PBFS->{$category} : $FOLDER_PBF));
			$model->set($parent_iter, 1, $categories{$category});
			foreach my $doc (sort keys %{$ITEMS->{$category}}) {
				if ($doc ne '') {
					my $iter = $model->append($parent_iter);
					$model->set($iter, 0, $PAGE_PBF);
					$model->set($iter, 1, $doc);
				}
			}
		}
	}
	return undef;
});

$APP->get_widget('index')->get_selection->signal_connect('changed', \&index_changed);

eval {
	$APP->get_widget('main_window')->set_icon(
		Gtk2::Gdk::Pixbuf->new_from_file($THEME->lookup_icon(lc($NAME), 16, 'force-svg')->get_filename)
	);
};
print STDERR $@;

### if the program was run with an argument, load the argument as a document:
if ($ARGV[0] ne '') {
	Glib::Timeout->add(50, sub {
		set_location($ARGV[0]);
		return undef;
	});
}

Glib::Timeout->add(50, \&timeout);

### apply the user's previous preferences:
$APP->get_widget('pane')->set_position($OPTIONS->{pane_position}	|| 200);
$APP->get_widget('vpane')->set_position(defined($OPTIONS->{vpane_position}) ? $OPTIONS->{vpane_position} : 250);
$APP->get_widget('show_index')->set_active($OPTIONS->{show_index} == 0 && defined($OPTIONS->{show_index}) ? undef : 1);
$APP->get_widget('location')->set_popdown_strings('', @HISTORY);
$APP->get_widget('main_window')->maximize if ($OPTIONS->{maximized} == 1);
$APP->get_widget('main_window')->set_default_size(
	($OPTIONS->{window_x} > 0 ? $OPTIONS->{window_x} : 800),
	($OPTIONS->{window_y} > 0 ? $OPTIONS->{window_y} : 600),
);

$APP->get_widget('main_window')->signal_connect('window-state-event', \&window_changed_state);

### load the bookmarks:
foreach my $bookmark (@BOOKMARKS) {
	add_bookmark_item($bookmark);
}

### construct the bookmarks list:
my $bookmarks_list = Gtk2::SimpleList->new_from_treeview(
	$APP->get_widget('bookmarks_list'),
	icon	=> 'pixbuf',
	doc	=> 'text',
);

$APP->get_widget('main_window')->show;

Gtk2->main;

exit;

### this runs every 50ms, and keeps track of the state of the navigation buttons and tooltips:
sub timeout {
	### the back button:
	if (scalar(@BACK) > 0 && !$APP->get_widget('back_button')->get('sensitive')) {
		$APP->get_widget('back_button')->set_sensitive(1);
		$TIPS->set_tip($APP->get_widget('back_button'), sprintf(gettext("Go back to '%s' (right-click for more)"), $BACK[scalar(@BACK) - 1]));

	} elsif (scalar(@BACK) < 1 && $APP->get_widget('back_button')->get('sensitive')) {
		$APP->get_widget('back_button')->set_sensitive(undef);
		$TIPS->set_tip($APP->get_widget('back_button'), '');

	}

	### the forward button:
	if (scalar(@FORWARD) > 0 && !$APP->get_widget('forward_button')->get('sensitive')) {
		$APP->get_widget('forward_button')->set_sensitive(1);
		$TIPS->set_tip($APP->get_widget('forward_button'), sprintf(gettext("Go forward to '%s' (right-click for more)"), $FORWARD[0]));

	} elsif (scalar(@FORWARD) < 1 && $APP->get_widget('forward_button')->get('sensitive')) {
		$APP->get_widget('forward_button')->set_sensitive(undef);
		$TIPS->set_tip($APP->get_widget('forward_button'), '');
	}

	### the up button:
	if ($APP->get_widget('location')->entry->get_text =~ /::/) {
		$APP->get_widget('up_button')->set_sensitive(1);
		my @parts = split(/::/, $APP->get_widget('location')->entry->get_text);
		pop(@parts);
		my $doc = join('::', @parts);
		$TIPS->set_tip($APP->get_widget('up_button'), sprintf(gettext("Go up to '%s' (right-click for more)"), $doc));

	} else {
		$APP->get_widget('up_button')->set_sensitive(undef);
		$TIPS->set_tip($APP->get_widget('up_button'), '');

	}

	### the go button:
	if ($APP->get_widget('location')->entry->get_text ne '' && !$APP->get_widget('go_button')->get('sensitive')) {
		$APP->get_widget('go_button')->set_sensitive(1);

	} elsif ($APP->get_widget('location')->entry->get_text eq '' && $APP->get_widget('go_button')->get('sensitive')) {
		$APP->get_widget('go_button')->set_sensitive(undef);

	}

	### the "add bookmark" item. turn off if location is blank or is already bookmarked:
	if ($APP->get_widget('location')->entry->get_text ne '' && !defined($BOOKMARK_ITEMS->{$APP->get_widget('location')->entry->get_text}) && !$APP->get_widget('add_bookmark_item')->get('sensitive')) {
		$APP->get_widget('add_bookmark_item')->set_sensitive(1);

	} elsif (($APP->get_widget('location')->entry->get_text eq '' || defined($BOOKMARK_ITEMS->{$APP->get_widget('location')->entry->get_text})) && $APP->get_widget('add_bookmark_item')->get('sensitive')) {
		$APP->get_widget('add_bookmark_item')->set_sensitive(undef);

	}
	return 1;
}

### pops up the 'open document' dialog:
sub open_dialog {
	$APP->get_widget('open_dialog')->set_icon($APP->get_widget('main_window')->get_icon);
	$APP->get_widget('open_dialog')->show_all;
	$APP->get_widget('open_dialog_location')->set_popdown_strings($APP->get_widget('location')->entry->get_text, @HISTORY);
	return 1;
}

### handles the 'open document' dialog response:
sub open_dialog_response {
	if ($_[1] eq 'ok' || $_[1] == 1) {
		set_location($APP->get_widget('open_dialog_location')->entry->get_text);
	}
	$APP->get_widget('open_dialog')->hide_all;
	return 1;
}
sub open_dialog_delete_event {
	$APP->get_widget('open_dialog')->hide_all;
	return 1;
}
sub on_open_dialog_location_activate {
	$APP->get_widget('open_dialog')->signal_emit('response', 1);
	return 1;
}
sub browse_button_clicked {
	my $dialog = Gtk2::FileChooserDialog->new(
		gettext('Choose File'),
		undef,
		'open',
		'gtk-cancel'	=> 'cancel',
		'gtk-ok'	=> 'ok'
	);
	$dialog->signal_connect('response', sub {
		if ($_[1] eq 'ok') {
			$APP->get_widget('open_dialog_location')->entry->set_text($dialog->get_filename);
		}
		$dialog->destroy;
	});
	$dialog->set_icon($APP->get_widget('main_window')->get_icon);
	$dialog->run;
	return 1;
}

### shows/hides the left pane of the window:
sub toggle_index {
	if ($_[0]->get_active) {
		$APP->get_widget('vpane')->show_all;
	} else {
		$APP->get_widget('vpane')->hide_all;
	}
	$OPTIONS->{show_index} = ($_[0]->get_active ? 1 : 0);

	return 1;
}

sub about {
	my $about = Gnome2::About->new(
		$NAME,
		$VERSION,
		gettext("This program is free software; you can redistribute\nit and/or modify it under the same terms as Perl itself."),
		gettext('A Perl Documentation Browser for GNOME'),
		[
			'Gavin Brown',
			'Torsten Schoenfeld',
			'Scott Arrington',
			'Steven Robson',
		],
		undef,
		undef,
		$APP->get_widget('main_window')->get_icon,
	);
	$about->set_icon($APP->get_widget('main_window')->get_icon);
	$about->show_all;
	return 1;
}

### this is used when the user requests a new document, via the location entry,
### the go button, the 'open document' dialog, the index or via a clicked link:
sub go {
	my $text = $APP->get_widget('location')->entry->get_text;

	$APP->get_widget('main_window')->window->set_cursor($BUSY_CURSOR);
	$viewer->get_window('text')->set_cursor($BUSY_CURSOR);
	Gtk2->main_iteration while (Gtk2->events_pending);

	if (!$viewer->load($text)) {
		$APP->get_widget('location')->entry->set_text($CURRENT_DOCUMENT);
		pop(@BACK);
		$APP->get_widget('main_window')->window->set_cursor($NORMAL_CURSOR);
		$viewer->get_window('text')->set_cursor($NORMAL_CURSOR);
		my $dialog = Gtk2::MessageDialog->new($APP->get_widget('main_window'), 'modal', 'error', 'ok', sprintf(gettext("Couldn't find a POD document for '%s'."), $text));
		$dialog->signal_connect('response', sub { $dialog->destroy });
		$dialog->run;

	} else {
		$CURRENT_DOCUMENT = $text;
		$APP->get_widget('main_window')->set_title(sprintf(gettext('%s - Pod Browser'), $text));
		$APP->get_widget('main_window')->window->set_cursor($NORMAL_CURSOR);
		$viewer->get_window('text')->set_cursor($NORMAL_CURSOR);

		### populate the index:
		@{$page_index->{data}} = ();
		map { push(@{$page_index->{data}}, [ $IDX_PBF, section_reformat(decode_entities($_)), $_ ]) } $viewer->get_marks;
		unshift(@HISTORY, $text);

		### update the history, removing duplicates:
		my %seen;
		for (my $i = 0 ; $i < scalar(@HISTORY) ; $i++) {
			if ($seen{$HISTORY[$i]} == 1) {
				splice(@HISTORY, $i, 1);
			} else {
				$seen{$HISTORY[$i]} = 1;
			}
		}
		$APP->get_widget('location')->set_popdown_strings(@HISTORY);
		$LAST_SEARCH_STR = '';

		### this goes through the site index, looking to see if the requested document is present, and
		### selects it if found:
		my $found = 0;
		for (my $i = 0 ; $i < scalar(keys(%categories)) ; $i++) {
			my $category = (grep { $_ ne '' } sort(keys(%categories)))[$i];
			for (my $j = 0 ; $j < scalar(keys(%{$ITEMS->{$category}})) ; $j++) {
				my $doc = (grep { $_ ne '' } sort(keys(%{$ITEMS->{$category}})))[$j];
				if ($doc eq $text) {
					$found++;
					$APP->get_widget('index')->expand_row(Gtk2::TreePath->new_from_indices($i), undef);

					unless ($APP->get_widget('index')->has_focus) {
						$APP->get_widget('index')->get_selection->select_path(Gtk2::TreePath->new_from_indices($i, $j));
						$APP->get_widget('index')->scroll_to_cell(Gtk2::TreePath->new_from_indices($i, ($j <= 4 ? 0 : $j - 4)), undef, 1, 0, 0);
					}
				}
			}
		}
		if ($found == 0) {
			$APP->get_widget('index')->collapse_all;
			$APP->get_widget('index')->get_selection->unselect_all;
		}
	}

	return 1;
}

sub close_window { close_program() }

sub close_program {
	if ($MAXIMIZED == 1) {
		$OPTIONS->{maximized} = 1;
	} else {
		$OPTIONS->{maximized} = 0;
		($OPTIONS->{window_x}, $OPTIONS->{window_y}) = $APP->get_widget('main_window')->get_size if ($FULLSCREEN == 0);
	}

	$OPTIONS->{pane_position} = $APP->get_widget('pane')->get_position;
	$OPTIONS->{vpane_position} = $APP->get_widget('vpane')->get_position;
	$OPTIONS->{history} = join('|', splice(@HISTORY, 0, 20));
	$OPTIONS->{bookmarks} = join('|', @BOOKMARKS);
	save_config();
	exit 0;
}

sub load_config {
	my $OPTIONS = {};
	if (open(RCFILE, $RCFILE)) {
		while (<RCFILE>) {
			chomp;
			my ($name, $value) = split(/\s*=\s*/, $_, 2);
			$OPTIONS->{lc($name)} = $value;
		}
		close(RCFILE);
	}
	return $OPTIONS;
}

sub save_config {
	if (!open(RCFILE, ">$RCFILE")) {
		printf(STDERR "Cannot open file '%s' for writing: %s\n", $RCFILE, $!);
		return undef;
	} else {
		foreach my $key (sort(keys(%{$OPTIONS}))) {
			printf(RCFILE "%s=%s\n", $key, $OPTIONS->{$key});
		}
		close(RCFILE);
		return 1;
	}
	return undef;
}

sub link_clicked {
	my (undef, $text) = @_;
	$text =~ s/\"$//g;
	$text =~ s/^\"//g;

	return undef if ($text eq '');

	my @marks = $viewer->get_marks;
	my $seen = 0;
	map { s/^[\"\']//g ; s/[\"\']$//g ; $seen++ if (lc($_) eq lc($text)) } @marks;

	if ($seen > 0) {
		# link referred to an anchor:
		for (my $i = 0 ; $i < scalar(@marks) ; $i++) {
			$marks[$i] =~ s/^[\"\']//g;
			$marks[$i] =~ s/[\"\']$//g;
			if (lc($marks[$i]) eq lc($text)) {
				$page_index->select($i);
				return 1;
			}
		}

	} elsif ($text =~ /\|\/?/) {
		# link referred to an anchor, but with some named text:
		my ($text, $section) = split(/\|\/?/, $text, 2);
		link_clicked(undef, $section);

	} elsif ($text =~ /^(\w+)\:\/\//) {
		# link referred to a URL:
		open_url($text);

	} elsif ($text =~ /^\// && ! -e $text) {
		# link referred to a non-existent file, remove the leading slash and try again:
		$text =~ s/^\///;
		link_clicked(undef, $text);

	} elsif ($text =~ /\// && ! -e $text) {
		# link referred to a poddoc/anchor anchor, split the text and try with the second part:
		my ($doc, $section) = split(/\//, $text, 2);
		set_location($doc);
		link_clicked(undef, $section);

	} else {
		# link referred to another pod document:
		set_location($text);
	}
	return 1;

}

sub set_status {
	my $str = shift;
	$APP->get_widget('status_bar')->push($APP->get_widget('status_bar')->get_context_id($str), $str);
	return 1;
}

sub set_location {
	my $locn = shift;
	if ($APP->get_widget('location')->entry->get_text ne '') {
		push(@BACK, $APP->get_widget('location')->entry->get_text);
	}
	@FORWARD = ();
	$APP->get_widget('location')->entry->set_text($locn);
	go();
}

### if it's a left-click, go back, if it's a right-click, run the popup (this
### works the same for the other two navigation buttons:
sub back_button_handler {
	if ($_[1]->button == 1) {
		go_back();
	} elsif ($_[1]->button == 3) {
		back_button_popup();
	}
	return 1;
}

### this is called by the function above, and by the 'popup_menu' signal on the
### button:
sub back_button_popup() {
	popup_menu(3, reverse(@BACK));
	return 1;
}

sub go_back {
	unshift(@FORWARD, $APP->get_widget('location')->entry->get_text);
	my $locn = pop(@BACK);
	$APP->get_widget('location')->entry->set_text($locn);
	go();
}

sub forward_button_handler {
	if ($_[1]->button == 1) {
		go_forward();
	} elsif ($_[1]->button == 3) {
		forward_button_popup();
	}
	return 1;
}

sub forward_button_popup() {
	popup_menu(3, @FORWARD);
	return 1;
}

sub go_forward {
	push(@BACK, $APP->get_widget('location')->entry->get_text);
	my $locn = shift(@FORWARD);
	$APP->get_widget('location')->entry->set_text($locn);
	go();
}

sub up_button_handler {
	if ($_[1]->button == 1) {
		go_up();
	} elsif ($_[1]->button == 3) {
		up_button_popup();
	}
	return 1;
}

sub up_button_popup() {
	my @items;
	my @parts;
	my $doc = $APP->get_widget('location')->entry->get_text;
	foreach my $part (split(/::/, $doc)) {
		push(@parts, $part);
		my $this_doc = join('::', @parts);
		push(@items, $this_doc) unless ($this_doc eq $doc);
	}
	popup_menu(3, @items);
	return 1;
}

sub go_up {
	my @parts = split(/::/, $APP->get_widget('location')->entry->get_text);
	pop(@parts);
	$APP->get_widget('location')->entry->set_text(join('::', @parts));
	push(@BACK, $CURRENT_DOCUMENT);
	@FORWARD = ();
	go();
}

sub user_set_location {
	return undef if ($APP->get_widget('location')->entry->get_text eq '');
	push(@BACK, $CURRENT_DOCUMENT) if ($CURRENT_DOCUMENT ne '' && $CURRENT_DOCUMENT ne $APP->get_widget('location')->entry->get_text);
	@FORWARD = ();
	go();
}

sub search {
	my $str = $APP->get_widget('search_entry')->get_text;

	$str =~ s/^\s*$//g;

	return undef if ($str eq '');

	set_status('Searching...');

	$APP->get_widget('main_window')->window->set_cursor($BUSY_CURSOR);
	$viewer->get_window('text')->set_cursor($BUSY_CURSOR);

	my $doc = $viewer->get_buffer->get_text(
		$viewer->get_buffer->get_start_iter,
		$viewer->get_buffer->get_end_iter,
		1
	);

	$str = quotemeta($str);
	$APP->get_widget('search_entry')->set_sensitive(0);

	$SEARCH_OFFSET = 0 if ($str ne $LAST_SEARCH_STR);
	$LAST_SEARCH_STR = $str;

	for ($SEARCH_OFFSET ; $SEARCH_OFFSET < length($doc) ; $SEARCH_OFFSET++) {
		Gtk2->main_iteration while (Gtk2->events_pending);
		if (substr($doc, $SEARCH_OFFSET) =~ /^$str/i) {
			my $iter = $viewer->get_buffer->get_iter_at_offset($SEARCH_OFFSET);
			$viewer->scroll_to_iter($iter, undef, 1, 0, 0);
			$APP->get_widget('search_entry')->set_sensitive(1);
			$APP->get_widget('search_entry')->grab_focus();
			$viewer->get_buffer->move_mark(
				$viewer->get_buffer->get_mark('insert'), 
				$viewer->get_buffer->get_iter_at_offset($SEARCH_OFFSET)
			);
			$viewer->get_buffer->move_mark(
				$viewer->get_buffer->get_mark('selection_bound'), 
				$viewer->get_buffer->get_iter_at_offset($SEARCH_OFFSET + length($str))
			);

			set_status('');
			$APP->get_widget('main_window')->window->set_cursor($NORMAL_CURSOR);
			$SEARCH_OFFSET += length($str);
			return 1;
		}
	}
	$APP->get_widget('search_entry')->set_sensitive(1);
	$APP->get_widget('search_entry')->grab_focus();

	set_status('');

	$SEARCH_OFFSET = 0;

	$APP->get_widget('main_window')->window->set_cursor($NORMAL_CURSOR);
	$viewer->get_window('text')->set_cursor($NORMAL_CURSOR);

	my $dialog = Gtk2::MessageDialog->new($APP->get_widget('main_window'), 'modal', 'info', 'ok', sprintf(gettext("The string '%s' was not found."), $str));
	$dialog->signal_connect('response', sub { $dialog->destroy });
	$dialog->show_all;

	return undef;
}

sub select_all {
	$viewer->get_buffer->move_mark(
		$viewer->get_buffer->get_mark('insert'), 
		$viewer->get_buffer->get_start_iter,
	);
	$viewer->get_buffer->move_mark(
		$viewer->get_buffer->get_mark('selection_bound'), 
		$viewer->get_buffer->get_end_iter,
	);
	return 1;
}

sub search_dialog {
	$APP->get_widget('search_dialog_entry')->set_text($APP->get_widget('search_entry')->get_text);
	$APP->get_widget('search_dialog')->show_all;
	return 1;
}

sub search_dialog_close {
	$APP->get_widget('search_dialog')->hide_all;
	return 1;
}

sub search_dialog_entry_activate() {
	search_dialog_response(undef, 'ok');
	return 1;
}

sub search_dialog_response {
	if ($_[1] eq 'ok') {
		$APP->get_widget('search_entry')->set_text($APP->get_widget('search_dialog_entry')->get_text);
		search();

	}

	$APP->get_widget('search_dialog')->hide_all;
	return 1;
}

sub open_url {
	my $url = shift;

	if (!-x $OPENER) {
		my $dialog = Gtk2::MessageDialog->new($APP->get_widget('main_window'), 'modal', 'info', 'ok', gettext("Cannot find the gnome-open program to launch this URL was not found."));
		$dialog->signal_connect('response', sub { $dialog->destroy });
		$dialog->show_all;
		return undef;

	} else {
		system("$OPENER \"$url\" &");
		return 1;

	}
}

### this looks through the system for perl pod documents that reference functions, modules and pod
### documents, and creates a hash of hashes that can be used to build a sitewide index:
sub generate_index {
	my %PATHS;

	# doing a reverse sort means that later versions of Perl are preferred over newer versions:
	foreach my $dir (reverse sort @INC) {
		if (-r "$dir/pod/perltoc.pod" && $PATHS{perltoc} eq '') {
			$PATHS{perltoc} = "$dir/pod/perltoc.pod";
		}

		if (-r "$dir/pod/perlfunc.pod" && $PATHS{perlfunc} eq '') {
			$PATHS{perlfunc} = "$dir/pod/perlfunc.pod";
		}

		if (-r "$dir/perllocal.pod" && $PATHS{perllocal} eq '') {
			$PATHS{perllocal} = "$dir/perllocal.pod";
		}
	}

	my $ITEMS = {};
	my $category;

	if (-r $PATHS{perltoc}) {
		if (!open(PERLTOC, $PATHS{perltoc})) {
			print STDERR "$PATHS{perltoc}: $!\n";

		} else {
			while (<PERLTOC>) {
				if (/BASIC DOCUMENTATION/) {
					$category = 'pods';

				} elsif (/PRAGMA DOCUMENTATION/) {
					$category = 'pragma';

				} elsif (/MODULE DOCUMENTATION/) {
					$category = 'modules';

				} elsif (/AUXILIARY DOCUMENTATION/) {
					$category = 'aux';

				} elsif (/^=head2/) {
					my (undef, $doc, undef) = split(' ', $_, 3);
					$doc =~ s/[^A-Za-z0-9\:\_]//g;
					$ITEMS->{$category}->{$doc}++;
				}
			}
			close(PERLTOC);
		}
	}

	$category = '';
	if (-r $PATHS{perlfunc}) {
		if (!open(PERLFUNC, $PATHS{perlfunc})) {
			print STDERR "$PATHS{perlfunc}: $!\n";

		} else {
			while (<PERLFUNC>) {
				if (/Alphabetical Listing of Perl Functions/) {
					$category = 'funcs';
				} elsif (/^=item/) {
					my (undef, $doc, undef) = split(' ', $_, 3);
					$doc =~ s/[^A-Za-z0-9\_\/\-]+//g;
					$ITEMS->{$category}->{$doc}++;
				}
			}
			close(PERLFUNC);
		}
	}

	if (-r $PATHS{perllocal}) {
		if (!open(PERLLOCAL, $PATHS{perllocal})) {
			print STDERR "$PATHS{perllocal}: $!\n";

		} else {
			while (<PERLLOCAL>) {
				if (/^=head2 .+? L<([A-Za-z0-9\:]+)\|/) {
					my $module = $1;
					$module =~ s/[^A-Za-z0-9\:\_]//g;
					$ITEMS->{modules}->{$1}++;
				}
			}
			close(PERLLOCAL);
		}
	}

	return $ITEMS;
}

sub section_reformat {
	my $str = shift;
	my @words = split(/[\s\t]+/, $str);
	my @return = '';
	foreach my $word (@words) {
		if ($word =~ /^[A-Z]+$/) {
			$word = ucfirst(lc($word));
		}
		push(@return, $word);
	}
	return join(' ', @return);
}

### this tracks the window's state:
sub window_changed_state {
	my $mask = $_[1]->changed_mask;
	if ("$mask" eq '[ withdrawn ]') {
		$MAXIMIZED  = 0;
		$FULLSCREEN = 0;
	} elsif ("$mask" eq '[ maximized ]') {
		if ($MAXIMIZED == 1) {
			$MAXIMIZED  = 0;
		} else {
			$MAXIMIZED  = 1;
		}
		$FULLSCREEN = 0;
	} elsif ("$mask" eq '[ fullscreen ]') {
		$MAXIMIZED  = 0;
		if ($FULLSCREEN == 1) {
			$FULLSCREEN  = 0;
		} else {
			$FULLSCREEN  = 1;
		}
	}
	return 1;
}

### this is called when the user clicks on an item in the site index:
my $index_changed_timeout;
sub index_changed {
	my ($path) = $APP->get_widget('index')->get_selection->get_selected_rows;
	if (defined($path)) {
		my $path = $path->to_string;
		my $iter = $APP->get_widget('index')->get_model->get_iter_from_string($path);
		my $value = $APP->get_widget('index')->get_model->get_value($iter, 1);
		my $seen = 0;
		foreach my $category (keys %categories) {
			$seen++ if ($categories{$category} eq $value);
		}
		if ($seen < 1) {
			# defer loading the page, in case the user is
			# arrowing around in the index.
			Glib::Source->remove($index_changed_timeout) if ($index_changed_timeout);
			$index_changed_timeout = Glib::Timeout->add(200, sub {
				$APP->get_widget('location')->entry->set_text($value);
				go();
				$index_changed_timeout = 0;
				0; # don't run again
			});
		}
	}
	return 1;
}

### returns an icon theme. When running in a GNOME session, the default is fine. if not,
### we have to load a custom one:
sub get_an_icon_theme {
	my $theme;
	if ($OPTIONS->{theme} ne '') {
		# user specified a particular theme in their .podbrowserrc:
		$theme = Gtk2::IconTheme->new;
		$theme->set_custom_theme($OPTIONS->{theme});
	} else {
		# get the default theme:
		$theme = Gtk2::IconTheme->get_default;
	}
	my @paths = (
		'/usr/share/icons',
		'/opt/share/icons',
		'/usr/local/share/icons',
		sprintf('%s/.icons',			$ENV{HOME}),
		sprintf('%s/.local/share/icons',	$ENV{HOME}),
		sprintf('%s/share/icons',		(-d $PREFIX ? $PREFIX : $ENV{PWD})),
		sprintf('%s/icons',			(-d $PREFIX ? $PREFIX : $ENV{PWD})),
		(-d $PREFIX ? $PREFIX : $ENV{PWD}),
	);
	map { $theme->append_search_path($_) } @paths;
	if ($theme->has_icon('gnome-mime-text') == 0) {
		# the first theme failed, try the 'gnome' theme:
		$theme = Gtk2::IconTheme->new;
		$theme->set_custom_theme('gnome');
		map { $theme->append_search_path($_) } @paths;
		if ($theme->has_icon('gnome-mime-text') == 0) {
			print STDERR "*** sorry, I tried my best but I still can't find a usable icon theme!\n";
			exit 256;
		}
	}
	return $theme;
}

### pop up the bookmarks editor:
sub edit_bookmarks_dialog {
	@{$bookmarks_list->{data}} = ();
	foreach my $bookmark (@BOOKMARKS) {
		push(@{$bookmarks_list->{data}}, [$PAGE_PBF, $bookmark]);
	}
	$APP->get_widget('bookmarks_dialog')->set_position('center');
	$APP->get_widget('bookmarks_dialog')->show_all;
	return 1;
}

### user clicked the "jump to" button on the dialog:
sub load_bookmark {
	my ($idx) = $bookmarks_list->get_selected_indices;
	return undef if (!defined($idx));
	my $bookmark = $BOOKMARKS[$idx];
	$APP->get_widget('bookmarks_dialog')->hide;
	link_clicked(undef, $bookmark);
	return 1;
}

### user clicked the "remove" button on the dialog:
sub remove_bookmark {
	my ($idx) = $bookmarks_list->get_selected_indices;
	return undef if (!defined($idx));
	my $bookmark = $BOOKMARKS[$idx];
	$APP->get_widget('bookmarks_menu')->get_submenu->remove($BOOKMARK_ITEMS->{$bookmark});
	splice(@{$bookmarks_list->{data}}, $idx, 1);
	splice(@BOOKMARKS, $idx, 1);
	return 1;
}

### just hide the dialog and return a true value so the dialog isn't destroyed:
sub edit_bookmarks_dialog_delete_event {
	$APP->get_widget('bookmarks_dialog')->hide;
	return 1;
}

### hide the dialog:
sub edit_bookmarks_dialog_response {
	$APP->get_widget('bookmarks_dialog')->hide;
	return 1;
}

### user clicked the "add bookmark" menu item:
sub add_bookmark {
	my $bookmark = $APP->get_widget('location')->entry->get_text;
	add_bookmark_item($bookmark);
	push(@BOOKMARKS, $bookmark);
	return 1;
}

sub new_window {
	system("$0 &");
	return 1;
}

### the next three functions are all used together, in various places: context
### menus for the navigation buttons, the bookmarks menu, and so on:

### append an item to the bookmarks menu. keep a reference in $BOOKMARK_ITEMS
### so that if the bookmark is deleted we can easily remove it from the menu:
sub add_bookmark_item {
	my $bookmark = shift;
	my $item = document_menu_item($bookmark);
	$item->show_all;
	$BOOKMARK_ITEMS->{$bookmark} = $item;
	$APP->get_widget('bookmarks_menu')->get_submenu->append($item);
	return 1;
}

### create a menu item for a document. they all have the same behaviour:
sub document_menu_item {
	my $document = shift;
	my $item = Gtk2::ImageMenuItem->new_with_mnemonic($document =~ /\// ? (split(/\//, $document))[-1] : $document);
	$item->set_image(Gtk2::Image->new_from_pixbuf($PAGE_PBF));
	$item->signal_connect('activate', sub { link_clicked(undef, $document) });
	$TIPS->set_tip($item, sprintf(gettext("Go to '%s'"), $document));
	return $item;
}

### create a popup menu for a list of documents:
sub popup_menu {
	my ($button, @docs) = @_;
	my $menu = Gtk2::Menu->new;
	foreach my $doc (@docs) {
		my $item = document_menu_item($doc);
		$item->show_all;
		$menu->append($item);
	}
	$menu->show_all;
	$menu->popup(undef, undef, undef, undef, $button, undef);
	return 1;
}

__END__

=pod

=head1 NAME

podbrowser - a Perl documentation browser for GNOME

PodBrowser is a more feature-complete version of podviewer, which comes with
Gtk2::PodViewer.

=head1 SYNTAX

B<podbrowser> [F<location>]

=head1 DESCRIPTION

PodBrowser is a documentation browser for Perl. You can view the documentation
for Perl's builtin functions, its "perldoc" pages, pragmatic modules and the
default and user-installed modules.

=head1 OPTIONS

F<location> If an argument is specified this argument is loaded as location.

=head1 AUTHOR

Gavin Brown E<lt>L<gavin.brown@uk.com>E<gt>. Original manpage by Florian Ragwitz E<lt>florian@mookooh.orgE<gt>.

=cut
