#!/usr/bin/perl
##############################################################################
#
#   File Name    - mtn-browse
#
#   Description  - Perl GUI utility for browsing a Monotone database without a
#                  workspace.
#
#   Author       - A.E.Cooper.
#
#   Legal Stuff  - Copyright (c) 2007 Anthony Edward Cooper
#                  <aecooper@coosoft.plus.com>.
#
#                  This program is free software; you can redistribute it
#                  and/or modify it under the terms of the GNU General Public
#                  License as published by the Free Software Foundation;
#                  either version 3 of the License, or (at your option) any
#                  later version.
#
#                  This program is distributed in the hope that it will be
#                  useful, but WITHOUT ANY WARRANTY; without even the implied
#                  warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
#                  PURPOSE. See the GNU General Public License for more
#                  details.
#
#                  You should have received a copy of the GNU General Public
#                  License along with this software; if not, write to the Free
#                  Software Foundation, Inc., 59 Temple Place - Suite 330,
#                  Boston, MA 02111-1307 USA.
#
##############################################################################
#
##############################################################################
#
#   Global Data For This Module
#
##############################################################################



# ***** DIRECTIVES *****

require 5.008005;

BEGIN
{
    use constant APPLICATION_NAME        => "mtn-browse";
    use constant APPLICATION_VERSION     => "1.10";
    use constant FILE_COMPARE_CMD        => "@INST:FILE_COMPARE_CMD@";
    use constant HTML_VIEWER_CMD         => "@INST:HTML_VIEWER_CMD@";
    use constant GRAPHVIZ_LAYOUT_PROGRAM => "@INST:GRAPHVIZ_LAYOUT_PROGRAM@";
    use constant LIB_DIR                 => "@INST:LIB_DIR@";
    use constant MIME_GLOB_FILE          => "@INST:GLOBS_FILE@";
    use constant PREFIX_DIR              => "@INST:PREFIX_DIR@";
}
use locale;
use strict;
use warnings;

# ***** REQUIRED PACKAGES *****

# Standard Perl and CPAN modules.

use Cwd qw(getcwd);
use Data::Dumper;
use Digest::MD5 qw(md5);
use Digest::SHA1 qw(sha1);
use Encode;
use File::Basename;
use File::Copy qw();
use File::Spec;
use File::Temp qw(tempdir);
use Glib qw(FALSE TRUE);
use Gnome2;
use Gnome2::Canvas;
use Gnome2::VFS -init;
use Gtk2 -init;
use Gtk2::Gdk::Keysyms;
use Gtk2::GladeXML;
use Gtk2::Helper;
use Gtk2::Pango;
use Gtk2::SourceView;
use IO::Dir;
use IO::File;
use IPC::Open3;
use List::Util qw(max);
use Locale::TextDomain (APPLICATION_NAME,
                        File::Spec->catfile(PREFIX_DIR, "share", "locale"));
use POSIX qw(:errno_h :locale_h :sys_wait_h floor strftime);
use Scalar::Util qw(blessed refaddr);
use Symbol qw(gensym);
use Text::Tabs;
use Time::Local;

# Add custom module directories to the module load search path.

use lib File::Spec->catfile(LIB_DIR, "perl");

# Monotone AutomateStdio module (via derived caching class).

use CachingAutomateStdio qw(:capabilities :severities);

# Modules specific to this application.

use Globals qw(:constants :variables);
use LocaleEnableUtf8;
use AdvancedFind;
use Annotate;
use ChangeLog;
use ComboAutoCompletion;
use Common;
use Completion;
use DateRange;
use FindFiles;
use FindTextAndGoToLine;
use History;
use HistoryGraph;
use ManageServerBookmarks;
use ManageTagWeightings;
use MultipleRevisions;
use Preferences;
use WindowManager;

# ***** GLOBAL DATA DECLARATIONS *****

# Constants for the columns within the manifest liststore widget.

use constant MLS_COLUMN_TYPES          => ("Glib::String",
                                           "Glib::String",
                                           "Glib::String",
                                           "Glib::String",
                                           "Glib::Scalar");
use constant MLS_ICON_COLUMN           => 0;
use constant MLS_NAME_COLUMN           => 1;
use constant MLS_DATE_COLUMN           => 2;
use constant MLS_AUTHOR_COLUMN         => 3;
use constant MLS_MANIFEST_ENTRY_COLUMN => 4;

# Constant for the maximum size of the open recent menu.

use constant OPEN_RECENT_MENU_MAX_SIZE => 10;

# Constant for user interface directory.

use constant UI_DIR => File::Spec->catfile(PREFIX_DIR,
                                           "share",
                                           APPLICATION_NAME,
                                           "glade");

# The type of window that is going to be managed by this module.

my $window_type = "main_window";

# The large logo used for the about dialog window.

my $large_logo;

# ***** FUNCTIONAL PROTOTYPES *****

# Private routines.

sub about_activate_cb($$);
sub advanced_find_button_clicked_cb($$);
sub annotate_button_clicked_cb($$);
sub close_toolbutton_clicked_cb($$);
sub compare_arbitrary_revisions_activate_cb($$);
sub compare_workspace_activate_cb($$);
sub connect_to_server_activate_cb($$);
sub context_help_activate_cb($$);
sub create_browser_widgets();
sub determine_mime_type($$$$);
sub directory_up_button_clicked_cb($$);
sub display_file($$);
sub file_change_history_button_clicked_cb($$);
sub find_files_button_clicked_cb($$);
sub get_browser_window(;$$$$$);
sub goto_text_line_button_clicked_cb($$);
sub handle_advanced_find($);
sub help_on_window_activate_cb($$);
sub help_toolbutton_clicked_cb($$);
sub history_graph_button_clicked_cb($$);
sub home_page_activate_cb($$);
sub main_window_delete_event_cb($$$);
sub manage_server_bookmarks_activate_cb($$);
sub manifest_browser_treeselection_changed_cb($$);
sub manifest_browser_treeview_row_activated_cb($$$$);
sub mtn_db_locked_handler($$);
sub mtn_error_handler($$);
sub new_blank_activate_cb($$);
sub new_toolbutton_clicked_cb($$);
sub open_recent_activate_cb($$);
sub open_toolbutton_clicked_cb($$);
sub preferences_toolbutton_clicked_cb($$);
sub quit_activate_cb($$);
sub quit_from_gtk2();
sub reload_toolbutton_clicked_cb($$);
sub remember_sizes_activate_cb($$);
sub reset_sizes_activate_cb($$);
sub revision_change_history_button_clicked_cb($$);
sub revision_change_log_button_clicked_cb($$);
sub save_as_button_clicked_cb($$);
sub search_text_button_clicked_cb($$);
sub setup_mtn_object($$);
sub setup_sigchld_handler($);
sub show_line_numbers_togglebutton_toggled_cb($$);
sub sigchld_handler();
sub update_browser_state($$);
sub update_open_recent_menu($);
sub update_server_bookmarks_menu($);
sub view_button_clicked_cb($$);
#
##############################################################################
#
#   Routine      - Main Body Of Code
#
#   Description  - This is the main body of code for the mtn-browse script.
#
#   Data         - @_           : The command line arguments.
#                  Return Value : Unix exit code.
#
##############################################################################



{

    my ($branch,
        $browser,
        $icon_factory,
        $icon_set,
        $mtn,
        $revision_id);

    # Initialise stuff.

    Gnome2::Program->init
        (APPLICATION_NAME,
         APPLICATION_VERSION,
         undef,
         app_datadir => File::Spec->catfile(PREFIX_DIR, "share"));
    setup_sigchld_handler(\&sigchld_handler);
    $glade_file = File::Spec->catfile(UI_DIR, "mtn-browse.glade");
    $tooltips = Gtk2::Tooltips->new();
    $line_image = Gtk2::Gdk::Pixbuf->
        new_from_file(File::Spec->catfile(UI_DIR, "line.png"));
    $icon_factory = Gtk2::IconFactory->new();
    Gtk2::Stock->add({stock_id => "mtnb-tick",
                      label    => __("Tick")});
    $icon_set = Gtk2::IconSet->new_from_pixbuf
        (Gtk2::Gdk::Pixbuf->new_from_file(File::Spec->catfile
                                          (UI_DIR, "tick.png")));
    $icon_factory->add("mtnb-tick", $icon_set);
    Gtk2::Stock->add({stock_id => "mtnb-weighting",
                      label    => __("Weighting")});
    $icon_set = Gtk2::IconSet->new_from_pixbuf
        (Gtk2::Gdk::Pixbuf->new_from_file(File::Spec->catfile
                                          (UI_DIR, "weighting.png")));
    $icon_factory->add("mtnb-weighting", $icon_set);
    $icon_factory->add_default();
    $icon_factory = $icon_set = undef;

    # Set up the default database locked and I/O wait handlers for the Monotone
    # class.

    CachingAutomateStdio->register_db_locked_handler(\&mtn_db_locked_handler);
    CachingAutomateStdio->register_io_wait_handler
        (sub {
             $pulse_widget->pulse() if (defined($pulse_widget));
             WindowManager->instance()->update_gui();
         },
         0.5);

    # Load in user preferences.

    exit(1) unless (defined($user_preferences = load_preferences()));
    $mime_match_table =
        build_mime_match_table($user_preferences->{mime_table});
    $mono_font = Gtk2::Pango::FontDescription->
        from_string($user_preferences->{fixed_font});

    # Create the temporary working directory.

    eval
    {
        $tmp_dir = tempdir(APPLICATION_NAME . "_XXXXXXXXXX",
                           TMPDIR => 1,
                           CLEANUP => 1);
    };
    if ($@)
    {
        my $dialog = Gtk2::MessageDialog->new
            (undef,
             ["modal"],
             "error",
             "close",
             __x("{error_message}\nThis is fatal, I am going to exit.",
                 error_message => $@));
        busy_dialog_run($dialog);
        $dialog->destroy();
        exit(1);
    }

    # Open a Monotone database. First attempt to open any database given on the
    # command line. If no database was given then try the current workspace's
    # database. If that doesn't work or the database listed in the user's
    # preferences file is to be used anyway then attempt to open that database
    # instead.

    if (scalar(@ARGV) > 0)
    {
        if (scalar(@ARGV) > 1)
        {
            my $dialog = Gtk2::MessageDialog->new
                (undef,
                 ["modal"],
                 "error",
                 "close",
                 __("Unexpected additional arguments were\n"
                    . "given on the command line.\n"
                    . "Usage: mtn-browse [Gnome/Gtk Options...]\n"
                    . "[Monotone Database]"));
            busy_dialog_run($dialog);
            $dialog->destroy();
            exit(1);
        }
        else
        {
            check_and_open_database(undef, $ARGV[0], \$mtn);
        }
    }
    else
    {
        if ($user_preferences->{workspace}->{takes_precedence})
        {

            # Attempt to open the current workspace's database. If this works
            # then get the workspace's details and then re-open the database
            # but this time explicitly specifying the database itself. When
            # given a database name, Monotone::AutomateStdio will make sure any
            # current workspace will not `interfere' the with the mtn
            # subprocess. One could use the "--no-workspace" option but this is
            # only supported on later versions.

            eval
            {
                my ($db,
                    $has_workspace);
                $mtn = check_and_create_mtn_object(undef, undef, 1);
                $mtn->get_option(\$db, "database");
                if ($user_preferences->{workspace}->{auto_select})
                {
                    $mtn->get_option(\$branch, "branch");
                    $mtn->get_base_revision_id(\$revision_id);

                    # Unset the branch name if there is no revision associated
                    # with it (this can happen if we are in a new workspace on
                    # a new branch that doesn't exist in the database yet).

                    $branch = undef if (! defined($revision_id));
                }
                $has_workspace = 1 if (defined($mtn->get_ws_path()));
                $mtn = undef;
                $mtn = check_and_create_mtn_object(undef, $db);
                $mtn->has_workspace($has_workspace);
            };

        }
        if (! defined($mtn) && $user_preferences->{default_mtn_db} ne "")
        {

            # Attempt to open the database specified in the user's preferences.

            check_and_open_database(undef,
                                    $user_preferences->{default_mtn_db},
                                    \$mtn);

        }
    }

    # Set up the error handlers for the Monotone class.

    CachingAutomateStdio->register_error_handler(MTN_SEVERITY_ALL,
                                                 \&mtn_error_handler);

    # Setup the character encodings and Monotone authentication keys lists,
    # these both need to be set up before creating any windows.

    @encodings = sort(Encode->encodings(":all"));
    if (defined($mtn))
    {
        my @list;
        $mtn->keys(\@list);
        foreach my $key (@list)
        {
            my $keystore;
            foreach my $location (@{$key->{private_location}})
            {
                if ($location eq "keystore")
                {
                    $keystore = 1;
                    last;
                }
            }
            if ($keystore)
            {
                my $name;
                if (exists($key->{name}))
                {
                    $name = $key->{name};
                }
                else
                {
                    $name = $key->{local_name};
                }
                eval
                {
                    $name = decode_utf8($name, Encode::FB_CROAK);
                };
                push(@keys, $name);
            }
        }
    }
    else
    {
        my $buffer;
        if (run_command(\$buffer,
                        undef,
                        undef,
                        undef,
                        undef,
                        undef,
                        "mtn",
                        "automate",
                        "keys"))
        {
            my $name;
            my @lines = split(/\n/, $buffer);
            foreach my $line (@lines)
            {
                if ($line =~ m/^\s+(local_)?name\s+\"(.+)\"$/)
                {
                    $name = $2;
                }
                elsif ($line =~ m/^\s*private_location\s+\"keystore\"$/)
                {
                    eval
                    {
                        $name = decode_utf8($name, Encode::FB_CROAK);
                    };
                    push(@keys, $name);
                }
            }
        }
    }

    # Create the browser window and display it. Please note that updating the
    # browser to reflect the current database or workspace is done in an idle
    # handler so that control can be handed over to Gtk2 before updating the
    # display.

    $browser = get_browser_window();
    if (defined($mtn))
    {
        setup_mtn_object($mtn, $browser->{window});
        $browser->{mtn} = $mtn;
        $browser->{connect_to_server_menuitem}->set_sensitive(FALSE)
            if (! $user_preferences->{remote_connections});
        Glib::Idle->add
            (sub {
                 my $browser = $_[0];

                 return if ($browser->{in_cb});
                 local $browser->{in_cb} = 1;

                 if (defined($branch))
                 {
                     $browser->{branch_combo_details}->{preset} = 1;
                     $browser->{branch_combo_details}->{value} = $branch;
                     $browser->{revision_combo_details}->{preset} = 1;
                     $browser->{revision_combo_details}->{value} =
                         $revision_id;
                     $browser->{tagged_checkbutton}->set_active(FALSE);
                 }
                 &{$browser->{update_handler}}($browser, DATABASE_CHANGED);

                 return FALSE;
             },
             $browser);
    }
    $mtn = undef;

    # Setup the callback for the <F1> accelerator key for all windows and a
    # no-help fall-back callback.

    WindowManager->instance()->help_connect
        (undef,
         undef,
         sub { display_help(); },
         sub {
             my ($window, $instance) = @_;
             my $dialog = Gtk2::MessageDialog->new
                 (defined($instance) ? $instance->{window} : undef,
                  ["modal"],
                  "info",
                  "close",
                  __("Sorry no help is available\nfor that item."));
             busy_dialog_run($dialog);
             $dialog->destroy();
         });

    # Hand control over to Gtk2.

    Gtk2->main();

    # Cleanup.

    WindowManager->instance()->cleanup();
    Gnome2::VFS->shutdown();
    $SIG{CHLD} = "IGNORE";

    exit(0);

}
#
##############################################################################
#
#   Routine      - new_blank_activate_cb
#
#   Description  - Callback routine called when the user clicks on the new
#                  blank menu item in a main browser window.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub new_blank_activate_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    # Simply get a new/unused browser window and display it.

    get_browser_window();

}
#
##############################################################################
#
#   Routine      - open_recent_activate_cb
#
#   Description  - Callback routine called when the user selects a open recent
#                  menu option.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $details : A record containing the browser instance that is
#                             associated with this widget and the name of the
#                             database that is to be opened.
#
##############################################################################



sub open_recent_activate_cb($$)
{

    my ($widget, $details) = @_;

    return if ($details->{browser}->{in_cb});
    local $details->{browser}->{in_cb} = 1;

    my $mtn;
    my $browser = $details->{browser};

    if (check_and_open_database($browser->{window},
                                $details->{database},
                                \$mtn))
    {
        setup_mtn_object($mtn, $browser->{window});
        $browser->{mtn} = $mtn;
        $browser->{appbar}->clear_stack();
        &{$browser->{update_handler}}($browser, DATABASE_CHANGED);
    }

}
#
##############################################################################
#
#   Routine      - connect_to_server_activate_cb
#
#   Description  - Callback routine called when the user selects a server
#                  bookmark menu option.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $details : A record containing the browser instance that is
#                             associated with this widget and the name of the
#                             server that is to be connected to.
#
##############################################################################



sub connect_to_server_activate_cb($$)
{

    my ($widget, $details) = @_;

    return if ($details->{browser}->{in_cb});
    local $details->{browser}->{in_cb} = 1;

    my ($exception,
        $host,
        $mtn);
    my $browser = $details->{browser};
    my $wm = WindowManager->instance();

    $wm->make_busy($browser, 1);
    $browser->{appbar}->push($browser->{appbar}->get_status()->get_text());
    $wm->update_gui();

    # Try connecting to the specified server.

    $host = $details->{server};
    $host =~ s/:\d+$//g;
    CachingAutomateStdio->register_error_handler
        (MTN_SEVERITY_ALL,
         sub {
             my ($severity, $message) = @_;
             my $dialog;
             $message =~ s/mtn: warning: //g;
             $message =~ s/mtn: error: //g;
             $message =~ s/mtn: //g;
             $message =~ s/^Corrupt\/missing mtn [^\n]+\n//g;
             $message =~ s/ at .+ line \d+$//g;
             $message =~ s/\s+$//g;
             $message =~ s/\n/ /g;
             $message .= "." unless ($message =~ m/.+\.$/);
             $dialog = Gtk2::MessageDialog->new_with_markup
                 ($browser->{window},
                  ["modal"],
                  "warning",
                  "close",
                  __x("There is a problem connecting to the server. The "
                          . "details are:\n<b><i>{error_message}</i></b>",
                      error_message => Glib::Markup::escape_text($message)));
             busy_dialog_run($dialog);
             $dialog->destroy();
             die("Bad open");
         });
    $browser->{appbar}->set_status(__x("Connecting to server `{server}'",
                                       server => $host));
    eval
    {
        if (defined($mtn_key))
        {
            $mtn = CachingAutomateStdio->new_from_service
                ($details->{server}, ["--key" => $mtn_key]);
        }
        else
        {
            $mtn = CachingAutomateStdio->new_from_service($details->{server});
        }

    };
    $exception = $@;
    CachingAutomateStdio->register_error_handler(MTN_SEVERITY_ALL,
                                                 \&mtn_error_handler);
    if (! $exception)
    {

        # Seems to be ok so set up the browser.

        setup_mtn_object($mtn, $browser->{window});
        $browser->{mtn} = $mtn;
        $browser->{appbar}->pop();
        $browser->{appbar}->clear_stack();
        &{$browser->{update_handler}}($browser, DATABASE_CHANGED);

    }
    else
    {
        $browser->{appbar}->pop();
    }

    $wm->make_busy($browser, 0);

}
#
##############################################################################
#
#   Routine      - manage_server_bookmarks_activate_cb
#
#   Description  - Callback routine called when the user selects the manage
#                  server bookmarks menu option.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub manage_server_bookmarks_activate_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    if (manage_server_bookmarks($browser->{window},
                                $user_preferences->{server_bookmarks}))
    {

        save_preferences($user_preferences, $browser->{window});

        # We now need to update all server bookmark menus.

        WindowManager->instance()->cond_find
            ($window_type,
             sub {
                 my ($browser, $type) = @_;
                 update_server_bookmarks_menu($browser);
                 return;
             });

    }

}
#
##############################################################################
#
#   Routine      - quit_activate_cb
#
#   Description  - Callback routine called when the user selects the quit menu
#                  option.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub quit_activate_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    quit_from_gtk2();

}
#
##############################################################################
#
#   Routine      - remember_sizes_activate_cb
#
#   Description  - Callback routine called when the user selects the remember
#                  sizes menu option.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub remember_sizes_activate_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    my %mapped_windows;

    # Go through all window instances and save their current window sizes.
    # Mapped windows take precedence over unmapped ones.

    WindowManager->instance()->cond_find
        (undef,
         sub {
             my ($instance, $type) = @_;
             my ($height,
                 $width);
             if ($instance->{window}->mapped()
                 || ! exists($mapped_windows{$type}))
             {
                 ($width, $height) = $instance->{window}->get_size();
                 $user_preferences->{window_sizes}->{$type} =
                     {width  => $width,
                      height => $height};
                 $mapped_windows{$type} = undef
                     if ($instance->{window}->mapped());
             }
             return;
         });

    # Save the user's preferences.

    if (save_preferences($user_preferences, $browser->{window}))
    {
        $browser->{appbar}->set_status(__("Saved current window sizes"));
    }

}
#
##############################################################################
#
#   Routine      - reset_sizes_activate_cb
#
#   Description  - Callback routine called when the user selects the reset
#                  sizes menu option.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub reset_sizes_activate_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    # Simply blank out all the stored window sizes.

    $user_preferences->{window_sizes} = {};

    # Save the user's preferences.

    if (save_preferences($user_preferences, $browser->{window}))
    {
        $browser->{appbar}->set_status(__("Reset saved window sizes"));
    }

}
#
##############################################################################
#
#   Routine      - compare_arbitrary_revisions_activate_cb
#
#   Description  - Callback routine called when the user selects the compare
#                  arbitrary revisions menu option.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub compare_arbitrary_revisions_activate_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    display_arbitrary_revision_comparison($browser->{mtn});

}
#
##############################################################################
#
#   Routine      - compare_workspace_activate_cb
#
#   Description  - Callback routine called when the user selects the compare
#                  arbitrary revisions menu option.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub compare_workspace_activate_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    my $mtn;

    # Create a new mtn subprocess running in the workspace.

    if (defined($mtn = check_and_create_mtn_object($browser->{window}, undef)))
    {

        my $revision_id;

        # Get the revision that the workspace is based on and then simply do a
        # revision comparison.

        $mtn->get_base_revision_id(\$revision_id);
        display_revision_comparison($mtn, $revision_id, undef);

    }

}
#
##############################################################################
#
#   Routine      - help_on_window_activate_cb
#
#   Description  - Callback routine called when the user selects the help on
#                  window menu option.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub help_on_window_activate_cb($$)
{

    my ($widget, $browser) = @_;

    # No need to protect against recursion here as this simply activates a help
    # callback which deals with it.

    WindowManager->instance()->display_window_help($browser);

}
#
##############################################################################
#
#   Routine      - context_help_activate_cb
#
#   Description  - Callback routine called when the user selects the context
#                  help menu option.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub context_help_activate_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    WindowManager->instance()->activate_context_sensitive_help(1);

}
#
##############################################################################
#
#   Routine      - home_page_activate_cb
#
#   Description  - Callback routine called when the user selects the home page
#                  menu option.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub home_page_activate_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    eval
    {
        display_html("http://www.coosoft.plus.com/software.html");
    };
    if ($@)
    {
        my $dialog = Gtk2::MessageDialog->new_with_markup
            ($browser->{window},
             ["modal"],
             "warning",
             "close",
             __x("Gnome cannot display Monotone Browser's\n"
                     . "home page. Gnome gave:\n"
                 . "<b><i>{gnome_error_message}</i></b>",
                 gnome_error_message => $@));
        busy_dialog_run($dialog);
        $dialog->destroy();
    }

}
#
##############################################################################
#
#   Routine      - about_activate_cb
#
#   Description  - Callback routine called when the user selects the about
#                  menu option.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub about_activate_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    $large_logo = Gtk2::Gdk::Pixbuf->new_from_file
        (File::Spec->catfile(UI_DIR, "mtn-browse-large.png"))
        if (! defined($large_logo));
    Gnome2::About->new
        (APPLICATION_NAME,
         APPLICATION_VERSION,
         __("Copyright \xa9 2007-2012 Anthony Cooper"),
         __("A graphical front-end browser for Monotone VCS databases"),
         ["Anthony Cooper <support\@coosoft.plus.com>"],
         __("Anthony Cooper <support\@coosoft.plus.com>"),
         __("Thomas Keller <me\@thomaskeller.biz> (German translation)"),
         $large_logo)->run();

}
#
##############################################################################
#
#   Routine      - new_toolbutton_clicked_cb
#
#   Description  - Callback routine called when the user clicks on the new
#                  toolbutton in a main browser window.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub new_toolbutton_clicked_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    my @revision_ids;

    # Simply get a new/unused browser window and display it.

    if (! defined($browser->{mtn}))
    {
        get_browser_window();
    }
    elsif (! $browser->{branch_combo_details}->{complete})
    {
        get_browser_window($browser->{mtn});
    }
    elsif (! $browser->{revision_combo_details}->{complete})
    {
        get_browser_window($browser->{mtn},
                           $browser->{branch_combo_details}->{value});
    }
    else
    {
        get_revision_ids($browser, \@revision_ids);
        get_browser_window($browser->{mtn},
                           $browser->{branch_combo_details}->{value},
                           $revision_ids[0]);
    }

}
#
##############################################################################
#
#   Routine      - open_toolbutton_clicked_cb
#
#   Description  - Callback routine called when the user clicks on the open
#                  toolbutton in a main browser window.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub open_toolbutton_clicked_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    my ($file_name,
        $mtn);

    if (open_database($browser->{window}, \$mtn, \$file_name))
    {
        setup_mtn_object($mtn, $browser->{window});
        $browser->{mtn} = $mtn;
        $browser->{appbar}->clear_stack();
        &{$browser->{update_handler}}($browser, DATABASE_CHANGED);
        update_open_recent_menu($browser)
            if (handle_history("open_recent_menu",
                               $file_name,
                               OPEN_RECENT_MENU_MAX_SIZE));
    }

}
#
##############################################################################
#
#   Routine      - close_toolbutton_clicked_cb
#
#   Description  - Callback routine called when the user clicks on the close
#                  toolbutton in a main browser window.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub close_toolbutton_clicked_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    # Simply reset the browser's Monotone instance and update its display.

    $browser->{mtn} = undef;
    $browser->{appbar}->clear_stack();
    &{$browser->{update_handler}}($browser, DATABASE_CHANGED);

}
#
##############################################################################
#
#   Routine      - reload_toolbutton_clicked_cb
#
#   Description  - Callback routine called when the user clicks on the
#                  reload toolbutton in a main browser window.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub reload_toolbutton_clicked_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    # Simply refresh the entire browser.

    if ($browser->{branch_combo_details}->{complete})
    {
        $browser->{branch_combo_details}->{preset} = 1;
        if ($browser->{revision_combo_details}->{complete})
        {
            $browser->{revision_combo_details}->{preset} = 1;
            if ($browser->{directory_combo_details}->{complete})
            {
                $browser->{directory_combo_details}->{preset} = 1;
                if (exists($browser->{file_being_viewed}->{manifest_entry}))
                {
                    $browser->{file_being_viewed_preset_value} =
                        $browser->{file_being_viewed}->{short_name};
                }
            }
        }
    }
    $browser->{mtn}->closedown();
    $browser->{appbar}->clear_stack();
    &{$browser->{update_handler}}($browser, ALL_CHANGED);

}
#
##############################################################################
#
#   Routine      - preferences_toolbutton_clicked_cb
#
#   Description  - Callback routine called when the user clicks on the
#                  preferences toolbutton in a main browser window.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub preferences_toolbutton_clicked_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    if (preferences($browser))
    {
        $user_preferences = load_preferences();
        $mime_match_table =
            build_mime_match_table($user_preferences->{mime_table});
    }

}
#
##############################################################################
#
#   Routine      - help_toolbutton_clicked_cb
#
#   Description  - Callback routine called when the user clicks on the
#                  help toolbutton in a main browser window.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub help_toolbutton_clicked_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    display_help();

}
#
##############################################################################
#
#   Routine      - advanced_find_button_clicked_cb
#
#   Description  - Callback routine called when the user clicks on the
#                  advanced find button in a main browser window.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub advanced_find_button_clicked_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    handle_advanced_find($browser);

}
#
##############################################################################
#
#   Routine      - history_graph_button_clicked_cb
#
#   Description  - Callback routine called when the user clicks on the history
#                  graph button in a main browser window.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub history_graph_button_clicked_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    my (@branches,
        $from_date,
        $revision_id,
        @revision_ids,
        $to_date);

    # No point in proceeding if dot isn't available.

    return unless (program_valid(GRAPHVIZ_LAYOUT_PROGRAM, $browser->{window}));

    # Ok work out what revision we are going to graph around and the time
    # window around that revision.

    # Get the details that the user has selected in the browser window.

    push(@branches, $browser->{branch_combo_details}->{value})
        if ($browser->{branch_combo_details}->{complete});
    get_revision_ids($browser, \@revision_ids)
        if ($browser->{revision_combo_details}->{complete});

    # If we don't have a revision but we do have a branch then use it's head
    # revision, otherwise the user has explicitly selected a revision so make a
    # note of this.

    if (scalar(@revision_ids) == 0)
    {
        $browser->{mtn}->heads(\@revision_ids, $branches[0])
            if (scalar(@branches) > 0);
    }
    else
    {
        $revision_id = $revision_ids[0];
    }

    # Ok if we now have a revision then set the from and to dates to two months
    # either side of the revision's date.

    if (scalar(@revision_ids) > 0)
    {
        my (@certs,
            @from_gm_time,
            @to_gm_time);
        $browser->{mtn}->certs(\@certs, $revision_ids[0]);
        foreach my $cert (@certs)
        {
            if ($cert->{name} eq "date")
            {
                $from_date = $cert->{value};
                last;
            }
        }
        @from_gm_time = @to_gm_time =
            gmtime(mtn_time_string_to_time($from_date));
        adjust_time(\@from_gm_time, -2, DURATION_MONTHS);
        $from_date = strftime(MTN_TIME_STRING, @from_gm_time);
        adjust_time(\@to_gm_time, 2, DURATION_MONTHS);
        $to_date = strftime(MTN_TIME_STRING, @to_gm_time);
    }

    # Now graph it. If the user explicitly specified the revision then we give
    # it's date as the to-date along with the revision id so that it can be
    # pre-selected in the graph.

    display_history_graph($browser->{mtn},
                          \@branches,
                          $from_date,
                          $to_date,
                          $revision_id);

}
#
##############################################################################
#
#   Routine      - revision_change_history_button_clicked_cb
#
#   Description  - Callback routine called when the user clicks on the
#                  revision change history button in a main browser window.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub revision_change_history_button_clicked_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    my (@revision_ids,
        $tag);

    get_revision_ids($browser, \@revision_ids, \$tag);
    display_revision_change_history($browser->{mtn}, $tag, $revision_ids[0]);

}
#
##############################################################################
#
#   Routine      - revision_change_log_button_clicked_cb
#
#   Description  - Callback routine called when the user clicks on the
#                  revision change log button in a main browser window.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub revision_change_log_button_clicked_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    my (@revision_ids,
        $tag);

    # Get the currently selected revision id and then display its change log.

    get_revision_ids($browser, \@revision_ids, \$tag);
    display_change_log($browser->{mtn}, $revision_ids[0], "", $tag);

}
#
##############################################################################
#
#   Routine      - directory_up_button_clicked_cb
#
#   Description  - Callback routine called when the user clicks on the up
#                  directory button in a main browser window.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub directory_up_button_clicked_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    my $value;

    # Simply go up one directory level in the manifest if we aren't already at
    # the top.

    $value = $browser->{directory_combo_details}->{value};
    if ($value ne "")
    {
        $value = dirname($value);
        $value = "" if ($value eq ".");
        $browser->{directory_combo_details}->{filter} = $value;
        $browser->{directory_combo_details}->{update} = 1;
        $browser->{directory_combo_details}->{value} = $value;
        $browser->{directory_combo_details}->{complete} = 1;
        $browser->{directory_entry}->set_text($value);
        $browser->{directory_entry}->set_position(-1);
        $browser->{appbar}->clear_stack();
        &{$browser->{update_handler}}($browser, DIRECTORY_CHANGED);
    }

}
#
##############################################################################
#
#   Routine      - find_files_button_clicked_cb
#
#   Description  - Callback routine called when the user clicks on the find
#                  files button in a main browser window.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub find_files_button_clicked_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    my (@revision_ids,
        $tag);

    get_revision_ids($browser, \@revision_ids, \$tag);
    display_find_files($browser->{mtn},
                       $tag,
                       $revision_ids[0],
                       $browser->{manifest},
                       $browser->{directory_combo_details}->{value});

}
#
##############################################################################
#
#   Routine      - show_line_numbers_togglebutton_toggled_cb
#
#   Description  - Callback routine called when the user toggles the show line
#                  numbers button in a main browser window.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub show_line_numbers_togglebutton_toggled_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    $browser->{file_view_sv}->set_show_line_numbers($widget->get_active());

}
#
##############################################################################
#
#   Routine      - search_text_button_clicked_cb
#
#   Description  - Callback routine called when the user clicks on the search
#                  text button in a main browser window.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub search_text_button_clicked_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    find_text($browser->{window}, $browser->{file_view_sv});

}
#
##############################################################################
#
#   Routine      - goto_text_line_button_clicked_cb
#
#   Description  - Callback routine called when the user clicks on the go to
#                  text line button in a main browser window.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub goto_text_line_button_clicked_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    goto_line($browser->{window}, $browser->{file_view_sv});

}
#
##############################################################################
#
#   Routine      - save_as_button_clicked_cb
#
#   Description  - Callback routine called when the user clicks on the save as
#                  button in a main browser window.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub save_as_button_clicked_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    my $data;

    $browser->{mtn}->get_file(\$data,
                              $browser->{file_being_viewed}->{manifest_entry}->
                                  {file_id});
    save_as_file($browser->{window},
                 $browser->{file_being_viewed}->{short_name},
                 \$data);

}
#
##############################################################################
#
#   Routine      - view_button_clicked_cb
#
#   Description  - Callback routine called when the user clicks on the view
#                  button in a main browser window.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub view_button_clicked_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    my ($app,
        $data,
        $fh,
        $file_name,
        $helper,
        $mime_details,
        $mime_obj,
        $mime_type);

    # Generate temporary disk file name.

    if (! defined($file_name =
                  generate_tmp_path($browser->{file_being_viewed}->
                                    {short_name})))
    {
        my $dialog = Gtk2::MessageDialog->new
            ($browser->{window},
             ["modal"],
             "warning",
             "close",
             __x("Cannot generate temporary file name:\n{error_message}.",
                 error_message => $!));
        busy_dialog_run($dialog);
        $dialog->destroy();
        return;
    }

    # Attempt to save the contents to the file.

    if (! defined($fh = IO::File->new($file_name, "w")))
    {
        my $dialog = Gtk2::MessageDialog->new
            ($browser->{window},
             ["modal"],
             "warning",
             "close",
             __x("{error_message}.", error_message => $!));
        busy_dialog_run($dialog);
        $dialog->destroy();
        return;
    }
    binmode($fh);
    $browser->{mtn}->get_file(\$data,
                              $browser->{file_being_viewed}->{manifest_entry}->
                                  {file_id});
    $fh->print($data);
    $fh->close();
    $fh = undef;

    # Get the user preference settings for this type of file.

    determine_mime_type($browser->{file_being_viewed}->{short_name},
                        \$data,
                        \$mime_type,
                        \$mime_details);

    # If the user has specified a helper application then use that to view the
    # file, otherwise use the default desktop settings.

    if (defined($mime_details) && $mime_details->{helper_application} ne "")
    {

        my $line_nr = 1;

        # Use the specified helper application (having validated it first),
        # replacing `{line}' with the number of the line currently in the
        # middle of the viewer window and `{file}' with the real file name.

        $helper = $mime_details->{helper_application};
        return unless (program_valid((split(/[[:blank:]]/, $helper))[0],
                                     $browser->{window}));
        if ($browser->{file_displayed_is_textual})
        {
            my ($iter,
                $rect,
                $y);
            $rect = $browser->{file_view_sv}->get_visible_rect();
            $y = $rect->y() + floor(($rect->height() + 0.5) / 2);
            $iter = ($browser->{file_view_sv}->get_line_at_y($y))[0];
            $line_nr = $iter->get_line();
            ++ $line_nr;
        }
        $helper =~ s/\{line\}/$line_nr/g;
        if ($helper =~ m/\{file\}/)
        {
            $helper =~ s/\{file\}/$file_name/g;
        }
        else
        {
            $helper .= " " . $file_name;
        }

        # Launch it.

        system($helper . " &");

    }
    else
    {

        # Use the desktop to load the file.

        # Use the appropriate application for this type of file, defaulting to
        # Vi if necessary.

        if (! defined($mime_type =
                      Gnome2::VFS->get_mime_type("file://" . $file_name)))
        {
            my $dialog = Gtk2::MessageDialog->new
                ($browser->{window},
                 ["modal"],
                 "warning",
                 "close",
                 __("Unknown file type, not viewing."));
            busy_dialog_run($dialog);
            $dialog->destroy();
            return;
        }
        $app = $mime_obj->get_default_application()
            if (defined($mime_obj = Gnome2::VFS::Mime::Type->new($mime_type)));
        if (defined($app))
        {

            # Use the command attribute if it is present rather than the
            # launch() method as this can crash on some systems.

            if (exists($app->{command}))
            {
                system($app->{command} . " " . $file_name . " &");
            }
            else
            {
                my $status;
                if (($status = $app->launch("file://" . $file_name)) ne "ok")
                {
                    my $dialog = Gtk2::MessageDialog->new_with_markup
                        ($browser->{window},
                         ["modal"],
                         "warning",
                         "close",
                         __x("Gnome cannot launch the helper application\n"
                                 . "for MIME type {mime_type}.\nGnome gave:\n"
                                 . "<b><i>{gnome_error_message}</i></b>",
                             mime_type           => $mime_type,
                             gnome_error_message => $status));
                    busy_dialog_run($dialog);
                    $dialog->destroy();
                }
            }
        }
        else
        {
            my $dialog = Gtk2::MessageDialog->new
                ($browser->{window},
                 ["modal"],
                 "info",
                 "close",
                 __x("No application is associated with\n"
                         . "MIME type {mime_type},\nusing Vi instead.",
                     mime_type => $mime_type));
            busy_dialog_run($dialog);
            $dialog->destroy();
            system("xterm -e vi " . $file_name . " &");
        }

    }

}
#
##############################################################################
#
#   Routine      - annotate_button_clicked_cb
#
#   Description  - Callback routine called when the user clicks on the
#                  annotate button in a main browser window.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub annotate_button_clicked_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    my @revision_ids;

    get_revision_ids($browser, \@revision_ids);
    display_annotation($browser->{mtn},
                       $revision_ids[0],
                       $browser->{file_being_viewed}->{manifest_entry}->
                           {name});

}
#
##############################################################################
#
#   Routine      - file_change_history_button_clicked_cb
#
#   Description  - Callback routine called when the user clicks on the file
#                  change history button in a main browser window.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub file_change_history_button_clicked_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    my @revision_ids;

    get_revision_ids($browser, \@revision_ids);
    display_file_change_history($browser->{mtn},
                                $revision_ids[0],
                                $browser->{file_being_viewed}->
                                    {manifest_entry}->{name});

}
#
##############################################################################
#
#   Routine      - manifest_browser_treeselection_changed_cb
#
#   Description  - Callback routine called when the user selects an entry in
#                  the manifest treeview in a main browser window.
#
#   Data         - $widget  : The widget object that received the signal.
#                  $browser : The browser instance that is associated with
#                             this widget.
#
##############################################################################



sub manifest_browser_treeselection_changed_cb($$)
{

    my ($widget, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    my ($manifest_entry,
        $short_name);

    # Get the manifest entry details for the item that was selected.

    if ($widget->count_selected_rows() > 0)
    {
        my ($iter,
            $model);
        ($model, $iter) = $widget->get_selected();
        $short_name = $model->get($iter, MLS_NAME_COLUMN);
        $manifest_entry = $model->get($iter, MLS_MANIFEST_ENTRY_COLUMN);
    }

    # If the item is a file then display its contents, otherwise if it is a
    # directory then just ignore it.

    if (defined($manifest_entry) && $manifest_entry->{type} eq "file")
    {
        $browser->{file_being_viewed} = {short_name     => $short_name,
                                         manifest_entry => $manifest_entry};
        $browser->{appbar}->clear_stack();
        &{$browser->{update_handler}}($browser, FILE_CHANGED);
    }

}
#
##############################################################################
#
#   Routine      - manifest_browser_treeview_row_activated_cb
#
#   Description  - Callback routine called when the user double clicks on an
#                  entry in the manifest treeview in a main browser window.
#
#   Data         - $widget           : The widget object that received the
#                                      signal.
#                  $tree_path        : A Gtk2::TreePath object for the
#                                      selected item.
#                  $tree_view_column : A Gtk2::TreeViewColumn object for the
#                                      selected item.
#                  $browser          : The browser instance that is associated
#                                      with this widget.
#
##############################################################################



sub manifest_browser_treeview_row_activated_cb($$$$)
{

    my ($widget, $tree_path, $tree_view_column, $browser) = @_;

    return if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    my $manifest_entry;

    # Get the manifest entry details for the item that was double-clicked.

    $widget->get_selection()->selected_foreach
        (sub {
             my ($model, $path, $iter) = @_;
             $manifest_entry = $model->get($iter, MLS_MANIFEST_ENTRY_COLUMN);
         });

    # If the item is a directory then change to it, otherwise if it is a file
    # then just ignore it.

    if (defined($manifest_entry) && $manifest_entry->{type} eq "directory")
    {
        $browser->{directory_combo_details}->{value} = $manifest_entry->{name};
        $browser->{directory_combo_details}->{complete} = 1;
        $browser->{directory_entry}->set_text($manifest_entry->{name});
        $browser->{directory_entry}->set_position(-1);
        $browser->{appbar}->clear_stack();
        &{$browser->{update_handler}}($browser, DIRECTORY_CHANGED);
    }

}
#
##############################################################################
#
#   Routine      - main_window_delete_event_cb
#
#   Description  - Callback routine called when a main window is about to be
#                  dismissed.
#
#   Data         - $widget      : The widget object that received the signal.
#                  $event       : A Gtk2::Gdk::Event object describing the
#                                 event that has occurred.
#                  $browser     : The browser instance that is associated with
#                                 this widget.
#                  Return Value : TRUE if the event has been handled and needs
#                                 no further handling, otherwise FALSE if the
#                                 event should carry on through the remaining
#                                 event handling.
#
##############################################################################



sub main_window_delete_event_cb($$$)
{

    my ($widget, $event, $browser) = @_;

    return TRUE if ($browser->{in_cb});
    local $browser->{in_cb} = 1;

    my $others_mapped;

    # Only exit if this is the only browser window currently being shown.

    $others_mapped = 0;
    WindowManager->instance()->cond_find
        ($window_type,
         sub {
             my ($instance, $type) = @_;
             if ($instance != $browser && $instance->{window}->mapped())
             {
                 $others_mapped = 1;
                 return 1;
             }
             return;
         });

    hide_find_text_and_goto_line($browser->{file_view_sv});
    $browser->{window}->hide();
    $browser->{mtn} = undef;

    # Flush out any cached information.

    $browser->{branch_combo_details}->{preset} = 0;
    $browser->{revision_combo_details}->{preset} = 0;
    $browser->{directory_combo_details}->{preset} = 0;
    $browser->{file_being_viewed_preset_value} = "";
    &{$browser->{update_handler}}($browser, DATABASE_CHANGED);

    quit_from_gtk2() unless ($others_mapped);

    return TRUE;

}
#
##############################################################################
#
#   Routine      - get_browser_window
#
#   Description  - Creates or prepares an existing browser window for use.
#
#   Data         - $mtn         : The Monotone::AutomateStdio object that is
#                                 to be used for the browser window. If it is
#                                 undef then no database is used and a blank
#                                 browser window is displayed.
#                  $branch      : The branch name that is to be preselected in
#                                 the browser window. This is optional unless
#                                 any of the following arguments are
#                                 specified.
#                  $revision_id : The revision id that is to be preselected in
#                                 the browser window. This is optional unless
#                                 any of the following arguments are
#                                 specified.
#                  $directory   : The directory that is to be preselected in
#                                 the browser window. This is optional unless
#                                 the following argument is specified.
#                  $file        : The file that is to be displayed in the
#                                 browser window. This is optional.
#                  Return Value : A reference to the newly created or unused
#                                 browser instance record.
#
##############################################################################



sub get_browser_window(;$$$$$)
{

    my ($mtn, $branch, $revision_id, $directory, $file) = @_;

    my $browser;
    my $wm = WindowManager->instance();

    # Create a new browser window if an unused one wasn't found, otherwise
    # reuse an existing unused one.

    if (! defined($browser = $wm->find_unused($window_type)))
    {

        my ($cb,
            $glade,
            $image,
            $menu_item,
            $renderer,
            $submenu,
            $subsubmenu,
            $tv_column);

        $browser = {};
        $browser->{mtn} = $mtn;
        $glade = create_browser_widgets();

        # Flag to stop recursive calling of callbacks.

        $browser->{in_cb} = 0;
        local $browser->{in_cb} = 1;

        # Connect Glade registered signal handlers.

        glade_signal_autoconnect($glade, $browser);

        # Link in the update handler for the browser.

        $browser->{update_handler} = \&update_browser_state;

        # Get the widgets that we are interested in.

        $browser->{window} = $glade->get_widget($window_type);
        foreach my $widget ("appbar",
                            "open_recent_menuitem",
                            "connect_to_server_menuitem",
                            "close_menuitem",
                            "file_encoding_menuitem",
                            "choose_authentication_key_menuitem",
                            "compare_arbitrary_revisions_menuitem",
                            "compare_workspace_menuitem",
                            "main_vbox",
                            "browser_hpaned",
                            "close_toolbutton",
                            "reload_toolbutton",
                            "branch_entry",
                            "branch_list_togglebutton",
                            "revision_entry",
                            "revision_list_togglebutton",
                            "tagged_checkbutton",
                            "history_graph_button",
                            "directory_entry",
                            "directory_list_togglebutton",
                            "directory_up_button",
                            "find_files_button",
                            "show_line_numbers_togglebutton",
                            "annotate_button",
                            "manifest_browser_treeview",
                            "file_view_scrolledwindow",
                            "file_name_value_label",
                            "file_author_value_label",
                            "last_update_value_label",
                            "file_revision_id_value_label",
                            "file_button_vbox",
                            "database_name_value_label",
                            "date_value_label",
                            "author_value_label",
                            "change_log_value_label")
        {
            $browser->{$widget} = $glade->get_widget($widget);
        }

        set_window_size($browser->{window}, $window_type);

        # Setup button sensitivity groups.

        $browser->{text_file_sensitive_group} = [];
        foreach my $item ("search_text", "goto_text_line", "annotate")
        {
            push(@{$browser->{text_file_sensitive_group}},
                 $glade->get_widget($item . "_button"));
        }
        $browser->{revision_sensitive_group} = [];
        foreach my $item ("revision_change_history", "revision_change_log")
        {
            push(@{$browser->{revision_sensitive_group}},
                 $glade->get_widget($item . "_button"));
        }

        # Make sure that all menu history lists are properly initialised.

        handle_history("open_recent_menu", undef, OPEN_RECENT_MENU_MAX_SIZE);

        # Setup the open recent and connect to server submenus.

        update_open_recent_menu($browser);
        update_server_bookmarks_menu($browser);

        # Setup the file encodings submenu.

        $submenu = Gtk2::Menu->new();
        $cb = sub
        {
            my ($widget, $details) = @_;
            return if ($details->{instance}->{in_cb});
            local $details->{instance}->{in_cb} = 1;
            $details->{instance}->{appbar}->set_status
                (__x("Changed file encoding from `{old_encoding}' to "
                         . "`{new_encoding}'",
                     old_encoding => __($file_encoding),
                     new_encoding => __($details->{encoding})));
            $file_encoding = $details->{encoding};
        };
        foreach my $encoding ("ascii", "utf8", "UTF-16")
        {
            $menu_item = Gtk2::MenuItem->new_with_label(__($encoding));
            $menu_item->signal_connect("activate",
                                       $cb,
                                       {instance => $browser,
                                        encoding => $encoding});
            $menu_item->show();
            $submenu->append($menu_item);
        }
        $subsubmenu = Gtk2::Menu->new();
        foreach my $encoding (@encodings)
        {
            $menu_item = Gtk2::MenuItem->new_with_label(__($encoding));
            $menu_item->signal_connect("activate",
                                       $cb,
                                       {instance => $browser,
                                        encoding => $encoding});
            $menu_item->show();
            $subsubmenu->append($menu_item);
        }
        $subsubmenu->show();
        $menu_item = Gtk2::MenuItem->new(__("All Encodings"));
        $menu_item->set_submenu($subsubmenu);
        $menu_item->show();
        $submenu->append($menu_item);
        $submenu->show();
        $browser->{file_encoding_menuitem}->set_submenu($submenu);

        # Setup the authentication keys submenu.

        $submenu = Gtk2::Menu->new();
        $cb = sub
        {
            my ($widget, $details) = @_;
            return if ($details->{instance}->{in_cb});
            local $details->{instance}->{in_cb} = 1;
            $details->{instance}->{appbar}->set_status
                (__x("Changed authentication key from `{old_key}' to "
                         . "`{new_key}'",
                     old_key => defined($mtn_key)
                                    ? $mtn_key : __("Unspecified"),
                     new_key => defined($details->{key})
                                    ? $details->{key} : __("Unspecified")));
            $mtn_key = $details->{key};
        };
        $menu_item = Gtk2::MenuItem->new(__("Unspecified"));
        $menu_item->signal_connect("activate",
                                   $cb,
                                   {instance => $browser,
                                    key      => undef});
        $menu_item->show();
        $submenu->append($menu_item);
        $menu_item = Gtk2::SeparatorMenuItem->new();
        $menu_item->show();
        $submenu->append($menu_item);
        if (scalar(@keys) > 0)
        {
            foreach my $key (@keys)
            {
                $menu_item = Gtk2::MenuItem->new_with_label($key);
                $menu_item->signal_connect("activate",
                                           $cb,
                                           {instance => $browser,
                                            key      => $key});
                $menu_item->show();
                $submenu->append($menu_item);
            }
        }
        else
        {
            $menu_item = Gtk2::MenuItem->new(__("No Keys"));
            $menu_item->set_sensitive(FALSE);
            $menu_item->show();
            $submenu->append($menu_item);
        }
        $submenu->show();
        $browser->{choose_authentication_key_menuitem}->set_submenu($submenu);

        # Setup the tree view manifest file browser.

        $browser->{manifest_liststore} =
            Gtk2::ListStore->new(MLS_COLUMN_TYPES);
        $browser->{manifest_browser_treeview}->
            set_model($browser->{manifest_liststore});

        $tv_column = Gtk2::TreeViewColumn->new();
        $image = Gtk2::Image->new_from_stock("gtk-file", "menu");
        $image->show_all();
        $tv_column->set_widget($image);
        $tv_column->set_resizable(FALSE);
        $tv_column->set_sort_column_id(MLS_ICON_COLUMN);
        $renderer = Gtk2::CellRendererPixbuf->new();
        $tv_column->pack_start($renderer, TRUE);
        $tv_column->set_attributes($renderer, "stock-id" => MLS_ICON_COLUMN);
        $browser->{manifest_browser_treeview}->append_column($tv_column);

        $tv_column = Gtk2::TreeViewColumn->new();
        $tv_column->set_title(__("File Name"));
        if ($user_preferences->{show_file_details})
        {
            $tv_column->set_resizable(TRUE);
            $tv_column->set_sizing("fixed");
            $tv_column->set_fixed_width(180);
        }
        else
        {
            $tv_column->set_resizable(FALSE);
            $tv_column->set_sizing("autosize");
        }
        $tv_column->set_sort_column_id(MLS_NAME_COLUMN);
        $renderer = Gtk2::CellRendererText->new();
        $tv_column->pack_start($renderer, TRUE);
        $tv_column->set_attributes($renderer, "text" => MLS_NAME_COLUMN);
        $browser->{manifest_browser_treeview}->append_column($tv_column);

        # Only set up the remaining columns if that is what the user wants.

        if ($user_preferences->{show_file_details})
        {
            $tv_column = Gtk2::TreeViewColumn->new();
            $tv_column->set_title(__("Last Update"));
            $tv_column->set_resizable(TRUE);
            $tv_column->set_sizing("grow-only");
            $tv_column->set_sort_column_id(MLS_DATE_COLUMN);
            $renderer = Gtk2::CellRendererText->new();
            $tv_column->pack_start($renderer, TRUE);
            $tv_column->set_attributes($renderer, "text" => MLS_DATE_COLUMN);
            $browser->{manifest_browser_treeview}->append_column($tv_column);

            $tv_column = Gtk2::TreeViewColumn->new();
            $tv_column->set_title(__("Author"));
            $tv_column->set_resizable(TRUE);
            $tv_column->set_sizing("autosize");
            $tv_column->set_sort_column_id(MLS_AUTHOR_COLUMN);
            $renderer = Gtk2::CellRendererText->new();
            $tv_column->pack_start($renderer, TRUE);
            $tv_column->set_attributes($renderer, "text" => MLS_AUTHOR_COLUMN);
            $browser->{manifest_browser_treeview}->append_column($tv_column);

            treeview_setup_search_column_selection
                ($browser->{manifest_browser_treeview}, 1 .. 3);
        }
        $browser->{show_file_details} = $user_preferences->{show_file_details};

        $browser->{manifest_browser_treeview}->
            set_search_column(MLS_NAME_COLUMN);
        $browser->{manifest_browser_treeview}->
            set_search_equal_func(\&treeview_column_searcher);

        $browser->{manifest_browser_treeview}->get_selection()->
            signal_connect("changed",
                           \&manifest_browser_treeselection_changed_cb,
                           $browser);

        # Setup the file file viewer (with syntax highlighting).

        $browser->{file_view_svbuffer} = Gtk2::SourceView::Buffer->new(undef);
        $browser->{file_view_svbuffer}->set_check_brackets(FALSE);
        $browser->{file_view_svbuffer}->set_max_undo_levels(0);
        $browser->{file_view_svbuffer}->begin_not_undoable_action();
        $browser->{file_view_svlangmgr} =
            Gtk2::SourceView::LanguagesManager->new();
        $browser->{file_view_sv} = Gtk2::SourceView::View->
            new_with_buffer($browser->{file_view_svbuffer});
        $browser->{file_view_sv}->modify_font($mono_font);
        $browser->{file_view_sv}->set_cursor_visible(FALSE);
        $browser->{file_view_sv}->set_editable(FALSE);
        $browser->{file_view_scrolledwindow}->add($browser->{file_view_sv});
        $browser->{file_view_sv_populate_popup_handler} =
            $browser->{file_view_sv}->signal_connect
                ("populate_popup",
                 \&find_text_and_goto_line_textview_populate_popup_cb,
                 $browser);
        $browser->{file_view_sv_key_press_handler} =
            $browser->{file_view_sv}->signal_connect
                ("key_press_event",
                 \&find_text_and_goto_line_textview_key_press_event_cb,
                 $browser);
        $browser->{file_view_sv}->show_all();

        # Enable the showing of line numbers by default if that is what the
        # user wants.

        if ($user_preferences->{show_line_numbers})
        {
            $browser->{show_line_numbers_togglebutton}->set_active(TRUE);
            $browser->{file_view_sv}->set_show_line_numbers(TRUE);
        }

        # Display the window.

        $browser->{window}->show_all();
        $browser->{window}->present();

        # Register the window for management and set up the help callbacks.

        $wm->manage($browser, $window_type, $browser->{window});
        register_help_callbacks
            ($browser,
             $glade,
             {widget   => "menubar_bonobodockitem",
              help_ref => __("mtnb-gsc-menus")},
             {widget   => "toolbar_bonobodockitem",
              help_ref => __("mtnb-gsc-tool-bar")},
             {widget   => "view_frame",
              help_ref => __("mtnb-gsc-selecting-a-branch-and-revision")},
             {widget   => "view_button_hbox",
              help_ref => __("mtnb-gsc-browser-buttons")},
             {widget   => "revision_browser_frame",
              help_ref => __("mtnb-gsc-selecting-a-file-to-display")},
             {widget   => "directory_button_hbox",
              help_ref => __("mtnb-gsc-browser-buttons")},
             {widget   => "file_viewer_frame",
              help_ref => __("mtnb-gsc-selecting-a-file-to-display")},
             {widget   => "file_details_hbox",
              help_ref => __("mtnb-gsc-information-fields")},
             {widget   => "file_button_vbox",
              help_ref => __("mtnb-gsc-browser-buttons")},
             {widget   => "details_frame",
              help_ref => __("mtnb-gsc-information-fields")},
             {widget   => undef,
              help_ref => __("mtnb-gsc-monotone-browser-window-at-a-glance")});

        # Update the browser's internal state.

        $browser->{branch_combo_details}->{preset} = 0;
        $browser->{revision_combo_details}->{preset} = 0;
        $browser->{directory_combo_details}->{preset} = 0;
        $browser->{file_being_viewed_preset_value} = "";
        $browser->{file_displayed_id} = "";
        $browser->{file_displayed_is_textual} = 0;
        &{$browser->{update_handler}}($browser, ALL_CHANGED);

        # Now the browser instance is completely initialised, set up the
        # branch, revision and directory entry widgets for auto-completion.

        activate_auto_completion($browser->{branch_entry}, $browser);
        activate_auto_completion($browser->{revision_entry}, $browser);
        activate_auto_completion($browser->{directory_entry}, $browser);

    }
    else
    {

        $browser->{in_cb} = 0;
        local $browser->{in_cb} = 1;

        # Reset the browser's state.

        set_window_size($browser->{window}, $window_type);
        $browser->{browser_hpaned}->set_position(300);
        $browser->{tagged_checkbutton}->set_active(FALSE);
        $browser->{appbar}->set_progress_percentage(0);
        $browser->{appbar}->clear_stack();
        $browser->{branch_combo_details}->{preset} = 0;
        $browser->{revision_combo_details}->{preset} = 0;
        $browser->{directory_combo_details}->{preset} = 0;
        $browser->{file_being_viewed_preset_value} = "";
        $browser->{file_displayed_id} = "";
        $browser->{file_displayed_is_textual} = 0;
        $browser->{manifest_liststore}->clear();
        $browser->{manifest_liststore} =
            Gtk2::ListStore->new(MLS_COLUMN_TYPES);
        $browser->{manifest_browser_treeview}->
            set_model($browser->{manifest_liststore});
        $browser->{manifest_browser_treeview}->
            set_search_column(MLS_NAME_COLUMN);
        if ($user_preferences->{show_line_numbers})
        {
            $browser->{show_line_numbers_togglebutton}->set_active(TRUE);
            $browser->{file_view_sv}->set_show_line_numbers(TRUE);
        }
        else
        {
            $browser->{show_line_numbers_togglebutton}->set_active(FALSE);
            $browser->{file_view_sv}->set_show_line_numbers(FALSE);
        }
        &{$browser->{update_handler}}($browser, ALL_CHANGED);
        $browser->{window}->show_all();
        $browser->{window}->present();

        # Now update with the details of the specified database.

        if (defined($mtn))
        {
            $browser->{mtn} = $mtn;
            &{$browser->{update_handler}}($browser, DATABASE_CHANGED);
        }

    }

    local $browser->{in_cb} = 1;

    # Now deal with any presetting that is required.

    if (defined($branch))
    {
        $browser->{branch_combo_details}->{preset} = 1;
        $browser->{branch_combo_details}->{value} = $branch;
        if (defined($revision_id))
        {
            $browser->{revision_combo_details}->{preset} = 1;
            $browser->{revision_combo_details}->{value} = $revision_id;
            if (defined($directory))
            {
                $browser->{directory_combo_details}->{preset} = 1;
                $browser->{directory_combo_details}->{value} = $directory;
                if (defined($file))
                {
                    $browser->{file_being_viewed_preset_value} = $file;
                }
            }
        }
        &{$browser->{update_handler}}($browser, ALL_CHANGED);
    }

    # Make sure that the branch entry has the focus and not the bonobo dock.

    $browser->{branch_entry}->grab_focus();
    $browser->{branch_entry}->set_position(-1);

    return $browser;

}
#
##############################################################################
#
#   Routine      - create_browser_widgets
#
#   Description  - Basically calls Gtk2::GladeXML->new() having first modified
#                  the Glade XML GUI specification data so as to take into
#                  account the user's toolbar appearance settings
#                  (unfortunately these settings have to be in place at
#                  creation time).
#
#   Data         - Return Value : The newly create Glade object.
#
##############################################################################



sub create_browser_widgets()
{

    # If the user's preferences indicate non-default toolbar settings then
    # pre-edit the XML before passing it to Gtk2::GladeXML->new_from_buffer(),
    # otherwise proceed as normal.

    if ($user_preferences->{toolbar_settings}->{hide_text}
        || $user_preferences->{toolbar_settings}->{fixed})
    {

        my ($file,
            @glade_data,
            $seen_toolbar);
        my $fixed     = $user_preferences->{toolbar_settings}->{fixed};
        my $hide_text = $user_preferences->{toolbar_settings}->{hide_text};

        # Open the Glade file and read it into an array.

        die(__x("open failed: {error_message}\n", error_message => $!))
            unless (defined($file = IO::File->new($glade_file, "r")));
        @glade_data = $file->getlines();
        $file->close();

        # Change the XML data depending upon the user's preferences.

        foreach my $line (@glade_data)
        {

            # Deal with the toolbar's icon only or both setting.

            if ($hide_text && $line =~ m/>GTK_TOOLBAR_BOTH</)
            {
                $line =~ s/>GTK_TOOLBAR_BOTH</>GTK_TOOLBAR_ICONS</g;
                $hide_text = undef;
            }
            if ($fixed)
            {
                if ($seen_toolbar)
                {
                    if ($line =~ m/BONOBO_DOCK_ITEM_BEH_EXCLUSIVE/)
                    {
                        $line =~
                            s/<\/prop/|BONOBO_DOCK_ITEM_BEH_LOCKED<\/prop/g;
                        $fixed = undef;
                    }
                }
                elsif ($line =~ m/id="toolbar_bonobodockitem">/)
                {
                    $seen_toolbar = 1;
                }
            }

            # Deal with the toolbar's fixed or floating setting.

            if ($hide_text && $line =~ m/>GTK_TOOLBAR_BOTH</)
            {
                $line =~ s/>GTK_TOOLBAR_BOTH</>GTK_TOOLBAR_ICONS</g;
                $hide_text = undef;
            }

            # We need to include the full path for any custom image files (this
            # is needed as Gtk2::GladeXML assumes that the image files are
            # relative to the XML file otherwise, but we didn't give it an XML
            # file and so it doesn't know where the directory is).

            $line = $1 . File::Spec->catfile(UI_DIR, $2) . $3
                if ($line =~ m/^(.*>)([^<>]+\.png)(<.*)$/);

        }

        # Create and return the Glade object.

        return Gtk2::GladeXML->new_from_buffer(join("", @glade_data),
                                               $window_type,
                                               APPLICATION_NAME);

    }
    else
    {
        return Gtk2::GladeXML->new($glade_file,
                                   $window_type,
                                   APPLICATION_NAME);
    }

}
#
##############################################################################
#
#   Routine      - update_open_recent_menu
#
#   Description  - Creates or updates an open recent menu for the specified
#                  browser window.
#
#   Data         - $browser : The browser instance that is to have its open
#                             recent menu created or updated.
#
##############################################################################



sub update_open_recent_menu($)
{

    my $browser = $_[0];

    my ($old_menu,
        $submenu);

    # Create a new open recent submenu.

    if (scalar(@{$user_preferences->{histories}->{open_recent_menu}}) > 0)
    {
        $submenu = Gtk2::Menu->new();
        foreach my $db (@{$user_preferences->{histories}->{open_recent_menu}})
        {
            my $menu_item = Gtk2::MenuItem->new_with_label($db);
            $menu_item->signal_connect("activate",
                                       \&open_recent_activate_cb,
                                       {browser  => $browser,
                                        database => $db});
            $menu_item->show();
            $submenu->append($menu_item);
        }
        $submenu->show();
        $browser->{open_recent_menuitem}->set_sensitive(TRUE);
    }
    else
    {
        $browser->{open_recent_menuitem}->set_sensitive(FALSE);
    }

    # Replace the existing menu with the new one.

    $old_menu = $browser->{open_recent_menuitem}->get_submenu();
    $browser->{open_recent_menuitem}->set_submenu($submenu)
        if (defined($submenu));
    if (defined($old_menu))
    {
        $old_menu->destroy()
    }

}
#
##############################################################################
#
#   Routine      - update_server_bookmarks_menu
#
#   Description  - Creates or updates a server bookmarks menu for the
#                  specified browser window.
#
#   Data         - $browser : The browser instance that is to have its server
#                             bookmarks menu created or updated.
#
##############################################################################



sub update_server_bookmarks_menu($)
{

    my $browser = $_[0];

    my ($menu_item,
        $old_menu,
        $submenu);

    # Create a new connect to server submenu.

    $submenu = Gtk2::Menu->new();
    if (scalar(@{$user_preferences->{server_bookmarks}}) > 0)
    {
        foreach my $server (@{$user_preferences->{server_bookmarks}})
        {
            $menu_item = Gtk2::MenuItem->new_with_label($server);
            $menu_item->signal_connect("activate",
                                       \&connect_to_server_activate_cb,
                                       {browser => $browser,
                                        server  => $server});
            $menu_item->show();
            $submenu->append($menu_item);
        }
    }
    else
    {
        $menu_item = Gtk2::MenuItem->new(__("No Bookmarks"));
        $menu_item->set_sensitive(FALSE);
        $menu_item->show();
        $submenu->append($menu_item);
    }
    $menu_item = Gtk2::SeparatorMenuItem->new();
    $menu_item->show();
    $submenu->append($menu_item);
    $menu_item = Gtk2::MenuItem->new(__("Manage Server Bookmarks"));
    $menu_item->signal_connect("activate",
                               \&manage_server_bookmarks_activate_cb,
                               $browser);
    $menu_item->show();
    $submenu->append($menu_item);
    $submenu->show();

    # Replace the existing menu with the new one.

    $old_menu = $browser->{connect_to_server_menuitem}->get_submenu();
    $browser->{connect_to_server_menuitem}->set_submenu($submenu);
    if (defined($old_menu))
    {
        $old_menu->destroy()
    }

}
#
##############################################################################
#
#   Routine      - update_browser_state
#
#   Description  - Update the display of the specified browser instance
#                  according to the specified state.
#
#   Data         - $browser : The browser instance that is to have its state
#                             updated.
#                  $changed : What the user has changed.
#
##############################################################################



sub update_browser_state($$)
{

    my ($browser, $changed) = @_;

    my $use_advanced_find;
    my $wm = WindowManager->instance();

    $wm->make_busy($browser, 1);
    $browser->{appbar}->push($browser->{appbar}->get_status()->get_text());
    $wm->update_gui();

    # The database has changed.

    if ($changed == DATABASE_CHANGED)
    {

        my $db_name;

        if (! defined($browser->{mtn}))
        {

            # Disable the browser as no database is associated with it.

            $browser->{close_menuitem}->set_sensitive(FALSE);
            $browser->{compare_arbitrary_revisions_menuitem}->
                set_sensitive(FALSE);
            $browser->{compare_workspace_menuitem}->set_sensitive(FALSE);
            $browser->{close_toolbutton}->set_sensitive(FALSE);
            $browser->{reload_toolbutton}->set_sensitive(FALSE);
            $browser->{main_vbox}->set_sensitive(FALSE);
            set_label_value($browser->{database_name_value_label}, "");

        }
        else
        {

            # Enable the browser as there is a database associated with it.

            $browser->{close_menuitem}->set_sensitive(TRUE);
            $browser->{compare_arbitrary_revisions_menuitem}->
                set_sensitive(TRUE);
            if ($browser->{mtn}->has_workspace())
            {
                $browser->{compare_workspace_menuitem}->set_sensitive(TRUE);
            }
            else
            {
                $browser->{compare_workspace_menuitem}->set_sensitive(FALSE);
            }
            $browser->{close_toolbutton}->set_sensitive(TRUE);
            $browser->{reload_toolbutton}->set_sensitive(TRUE);
            $browser->{main_vbox}->set_sensitive(TRUE);
            if (defined($browser->{mtn}->get_service_name()))
            {
                set_label_value
                    ($browser->{database_name_value_label},
                     __x("<Service> ({service_name})",
                         service_name => $browser->{mtn}->get_service_name()));
            }
            else
            {
                if (defined($browser->{mtn}->get_db_name()))
                {
                    set_label_value($browser->{database_name_value_label},
                                    $browser->{mtn}->get_db_name());
                }
                else
                {
                    $browser->{mtn}->get_option(\$db_name, "database");
                    set_label_value($browser->{database_name_value_label},
                                    __x("<WorkSpace> ({database_name})",
                                        database_name => $db_name));
                }
            }

            # Make sure that the branch entry has the focus and not the bonobo
            # dock.

            $browser->{branch_entry}->grab_focus();
            $browser->{branch_entry}->set_position(-1);

        }

    }

    # The list of available branches has changed.

    if ($changed & BRANCH)
    {

        my @branch_list;

        # Reset the branch selection.

        $browser->{branch_combo_details}->{completion} = undef;
        if ($browser->{branch_combo_details}->{preset})
        {
            $browser->{branch_combo_details}->{complete} = 1;
            $browser->{branch_combo_details}->{preset} = 0;
        }
        else
        {
            $browser->{branch_combo_details}->{complete} = 0;
            $browser->{branch_combo_details}->{value} = "";
        }

        # Get the new list of branches.

        $browser->{appbar}->set_status(__("Fetching branch list"));
        $wm->update_gui();
        {
            local $pulse_widget = $browser->{appbar}->get_progress();
            $browser->{mtn}->branches(\@branch_list)
                if (defined($browser->{mtn}));
            $browser->{branch_combo_details}->{list} = \@branch_list;
        }

        # Update the branch entry.

        $browser->{branch_combo_details}->{filter} =
            $browser->{branch_combo_details}->{value};
        $browser->{branch_combo_details}->{update} = 1;
        $browser->{branch_entry}->
            set_text($browser->{branch_combo_details}->{value});
        $browser->{branch_entry}->set_position(-1);
        $browser->{appbar}->set_progress_percentage(0);
        $browser->{appbar}->set_status("");
        $wm->update_gui();

    }

    # The list of available revisions has changed.

    if ($changed & REVISION)
    {

        my @revision_list;

        # If auto-selection of the head revision is wanted by the user and it
        # is appropriate then preset the revision id.

        if ($user_preferences->{auto_select_head}
            && ! $browser->{tagged_checkbutton}->get_active()
            && $browser->{branch_combo_details}->{complete}
            && ! $browser->{revision_combo_details}->{preset})
        {

            my @revision_ids;

            # Get the head revision(s) for the branch.

            $browser->{appbar}->set_status(__("Auto selecting head revision"));
            $wm->update_gui();
            $browser->{mtn}->select
                (\@revision_ids,
                 "h:" . $browser->{branch_combo_details}->{value});

            # One head on the branch.

            if (scalar(@revision_ids) == 1)
            {
                $browser->{revision_combo_details}->{preset} = 1;
                $browser->{revision_combo_details}->{value} = $revision_ids[0];
                $browser->{tagged_checkbutton}->set_active(FALSE);
            }

            # Multiple heads on the branch.

            elsif (scalar(@revision_ids) > 1)
            {

                my ($response,
                    $revision_id);

                # Ask the user what they want to do.

                $response = multiple_revisions_selection
                    ($browser->{window},
                     __x("The `{branch}' branch\n"
                             . "has multiple heads and <i>Auto select head "
                             . "revision</i> is switched\n"
                             . "on. Please either select the revision by its "
                             . "id using the combobox\n"
                             . "below or use the <i>Advanced Find</i> "
                             . "feature.",
                         branch => $browser->{branch_combo_details}->{value}),
                     \$revision_id,
                     @revision_ids);

                # Deal with their response (either they have selected a
                # revision, they have asked to use the advanced find dialog
                # window or they have aborted in some way).

                if ($response eq "ok")
                {

                    # We have single revision id so simply preset the revision
                    # selection to that revision.

                    $browser->{revision_combo_details}->{preset} = 1;
                    $browser->{revision_combo_details}->{value} = $revision_id;
                    $browser->{tagged_checkbutton}->set_active(FALSE);

                }
                else
                {

                    # The user has either selected advanced find or aborted in
                    # some way.

                    # If necessary flag the fact that the user wants to use
                    # advanced find.

                    $use_advanced_find = 1 if ($response eq "advanced-find");

                    # Either way, no revision is currently selected so we need
                    # to blank out all the revision selection details and
                    # proceed as normal (this will blank out the rest of the
                    # browser).

                    $browser->{revision_combo_details}->{complete} = 0;
                    $browser->{revision_combo_details}->{value} = "";
                    $browser->{tagged_checkbutton}->set_active(FALSE);

                }

            }

            # No heads or revisions on the branch.

            else
            {
                $browser->{revision_combo_details}->{complete} = 0;
                $browser->{revision_combo_details}->{value} = "";
                $browser->{tagged_checkbutton}->set_active(FALSE);
            }

            $browser->{appbar}->set_status("");
            $wm->update_gui();

        }

        # Reset the revision selection.

        $browser->{revision_combo_details}->{completion} = undef;
        if ($browser->{revision_combo_details}->{preset})
        {
            $browser->{revision_combo_details}->{complete} = 1;
            $browser->{revision_combo_details}->{preset} = 0;
        }
        else
        {
            $browser->{revision_combo_details}->{complete} = 0;
            $browser->{revision_combo_details}->{value} = "";
        }

        # Get the new list of revisions.

        if ($browser->{branch_combo_details}->{complete})
        {
            $browser->{appbar}->set_status(__("Fetching revision list"));
            $wm->update_gui();
            get_branch_revisions($browser->{mtn},
                                 $browser->{branch_combo_details}->{value},
                                 $browser->{tagged_checkbutton}->get_active(),
                                 $browser->{appbar},
                                 \@revision_list);
        }
        $browser->{revision_combo_details}->{list} = \@revision_list;

        # Update the revision entry.

        $browser->{revision_combo_details}->{filter} =
            $browser->{revision_combo_details}->{value};
        $browser->{revision_combo_details}->{update} = 1;
        $browser->{revision_entry}->
            set_text($browser->{revision_combo_details}->{value});
        $browser->{appbar}->set_progress_percentage(0);
        $browser->{appbar}->set_status("");
        $wm->update_gui();

    }

    # The list of available files and directories has changed.

    if ($changed & DIRECTORY)
    {

        my (@directory_list,
            @manifest_list);

        # Reset the directory combo.

        $browser->{directory_combo_details}->{completion} = undef;
        if ($browser->{directory_combo_details}->{preset})
        {
            $browser->{directory_combo_details}->{complete} = 1;
            $browser->{directory_combo_details}->{preset} = 0;
        }
        else
        {
            $browser->{directory_combo_details}->{complete} = 1;
            $browser->{directory_combo_details}->{value} = "";
        }
        set_label_value($browser->{date_value_label}, "");
        set_label_value($browser->{author_value_label}, "");
        set_label_value($browser->{change_log_value_label}, "");

        # Reset the name of the file being viewed.

        $browser->{file_being_viewed} = {};

        # Get the new manifest.

        $browser->{appbar}->set_status(__("Fetching manifest"));
        $wm->update_gui();
        if ($browser->{revision_combo_details}->{complete})
        {

            my ($revision_id,
                @revision_ids);

            # Get the revision id(s) that match what the user has selected.
            # More than one revision id may be returned if the selected tag
            # isn't unique on that branch.

            get_revision_ids($browser, \@revision_ids);

            # Deal with more than one revision id being selected.

            if (scalar(@revision_ids) > 1)
            {

                my $response;

                # More than one revision matches the tag that was selected by
                # the user. Ask them what they want to do.

                $response = multiple_revisions_selection
                    ($browser->{window},
                     __x("The `{tag_name}' tag is not unique on this\n"
                             . "branch. Please either select the revision by "
                             . "its id using the\n"
                             . "combobox below or use the "
                             . "<i>Advanced Find</i> feature.",
                         tag_name =>
                             $browser->{revision_combo_details}->{value}),
                     \$revision_id,
                     @revision_ids);

                # Deal with their response (either they have selected a
                # revision, they have asked to use the advanced find dialog
                # window or they have aborted in some way).

                if ($response eq "ok")
                {

                    # We have a single revision id so we need to redo the
                    # revision selection bit but with tag names switched off.
                    # The easiest way to do this is to recursively call this
                    # routine with $changed set to just REVISION. Afterwards
                    # just carry on as if a revision id had been selected in
                    # the first place.

                    $browser->{revision_combo_details}->{preset} = 1;
                    $browser->{revision_combo_details}->{value} = $revision_id;
                    $browser->{tagged_checkbutton}->set_active(FALSE);
                    &{$browser->{update_handler}}($browser, REVISION);

                }
                else
                {

                    # The user has either selected advanced find or aborted in
                    # some way.

                    # If necessary flag the fact that the user wants to use
                    # advanced find.

                    $use_advanced_find = 1 if ($response eq "advanced-find");

                    # Either way, no revision is currently selected so we need
                    # to blank out all the revision selection details and
                    # proceed as normal (this will blank out the rest of the
                    # browser).

                    $browser->{revision_combo_details}->{complete} = 0;
                    $browser->{revision_combo_details}->{filter} = "";
                    $browser->{revision_combo_details}->{update} = 1;
                    $browser->{revision_combo_details}->{value} = "";
                    $browser->{revision_entry}->set_text("");

                }

            }
            else
            {

                # Only one revision id was selected. This is the norm.

                $revision_id = $revision_ids[0];

            }

            # If we now have one revision selected then get its details.

            if (defined($revision_id))
            {
                my ($author,
                    @certs_list,
                    $change_log,
                    $date);
                if ($browser->{mtn}->supports(MTN_GET_EXTENDED_MANIFEST_OF))
                {
                    $browser->{mtn}->get_extended_manifest_of(\@manifest_list,
                                                              $revision_id);
                }
                else
                {
                    $browser->{mtn}->get_manifest_of(\@manifest_list,
                                                     $revision_id);
                }
                $browser->{mtn}->certs(\@certs_list, $revision_id);
                $author = $change_log = $date = "";
                foreach my $cert (@certs_list)
                {
                    if ($cert->{name} eq "author")
                    {
                        $author = $cert->{value};
                    }
                    elsif ($cert->{name} eq "changelog")
                    {
                        $change_log = $cert->{value};
                        $change_log =~ s/\s+$//s;
                    }
                    elsif ($cert->{name} eq "date")
                    {
                        $date = mtn_time_string_to_locale_time_string
                            ($cert->{value});
                    }
                }
                set_label_value($browser->{date_value_label}, $date);
                set_label_value($browser->{author_value_label}, $author);
                set_label_value($browser->{change_log_value_label},
                                $change_log);
            }

        }
        $browser->{manifest} = \@manifest_list;

        # Generate a simple list of directories for auto completion.

        $browser->{appbar}->set_progress_percentage(0.5);
        $wm->update_gui();
        foreach my $item (@manifest_list)
        {
            push(@directory_list, $item->{name})
                if ($item->{type} eq "directory");
        }
        $browser->{directory_combo_details}->{list} = \@directory_list;
        $browser->{appbar}->set_progress_percentage(1);
        $wm->update_gui();

        # Update the directory entry.

        $browser->{directory_combo_details}->{filter} =
            $browser->{directory_combo_details}->{value};
        $browser->{directory_combo_details}->{update} = 1;
        $browser->{directory_entry}->
            set_text($browser->{directory_combo_details}->{value});
        $browser->{directory_entry}->set_position(-1);
        foreach my $widget (@{$browser->{revision_sensitive_group}})
        {
            $widget->
                set_sensitive($browser->{revision_combo_details}->{complete}
                              ? TRUE : FALSE);
        }
        $browser->{appbar}->set_progress_percentage(0);
        $browser->{appbar}->set_status("");
        $wm->update_gui();

    }

    # The list of displayed files and directories has changed.

    if ($changed & DIRECTORY_VIEW)
    {

        my ($author,
            $counter,
            @directory_entry_list,
            $last_update,
            @revision_ids,
            $taking_our_time);

        # Reset the manifest tree view.

        $browser->{manifest_liststore}->clear();

        # If we are currently at a valid directory then get its contents and
        # enable the find files button.

        if ($browser->{revision_combo_details}->{complete}
            && $browser->{directory_combo_details}->{complete})
        {
            get_dir_contents($browser->{directory_combo_details}->{value},
                             $browser->{manifest},
                             \@directory_entry_list);
            $browser->{find_files_button}->set_sensitive(TRUE);
        }
        else
        {
            $browser->{find_files_button}->set_sensitive(FALSE);
        }

        # Disable the directory up button if we are already at the top level,
        # otherwise make sure it is enabled.

        $browser->{directory_up_button}->set_sensitive
            (($browser->{directory_combo_details}->{value} eq "")
             ? FALSE : TRUE);

        # Update the directory tree view.

        $browser->{appbar}->set_status(__("Populating file details"));
        $wm->update_gui();
        $counter = 1;
        $taking_our_time = 0;
        get_revision_ids($browser, \@revision_ids);
        foreach my $item (@directory_entry_list)
        {

            # Get the latest modification time and the author if that is what
            # the user wants and the entry is a file (caching the result in the
            # manifest for future reference if we have to work it out).

            if ($browser->{show_file_details}
                && $item->{manifest_entry}->{type} eq "file")
            {
                if (! exists($item->{manifest_entry}->{author}))
                {
                    $taking_our_time = 1;
                    cache_extra_file_info($browser->{mtn},
                                          $revision_ids[0],
                                          $item->{manifest_entry});
                }
                $author = $item->{manifest_entry}->{author};
                $last_update = mtn_time_string_to_locale_time_string
                    ($item->{manifest_entry}->{last_update});
            }
            else
            {
                $author = "";
                $last_update = "";
            }

            # Put the entry into the liststore.

            $browser->{manifest_liststore}->
                set($browser->{manifest_liststore}->append(),
                    MLS_ICON_COLUMN,
                        ($item->{manifest_entry}->{type} eq "directory")
                        ? "gtk-open" : "gtk-file",
                    MLS_NAME_COLUMN, $item->{name},
                    MLS_DATE_COLUMN, $last_update,
                    MLS_AUTHOR_COLUMN, $author,
                    MLS_MANIFEST_ENTRY_COLUMN, $item->{manifest_entry});

            if ($browser->{file_being_viewed_preset_value} ne ""
                && $browser->{file_being_viewed_preset_value} eq $item->{name})
            {
                $browser->{file_being_viewed} =
                    {short_name     => $item->{name},
                     manifest_entry => $item->{manifest_entry}};
                $browser->{file_being_viewed_preset_value} = "";
            }

            if ($taking_our_time && ($counter % 10) == 0)
            {
                $browser->{appbar}->set_progress_percentage
                    ($counter / scalar(@directory_entry_list));
                $wm->update_gui();
            }
            ++ $counter;

        }
        if ($taking_our_time)
        {
            $browser->{appbar}->set_progress_percentage(1);
            $wm->update_gui();
        }

        $browser->{manifest_browser_treeview}->scroll_to_point(0, 0)
            if ($browser->{manifest_browser_treeview}->realized());

        $browser->{appbar}->set_progress_percentage(0);
        $browser->{appbar}->set_status("");
        $wm->update_gui();

    }

    # The displayed file contents has changed.

    if ($changed & DISPLAY_OF_FILE)
    {

        # Load up the selected file's contents into the file viewer.

        if (exists($browser->{file_being_viewed}->{manifest_entry}))
        {

            my $manifest_entry =
                $browser->{file_being_viewed}->{manifest_entry};

            # Only do anything if the selected file has changed.

            if ($browser->{file_displayed_id} ne $manifest_entry->{file_id})
            {

                my $last_update;

                # Enable the file buttons, keeping the ones relating to text
                # files disabled, and reset any associated find text window.

                $browser->{file_button_vbox}->set_sensitive(TRUE);
                foreach my $widget (@{$browser->{text_file_sensitive_group}})
                {
                    $widget->set_sensitive(FALSE);
                }
                reset_find_text($browser->{file_view_sv});
                enable_find_text_and_goto_line($browser->{file_view_sv}, 0);

                # Display the selected file's contents.

                display_file($browser, \$browser->{file_displayed_is_textual});

                # If we have just displayed a text file then enable the file
                # buttons applicable to text files and enable any associated
                # find text window that may be displayed.

                if ($browser->{file_displayed_is_textual})
                {
                    foreach my $widget
                        (@{$browser->{text_file_sensitive_group}})
                    {
                        $widget->set_sensitive(TRUE);
                    }
                    $browser->{annotate_button}->set_sensitive(FALSE)
                        if (defined($browser->{mtn}->get_service_name()));
                    enable_find_text_and_goto_line($browser->{file_view_sv},
                                                   1);
                }

                # Update the file details labels.

                if (! exists($manifest_entry->{last_changed_revision}))
                {
                    my @revision_ids;
                    get_revision_ids($browser, \@revision_ids);
                    cache_extra_file_info($browser->{mtn},
                                          $revision_ids[0],
                                          $manifest_entry);
                }
                $last_update = mtn_time_string_to_locale_time_string
                    ($manifest_entry->{last_update});
                set_label_value($browser->{file_name_value_label},
                                $manifest_entry->{name});
                set_label_value($browser->{file_author_value_label},
                                $manifest_entry->{author});
                set_label_value($browser->{last_update_value_label},
                                $last_update);
                set_label_value($browser->{file_revision_id_value_label},
                                $manifest_entry->{last_changed_revision});

                $browser->{file_displayed_id} = $manifest_entry->{file_id};

            }

        }
        else
        {

            # Reset the file view buffer and the associated find text window.

            $browser->{file_displayed_id} = "";
            $browser->{file_displayed_is_textual} = 0;
            $browser->{file_button_vbox}->set_sensitive(FALSE);
            $browser->{file_view_svbuffer}->
                place_cursor($browser->{file_view_svbuffer}->get_start_iter());
            $browser->{file_view_svbuffer}->set_text("");
            $browser->{file_view_svbuffer}->set("highlight", FALSE);
            set_label_value($browser->{file_name_value_label}, "");
            set_label_value($browser->{file_author_value_label}, "");
            set_label_value($browser->{last_update_value_label}, "");
            set_label_value($browser->{file_revision_id_value_label}, "");
            enable_find_text_and_goto_line($browser->{file_view_sv}, 0);

        }

    }

    $browser->{appbar}->pop();
    $wm->make_busy($browser, 0);

    # All done. Now check to see if during the update process the user asked to
    # call up the advanced find dialog window, if so then display it now.

    handle_advanced_find($browser) if ($use_advanced_find);

}
#
##############################################################################
#
#   Routine      - handle_advanced_find
#
#   Description  - Call up the advanced find dialog window for the specified
#                  browser and then populate that browser with any results
#                  that come back from that search.
#
#   Data         - $browser : The browser instance from which the advanced
#                             find is to be done.
#
##############################################################################



sub handle_advanced_find($)
{

    my $browser = $_[0];

    my (@branches,
        $preset_branch,
        $revision_id,
        $state);

    if (advanced_find($browser, \$revision_id, \@branches))
    {

        # Preset branch name. If we already have a selected branch then try and
        # match branch names, if that fails then just pick the first name.

        $preset_branch = 1;
        $state = BRANCH_CHANGED;
        if ($browser->{branch_combo_details}->{complete})
        {
            foreach my $name (@branches)
            {
                if ($name eq $browser->{branch_combo_details}->{value})
                {
                    $preset_branch = 0;
                    last;
                }
            }
        }
        if ($preset_branch)
        {
            $browser->{branch_combo_details}->{preset} = 1;
            $browser->{branch_combo_details}->{value} =
                (scalar(@branches) > 0) ? $branches[0] : "";
            $state = ALL_CHANGED;
        }

        # Preset revision id.

        $browser->{revision_combo_details}->{preset} = 1;
        $browser->{revision_combo_details}->{value} = $revision_id;

        # A revision id is what is returned so switch off the listing of tag
        # names.

        $browser->{tagged_checkbutton}->set_active(FALSE);

        $browser->{appbar}->clear_stack();
        &{$browser->{update_handler}}($browser, $state);

    }

}
#
##############################################################################
#
#   Routine      - display_file
#
#   Description  - Display the currenty selected file in the sourceview
#                  textview.
#
#   Data         - $browser      : The browser instance that is to display the
#                                  file.
#                  $textual_data : A reference to a variable that is to
#                                  contain a boolean indicator as to whether
#                                  the displayed file contains textual or
#                                  binary data.
#
##############################################################################



sub display_file($$)
{

    my ($browser, $textual_data) = @_;

    my ($contents,
        $iter,
        $lang,
        $mime_details,
        $mime_type);

    $$textual_data = 0;

    # Reset the file view buffer.

    $browser->{file_view_svbuffer}->
        place_cursor($browser->{file_view_svbuffer}->get_start_iter());
    $browser->{file_view_svbuffer}->set_text("");
    $browser->{file_view_svbuffer}->set("highlight", FALSE);

    # Get contents.

    $browser->{mtn}->get_file(\$contents,
                              $browser->{file_being_viewed}->{manifest_entry}->
                                  {file_id});

    # Try and work out the MIME type.

    determine_mime_type($browser->{file_being_viewed}->{short_name},
                        \$contents,
                        \$mime_type,
                        \$mime_details);

    # Only attempt to render the file's contents if requested to do so.

    if (! defined($mime_type)
        || (defined($mime_details) && ! $mime_details->{display_internally}))
    {

        # The user doesn't want to display this type of file.

        $browser->{file_view_svbuffer}->
            set_text("<"
                     . (defined($mime_type)
                        ? $mime_type : __("Unknown Contents"))
                     . ">");

    }
    else
    {

        # The user wants to display this type of file.

        # Image/non-image data?

        if ($mime_type =~ m/^image\/.+$/)
        {

            # Image data.

            my $exception;
            my $loader = Gtk2::Gdk::PixbufLoader->new();
            eval
            {
                $loader->write($contents);
            };
            $exception = $@;
            eval
            {
                $loader->close();
            };
            $exception = $@ if ($@);
            if (! $exception)
            {
                $browser->{file_view_svbuffer}->insert_pixbuf
                    ($browser->{file_view_svbuffer}->get_start_iter(),
                     $loader->get_pixbuf());
            }
            else
            {
                $browser->{file_view_svbuffer}->
                    set_text("<" . $mime_type . ">");
            }

        }
        else
        {

            # Non-image data.

            # Binary/text data?

            if (data_is_binary(\$contents))
            {

                # Binary data.

                # We have been asked to display this data. The only thing we
                # can do is to hex dump it out.

                $browser->{file_view_svbuffer}->
                    set_text("<" . $mime_type . "> " . __("Hex dump:\n"));
                $browser->{file_view_svbuffer}->
                    insert($browser->{file_view_svbuffer}->get_end_iter(),
                           ${hex_dump(\$contents)});

            }
            else
            {

                # Text data.

                $$textual_data = 1;

                # Enable syntax highlighting if the user wants it on and it is
                # available for this type of file.

                if ((! defined($mime_details)
                     || (defined($mime_details)
                         && $mime_details->{syntax_highlight}))
                    && defined($lang =
                               $browser->{file_view_svlangmgr}->
                                   get_language_from_mime_type($mime_type)))
                {
                    $browser->{file_view_svbuffer}->set("highlight", TRUE);
                    $browser->{file_view_svbuffer}->set_language($lang);
                }

                # Load in the contents (ensuring UTF-8 encoding if possible).

                eval
                {
                    $contents =
                        decode($file_encoding, $contents, Encode::FB_CROAK);
                };
                $browser->{file_view_svbuffer}->set_text($contents);

            }

            # Delete the trailing newline.

            $iter = $browser->{file_view_svbuffer}->get_end_iter();
            $browser->{file_view_svbuffer}->delete
                ($iter, $browser->{file_view_svbuffer}->get_end_iter())
                if ($iter->backward_char());

        }

    }

    # Scroll back up to the top left.

    $browser->{file_view_svbuffer}->
        place_cursor($browser->{file_view_svbuffer}->get_start_iter());
    if ($browser->{file_view_scrolledwindow}->realized())
    {
        $browser->{file_view_scrolledwindow}->get_vadjustment()->set_value(0);
        $browser->{file_view_scrolledwindow}->get_hadjustment()->set_value(0);
    }

}
#
##############################################################################
#
#   Routine      - determine_mime_type
#
#   Description  - Given a file name and its contents, determine and return
#                  its MIME type and the related entry in the MIME details
#                  table.
#
#   Data         - $file_name    : The name of the file.
#                  $contents     : A reference to a variable containing the
#                                  contents of the file.
#                  $mime_type    : A reference to a variable that is to
#                                  contain the MIME type. This will be undef
#                                  if no match can be found.
#                  $mime_details : A reference to a variable that is to
#                                  contain a refernce to the related entry in
#                                  the MIME information table. This will be
#                                  undef if no match can be found.
#
##############################################################################



sub determine_mime_type($$$$)
{

    my ($file_name, $contents, $mime_type, $mime_details) = @_;

    $$mime_type = $$mime_details = undef;

    # Try and work out the MIME type through file name pattern matching.

    foreach my $entry (@$mime_match_table)
    {
        if ($file_name =~ m/$entry->{re}/)
        {
            $$mime_type = $entry->{details}->{name};
            $$mime_details = $entry->{details};
            return;
        }
    }

    # If that didn't work then try determining the MIME type based upon its
    # contents.

    $$mime_type = Gnome2::VFS->get_mime_type_for_data($$contents);

    # If we now have a MIME type then do another lookup with it but by matching
    # MIME type names and not file name patterns.

    if (defined($$mime_type))
    {
        foreach my $entry (@{$user_preferences->{mime_table}})
        {
            if ($$mime_type eq $entry->{name})
            {
                $$mime_details = $entry;
                return;
            }
        }
    }

}
#
##############################################################################
#
#   Routine      - quit_from_gtk2
#
#   Description  - Routine called when this application wants to quit from
#                  Gtk2. This routine hides all windows and then calls any
#                  registered cleanup handlers (this is mainly used to deal
#                  with Gnome2 canvases which are buggy when being destroyed.
#
#   Data         - None.
#
##############################################################################



sub quit_from_gtk2()
{

    # Hide all the windows, close all the database handles, call all cleanup
    # handlers and then quit out of Gtk2.

    WindowManager->instance()->cond_find
        (undef,
         sub {
             my ($instance, $type) = @_;
             $instance->{window}->hide() if ($instance->{window}->mapped());
             $instance->{mtn} = undef if (exists($instance->{mtn}));
             if (exists($instance->{cleanup_handler})
                 && defined($instance->{cleanup_handler}))
             {
                 $instance->{cleanup_handler}($instance);
             }
             return;
         });
    Gtk2->main_quit();

}
#
##############################################################################
#
#   Routine      - setup_mtn_object
#
#   Description  - Sets up a new Monotone::AutomateStdio object so that it can
#                  be used in this application and also determines what
#                  features are available depending upon the version of
#                  Monotone that is currently in use.
#
#   Data         - $mtn    : The Monotone::AutomateStdio object that is to be
#                            setup.
#                  $parent : The parent window widget that is to be used for
#                            assorted dialog windows that may appear.
#
##############################################################################



sub setup_mtn_object($$)
{

    my ($mtn, $parent) = @_;

    my $save_prefs;

    # Deal with branch suspend certificate settings.

    if ($user_preferences->{show_suspended})
    {
        if ($mtn->supports(MTN_IGNORING_OF_SUSPEND_CERTS))
        {
            $mtn->ignore_suspend_certs(1);
        }
        else
        {
            my $dialog = Gtk2::MessageDialog->
                new($parent,
                    ["modal"],
                    "info",
                    "close",
                    __("Your version of Monotone does not support\n"
                       . "suspend certificates. I will adjusted your\n"
                       . "preferences accordingly."));
            busy_dialog_run($dialog);
            $dialog->destroy();
            $user_preferences->{show_suspended} = 0;
            $save_prefs = 1;
        }
    }

    # Determine whether remote connections to servers are supported.

    if (($mtn->supports(MTN_REMOTE_CONNECTIONS)
         && ! $user_preferences->{remote_connections})
        || (! $mtn->supports(MTN_REMOTE_CONNECTIONS)
            && $user_preferences->{remote_connections}))
    {
        $user_preferences->{remote_connections} =
            $mtn->supports(MTN_REMOTE_CONNECTIONS) ? 1 : 0;
        $save_prefs = 1;
    }

    # Save the user's preferences if they have been changed.

    if ($save_prefs)
    {
        save_preferences($user_preferences, $parent);
    }

}
#
##############################################################################
#
#   Routine      - mtn_db_locked_handler
#
#   Description  - This routine is called when ever a locked database
#                  condition is detected.
#
#   Data         - $mtn         : The Monotone::AutomateStdio object that
#                                 encountered the locked database.
#                  $parent      : The parent window for any dialogs that are
#                                 to be displayed.
#                  Return Value : True if the command is to be retried,
#                                 otherwise false if the locked database
#                                 should be reported as an error.
#
##############################################################################



sub mtn_db_locked_handler($$)
{

    my ($mtn, $parent) = @_;

    my $dialog;

    $dialog = Gtk2::MessageDialog->new
        ($parent,
         ["modal"],
         "info",
         "close",
         __("The Monotone database is currently locked, please\n"
            . "dismiss this dialog when this is no longer the case."));
    busy_dialog_run($dialog);
    $dialog->destroy();

    return 1;

}
#
##############################################################################
#
#   Routine      - mtn_error_handler
#
#   Description  - This routine is called when ever there is a problem with
#                  Monotone.
#
#   Data         - $severity : The severity of the error.
#                  $message  : The error message.
#
##############################################################################



sub mtn_error_handler($$)
{

    my ($severity, $message) = @_;

    my $dialog;

    if ($severity == MTN_SEVERITY_WARNING)
    {
        if (! $suppress_mtn_warnings)
        {
            $dialog = Gtk2::MessageDialog->new_with_markup
                (undef,
                 ["modal"],
                 "warning",
                 "close",
                 __x("Problem with Monotone request, got:\n"
                         . "<b><i>{error_message}</i></b>\n"
                         . "This should not be happening!",
                     error_message => Glib::Markup::escape_text($message)));
            busy_dialog_run($dialog);
            $dialog->destroy();
            WindowManager->instance()->reset_state();
            die($message);
        }
    }
    else
    {
        $dialog = Gtk2::MessageDialog->new_with_markup
            (undef,
             ["modal"],
             "error",
             "close",
             __x("The Monotone subprocess unexpectedly exited with:\n"
                     . "<b><i>{error_message}</i></b>\n"
                     . "This should not be happening!",
                 error_message => Glib::Markup::escape_text($message)));
        busy_dialog_run($dialog);
        $dialog->destroy();
        WindowManager->instance()->reset_state();
        die($message);
    }

}
#
##############################################################################
#
#   Routine      - sigchld_handler
#
#   Description  - This routine is called when ever a subprocess exits.
#
#   Data         - None.
#
##############################################################################



sub sigchld_handler()
{

    my $pid;
    my $wm = WindowManager->instance();

    while (($pid = waitpid(-1, WNOHANG)) > 0)
    {
        my $exit_status = $?;
        if (WIFEXITED($exit_status) || WIFSIGNALED($exit_status))
        {

            my $dealt_with;

            # If it is an mtn process then close down the relevant object so
            # that it will automatically restart when needed.

            $wm->cond_find
                (undef,
                 sub {
                     my ($instance, $type) = @_;
                     if (exists($instance->{mtn}) && defined($instance->{mtn})
                         && $instance->{mtn}->get_pid() == $pid)
                     {
                         my $message;
                         $instance->{mtn}->closedown();
                         if (WIFSIGNALED($exit_status))
                         {
                             $message = __x("terminated by signal {number}",
                                            number => WTERMSIG($exit_status));
                         }
                         else
                         {
                             $message =
                                 __x("exited with status {number}",
                                     number => WEXITSTATUS($exit_status));
                         }
                         my $dialog = Gtk2::MessageDialog->new
                             (undef,
                              ["modal"],
                              "warning",
                              "close",
                              __x("The Monotone subprocess just unexpectedly\n"
                                      . "exited ({error_message}).\n"
                                      . "This should not be happening!\n"
                                      . "It will be restarted when needed.",
                                  error_message => $message));
                         busy_dialog_run($dialog);
                         $dialog->destroy();
                         $wm->reset_state();
                         $dealt_with = 1;
                         return 1;
                     }
                     return;
                 });

            # If the reaped process hasn't been dealt with then store the
            # details so that other parts of this program can access them if
            # they need to.

            add_reaped_process_details($pid, $exit_status)
                unless ($dealt_with);

        }
    }
    warn(__x("waitpid failed: {error_message}", error_message => $!))
        if ($pid < 0 && $! != ECHILD);

}
#
##############################################################################
#
#   Routine      - setup_sigchld_handler
#
#   Description  - This routine sets up the handler for SIGCHLD signals.
#
#   Data         - $handler - A reference to the SIGCHLD handler routine.
#
##############################################################################



sub setup_sigchld_handler($)
{

    my $handler = $_[0];

    my ($reader,
        $writer);

    # Basically set up a SIGCHLD handler that simply writes a character down an
    # anonymous pipe in order to wake up the actual handler that is registered
    # with Gtk2 as a file activity handler. This is efficient and safer than
    # some alternatives.

    die(__x("pipe failed: {error_message}", error_message => $!))
        unless (pipe($reader, $writer));
    $SIG{CHLD} = sub { syswrite($writer, "\n", 1); };
    Gtk2::Helper->add_watch(fileno($reader), "in",
                            sub {
                                my $buffer;
                                sysread($reader, $buffer, 1);
                                &$handler();
                                return 1;
                            });

}
