#!/usr/bin/perl

# gscan2pdf --- to aid the scan to PDF or DjVu process
# Copyright (C) 2006--2013 Jeffrey Ratcliffe <Jeffrey.Ratcliffe@gmail.com>

# This program is free software: you can redistribute it and/or modify
# it under the terms of the version 3 GNU General Public License as
# published by the Free Software Foundation.

# 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 program.  If not, see <http://www.gnu.org/licenses/>.

# Release procedure:
#    Use
#      make tidy
#      TEST_AUTHOR=1 make test
#    immediately before release so as not to affect any patches
#    in between, and then consistently before each commit afterwards.
# 0. Test scan in lineart, greyscale and colour.
# 1. New screendump required? Print screen creates screenshot.png in Desktop.
#    Download new translations (https://translations.launchpad.net/gscan2pdf)
#    Upload .pot
#    Update translators in credits (https://launchpad.net/gscan2pdf/+topcontributors)
#    Check a locale with LC_ALL=de_DE LC_MESSAGES=de_DE LC_CTYPE=de_DE LANG=de_DE LANGUAGE=de_DE bin/gscan2pdf --log=log --locale=<wherever the locale directory is>
#    Check $VERSION. If necessary bump with something like
#     xargs sed -i "s/\(\$VERSION *= \)'1\.2\.2'/\1'1.2.3'/" < MANIFEST
#    Make appropriate updates to debian/changelog
# 2.  perl Makefile.PL
#     make rpmdist debdist
#    test dist sudo dpkg -i gscan2pdf_x.x.x_all.deb
# 3.  git status
#     git tag vx.x.x
#     git push --tags ssh://ra28145@git.code.sf.net/p/gscan2pdf/code master
# 4.  make remote-html
# 5. create version directory in https://sourceforge.net/projects/gscan2pdf/files/gscan2pdf
#     make file_releases
# 6. Freshmeat freshmeat.net/projects/gscan2pdf/releases/new (requires summary of changes)
# 7. Launchpad (https://launchpad.net/gscan2pdf/trunk/+addrelease)
# 8. make ppa
#    name the release -0~ppa1<release>, where release is precise, hardy, etc
#     debuild -S -sa
#     dput gscan2pdf-ppa .changes
#    https://launchpad.net/~jeffreyratcliffe/+archive
#    gscan2pdf-announce@lists.sourceforge.net, gscan2pdf-help@lists.sourceforge.net, gnome-announce-list@gnome.org, sane-devel@lists.alioth.debian.org
#     sudo DIST=sid pbuilder update
#     DIST=sid pdebuild
#    check contents with dpkg-deb --contents
#     dput ftp-master

use warnings;
use strict;
use feature "switch";

use Gscan2pdf::Document;
use Gscan2pdf::Frontend::Sane;
use Gscan2pdf::Frontend::CLI;
use Gscan2pdf::NetPBM;
use Gscan2pdf::Scanner::Options;
use Gscan2pdf::Dialog::Scan::Sane;
use Gscan2pdf::Dialog::Scan::CLI;
use Gscan2pdf::Tesseract;
use Gscan2pdf::Ocropus;
use Gscan2pdf::Cuneiform;
use Gscan2pdf::Unpaper;
use Gscan2pdf::Dialog::Renumber;
use Sane 0.05;    # To get SANE_* enums
use Gtk2::ImageView;
use Goo::Canvas;
use Gtk2::Ex::Simple::List;
use Gtk2 -init;    # Could just call init separately
use Gtk2::Gdk::Keysyms;
use Cwd;               # To obtain current working directory
use File::Basename;    # Split filename into dir, file, ext
use File::Copy;
use File::Temp;        # To create temporary files
use File::Path qw(remove_tree);
use Glib qw(TRUE FALSE);    # To get TRUE and FALSE
use Config::General 2.40;
use PDF::API2;
use Getopt::Long;
use Set::IntSpan 1.10;      # For size method for page numbering issues
use Proc::Killfam;
use Fcntl qw(:flock)
  ;    # import LOCK_* constants to prevent us clobbering running instances
use Log::Log4perl;
use Try::Tiny;
use Data::Dumper;

# To sort out LC_NUMERIC and $SIG{CHLD}
use POSIX qw(locale_h :signal_h :errno_h :sys_wait_h);
use Locale::gettext 1.05;    # For translations

Glib::set_application_name('gscan2pdf');
my $prog_name = Glib::get_application_name;
my $VERSION   = '1.2.3';

# Window parameters
my $border_width = 6;

# Image border to ensure that a scaled to fit image gets no scrollbars
my $border = 1;

# Set up domain for gettext (internationalisation)
# Expects /usr/share/locale/LANGUAGE/LC_MESSAGES/$prog_name.mo
# or whatever is set by $d->dir([NEWDIR]);
my $d      = Locale::gettext->domain($prog_name);
my $d_sane = Locale::gettext->domain('sane-backends');

my $debug = FALSE;
my (
 $test,   $help,        $log,        $log_level, @device,
 $locale, @device_list, $test_image, $logger
);
parse_arguments();

my ( %SETTING, $conf );
my $rc = "$ENV{'HOME'}/.$prog_name";
read_config_file($rc);

set_up_test_mode();

$logger->info("Perl version $^V");
$logger->info("Glib-Perl version $Glib::VERSION");
$logger->info( 'Built for Glib ' . join( '.', Glib->GET_VERSION_INFO ) );
$logger->info( 'Running with Glib '
   . join( '.', Glib::major_version, Glib::minor_version, Glib::micro_version )
);
$logger->info("Gtk2-Perl version $Gtk2::VERSION");
$logger->info( "Built for GTK " . join( '.', Gtk2->GET_VERSION_INFO ) );
$logger->info( "Running with GTK " . join( '.', Gtk2->get_version_info ) );
$logger->info( "Using GtkImageView version ",
 Gtk2::ImageView->library_version );
$logger->info("Using Gtk2::ImageView version $Gtk2::ImageView::VERSION");
$logger->info("Using PDF::API2 version $PDF::API2::VERSION");
$logger->info( 'Using Sane version ' . join( '.', Sane->get_version ) );
$logger->info("Using libsane-perl version $Sane::VERSION");

$logger->debug( Dumper( \%SETTING ) ) if ($debug);

# Just in case dependencies have changed, put put startup warning again
if ( not defined( $SETTING{version} ) or $SETTING{version} ne $VERSION ) {
 $SETTING{'startup warning'} = TRUE;
 delete $SETTING{cache} if ( defined $SETTING{cache} );
}
$SETTING{version} = $VERSION;

# Initialise thread handler
Gscan2pdf::Document->setup($logger);

# Initialise SANE frontends
Gscan2pdf::Frontend::Sane->setup($logger);
Gscan2pdf::Frontend::CLI->setup($logger);

# Update list in Gscan2pdf::Document so that it can be used by get_resolution
Gscan2pdf::Document->set_paper_sizes( $SETTING{Paper} );

# Create icons for rotate buttons
my $IconFactory;
my $iconpath;
if ( -d '/usr/share/gscan2pdf' ) {
 $iconpath = '/usr/share/gscan2pdf';
}
else {
 $iconpath =
   '.';    # FIXME: make this a big cleverer, going one dir down from bin.
}
init_icons(
 [ 'rotate90',  "$iconpath/stock-rotate-90.svg" ],
 [ 'rotate180', "$iconpath/stock-rotate-180.svg" ],
 [ 'rotate270', "$iconpath/stock-rotate-270.svg" ],
 [ 'scanner',   "$iconpath/scanner.svg" ],
 [ 'pdf',       "$iconpath/pdf.svg" ],
 [ 'selection', "$iconpath/stock-selection-all-16.png" ],
);

# Define application-wide variables here so that they can be referenced
# in the menu callbacks
my (
 $slist, $windowi, $windowe, $windows, $windowh, $windowo, $windowrn,
 $windowu,
 @undo_buffer,
 @redo_buffer, @undo_selection, @redo_selection, %dependencies,
 %helperTag,   $menubar,        $toolbar,
 @ocr_engine,  @clipboard,
 $windowr,        $view,    $windowp,
 $print_settings, $windowc, $current_page,

 # start page on scan dialog
 $start,

 # Goo::Canvas for OCR output
 $canvas,

 # List of PageRange widgets
 @prlist,

 # List of user-defined tools
 @user_defined_tools,

 # Spinbuttons for selector on crop dialog
 $sb_selector_x, $sb_selector_y, $sb_selector_w, $sb_selector_h,

 # dir below session dir
 $tmpdir,

 # session dir
 $session,

 # filehandle for session lockfile
 $lockfh,

 # Temp::File object for PDF to be emailed
 # Define here to make sure that it doesn't get deleted until the next email
 # is created or we quit
 $pdf,

 # Simple::List in preferences dialog
 $option_visibility_list,

 # Declare the XML structure
 $uimanager,
);

# Create the window
my $window = create_main_window();

my $main_vbox = Gtk2::VBox->new;
$window->add($main_vbox);

# Create the menu bar
create_menu_bar();
$main_vbox->pack_start( $menubar, FALSE, TRUE,  0 );
$main_vbox->pack_start( $toolbar, FALSE, FALSE, 0 );

my $tooltips = Gtk2::Tooltips->new;
$tooltips->enable;

# HPaned for thumbnails and detail view
my $hpaned = Gtk2::HPaned->new;
$hpaned->set_position( $SETTING{'thumb panel'} );
$main_vbox->pack_start( $hpaned, TRUE, TRUE, 0 );

# Scrolled window for thumbnails
my $scwin_thumbs = Gtk2::ScrolledWindow->new;

# resize = FALSE to stop the panel expanding on being resized (Debian #507032)
$hpaned->pack1( $scwin_thumbs, FALSE, TRUE );
$scwin_thumbs->set_policy( 'automatic', 'automatic' );
$scwin_thumbs->set_shadow_type('etched-in');

# Set up a SimpleList
$slist = Gscan2pdf::Document->new;

my $target_entry = {
 target => 'Glib::Scalar',    # some string representing the drag type
 flags  => 'same-widget',     # Gtk2::TargetFlags
 info   => 1,                 # some app-defined integer identifier
};
$slist->drag_source_set( 'button1-mask', [ 'copy', 'move' ], $target_entry );
$slist->drag_dest_set(
 [ 'motion', 'highlight' ],
 [ 'copy',   'move' ],
 $target_entry
);

$slist->signal_connect(
 'drag-data-get' => sub {
  my ( $tree, $context, $sel ) = @_;
  $sel->set( $sel->target, 8, 'data' );
 }
);

$slist->signal_connect(
 'drag-data-delete' => sub {
  my ( $tree, $context ) = @_;
  my $model = $tree->get_model;
  my @data  = $tree->get_selection->get_selected_rows;

  for ( reverse @data ) {
   my $iter = $model->get_iter($_);
   my $info = $model->get( $iter, 0 );
   $model->remove($iter);
  }

  $tree->get_selection->unselect_all;
 }
);

$slist->signal_connect( 'drag-data-received' => \&drag_data_received_callback );

# Callback for dropped signal.
$slist->signal_connect(
 drag_drop => sub {
  my ( $tree, $context, $x, $y, $when ) = @_;
  if ( my $targ = $context->targets ) {
   $tree->drag_get_data( $context, $targ, $when );
   return TRUE;
  }
  return FALSE;
 }
);

# If dragged below the bottom of the window, scroll it.
$slist->signal_connect( 'drag-motion' => \&drag_motion_callback );

# Set up callback for right mouse clicks.
$slist->signal_connect( button_press_event   => \&handle_clicks );
$slist->signal_connect( button_release_event => \&handle_clicks );

# Set the page number to be editable
$slist->set_column_editable( 0, TRUE );

# Set-up the callback when the page number has been edited.
$slist->{row_changed_signal} = $slist->get_model->signal_connect(
 'row-changed' => sub {
  $slist->get_model->signal_handler_block( $slist->{row_changed_signal} );

  # Sort pages
  $slist->manual_sort_by_column(0);

  # And make sure there are no duplicates
  $slist->renumber;
  $slist->get_model->signal_handler_unblock( $slist->{row_changed_signal} );

  # Update the start spinbutton if necessary
  update_start();
 }
);

$scwin_thumbs->add($slist);

# Notebook for detail view and OCR output
my $vnotebook = Gtk2::Notebook->new;
$hpaned->pack2( $vnotebook, TRUE, TRUE );

# ImageView for detail view
$view = Gtk2::ImageView->new;
my $dragger  = Gtk2::ImageView::Tool::Dragger->new($view);
my $selector = Gtk2::ImageView::Tool::Selector->new($view);
my $painter  = Gtk2::ImageView::Tool::Painter->new($view);
$vnotebook->append_page( $view, $d->get('Image') );

$view->signal_connect(
 button_press_event => sub {
  my ( $widget, $event ) = @_;
  handle_clicks( $widget, $event );
 }
);
$view->signal_connect( button_release_event => \&handle_clicks );
$view->signal_connect(
 'zoom-changed' => sub {
  $canvas->set_scale( $view->get_zoom ) if ( defined $canvas );
 }
);

# Callback if the selection changes
$selector->{selection_changed_signal} = $selector->signal_connect(
 'selection-changed' => sub {
  my $sel = $selector->get_selection;
  if ( defined $sel ) {
   $SETTING{selection} = [ $sel->values ];
   if ( defined $sb_selector_x ) {
    $sb_selector_x->set_value( $SETTING{selection}[0] );
    $sb_selector_y->set_value( $SETTING{selection}[1] );
    $sb_selector_w->set_value( $SETTING{selection}[2] );
    $sb_selector_h->set_value( $SETTING{selection}[3] );
   }
  }
 }
);

# ScrolledWindow for Goo::Canvas for OCR output
my $scwin_buffer = Gtk2::ScrolledWindow->new;
$scwin_buffer->set_policy( 'automatic', 'automatic' );
$scwin_buffer->set_shadow_type('etched-in');
$vnotebook->append_page( $scwin_buffer, $d->get('OCR Output') );

# Set up call back for list selection to update detail view
$slist->{selection_changed_signal} = $slist->get_selection->signal_connect(
 changed => \&selection_changed_callback );

# _after ensures that Editables get first bite
$window->signal_connect_after(
 key_press_event => sub {
  my ( $widget, $event ) = @_;

  # Let the keypress propagate
  return FALSE
    unless ( $event->keyval ==
   $Gtk2::Gdk::Keysyms{Delete} )    ## no critic (ProhibitPackageVars)
    ;

  delete_pages();
  return TRUE;
 }
);

# If defined in the config file, set the current directory
$SETTING{'cwd'} = getcwd unless ( defined( $SETTING{'cwd'} ) );

my $unpaper = Gscan2pdf::Unpaper->new( $SETTING{'unpaper options'} );

update_uimanager();

create_temp_directory();

$window->show_all;

# Progress bars below window
my $phbox = Gtk2::HBox->new;
$main_vbox->pack_end( $phbox, FALSE, FALSE, 0 );
$phbox->show;
my $shbox = Gtk2::HBox->new;
$phbox->add($shbox);
my $spbar = Gtk2::ProgressBar->new;
$shbox->add($spbar);
my $scbutton = Gtk2::Button->new;
$scbutton->set_image( Gtk2::Image->new_from_stock( 'gtk-cancel', 'button' ) );
$shbox->pack_end( $scbutton, FALSE, FALSE, 0 );
my $thbox = Gtk2::HBox->new;
$phbox->add($thbox);
my $tpbar = Gtk2::ProgressBar->new;
$thbox->add($tpbar);
my $tcbutton = Gtk2::Button->new;
$tcbutton->set_image( Gtk2::Image->new_from_stock( 'gtk-cancel', 'button' ) );
$thbox->pack_end( $tcbutton, FALSE, FALSE, 0 );

# Open scan dialog in background
scan_dialog(TRUE) if ( $SETTING{'auto-open-scan-dialog'} );

### Subroutines

sub parse_arguments {
 my @args = (
  'device=s'     => \@device,
  'test=s%'      => \$test,
  'test-image=s' => \$test_image,
  'locale=s'     => \$locale,
  'help'         => \$help,
  'log=s'        => \$log,
  'debug'        => sub { $log_level = 'DEBUG'; $debug = TRUE },
  'info'  => sub { $log_level = 'INFO' },
  'warn'  => sub { $log_level = 'WARN' },
  'error' => sub { $log_level = 'ERROR' },
  'fatal' => sub { $log_level = 'FATAL' },
  'version' => sub { warn "$prog_name $VERSION\n"; exit 0 },
 );
 exit 1 unless ( GetOptions(@args) );
 $Sane::DEBUG = $debug;

 if ( not defined($log_level) ) {
  if ( defined $log ) {
   $log_level = 'DEBUG';
   $debug     = TRUE;
  }
  else {
   $log_level = 'ERROR';
  }
 }
 my $log_conf = <<'EOS';
 log4perl.appender.Screen        = Log::Log4perl::Appender::Screen
 log4perl.appender.Screen.layout = Log::Log4perl::Layout::SimpleLayout
EOS

 if ( defined $log ) {
  $log_conf .= <<"EOS";
 log4perl.appender.Logfile          = Log::Log4perl::Appender::File
 log4perl.appender.Logfile.filename = $log
 log4perl.appender.Logfile.mode     = write
 log4perl.appender.Logfile.layout   = Log::Log4perl::Layout::SimpleLayout
 log4perl.category                  = $log_level, Logfile, Screen
EOS
 }
 else {
  $log_conf .= <<"EOS";
 log4perl.category                  = $log_level, Screen
EOS
 }

 if ( defined $help ) {
  system("perldoc $0") == 0 or die $d->get('Error displaying help'), "\n";
 }

 Log::Log4perl::init( \$log_conf );
 $logger = Log::Log4perl::get_logger();

 $logger->info("Starting $prog_name $VERSION");
 $logger->info("Log level $log_level");

 if ( defined $locale ) {
  if ( $locale =~ /^\//x ) {
   $d->dir($locale);
  }
  else {
   $d->dir( getcwd . "/$locale" );
  }
 }

 for (@device) {
  push @device_list, { name => $_, label => $_ };
 }

 # Set LC_NUMERIC to C to prevent decimal commas (or anything else) confusing
 # scanimage
 setlocale( LC_NUMERIC, "C" );
 $logger->info( "Using ", setlocale(LC_CTYPE), " locale" );
 $logger->info( "Startup LC_NUMERIC ", setlocale(LC_NUMERIC) );
 return;
}

# Perl-Critic is confused by @_ in finally{} See P::C bug #79138
sub read_config_file {    ## no critic (RequireArgUnpacking)
 my ($filename) = @_;
 $logger->info("Reading config from $filename");
 system("touch $filename") unless ( -r $filename );
 try {
  $conf = Config::General->new(
   -ConfigFile  => $filename,
   -SplitPolicy => 'equalsign',
   -UTF8        => 1,
  );
 }
 catch {
  $logger->error(
   $d->get(
"Error: unable to load settings.\nBacking up settings\nReverting to defaults"
   )
  );
  move( $filename, "$filename.old" );
 }
 finally {
  %SETTING = $conf->getall unless (@_);
 };

 my %default_settings = (
  'window_width'      => 800,
  'window_height'     => 600,
  'window_maximize'   => TRUE,
  'thumb panel'       => 100,
  'Page range'        => 'all',
  'layout'            => 'single',
  'downsample'        => FALSE,
  'downsample dpi'    => 150,
  'threshold tool'    => 80,
  'unsharp radius'    => 0,
  'unsharp sigma'     => 1,
  'unsharp amount'    => 1,
  'unsharp threshold' => 0.05,
  'cache options'     => TRUE,
  'restore window'    => TRUE,
  'startup warning'   => FALSE,
  'date offset'       => 0,
  'pdf compression'   => 'auto',
  'quality'           => 75,
  'pages to scan'     => 1,
  'unpaper on scan'   => TRUE,
  'OCR on scan'       => TRUE,
  'frontend'          => 'libsane-perl',
  'rotate facing'     => 0,
  'rotate reverse'    => 0,
  'default filename'  => '%a %y-%m-%d',
  'scan prefix'       => '',
  'Blank threshold' => 0.005,       # Blank page standard deviation threshold
  'Dark threshold'  => 0.12,        # Dark page mean threshold
  'ocr engine'      => 'ocropus',
  'OCR output' =>
    'replace',    # When a page is re-OCRed, replace old text with new text
  'auto-open-scan-dialog' => TRUE,
  'Paper'                 => {
   $d->get('A4') => {
    x => 210,
    y => 297,
    l => 0,
    t => 0,
   },
   $d->get('US Letter') => {
    x => 216,
    y => 279,
    l => 0,
    t => 0,
   },
   $d->get('US Legal') => {
    x => 216,
    y => 356,
    l => 0,
    t => 0,
   },
  },
  user_defined_tools => ['gimp %i'],

  # show the options marked with 1, hide those with 0
  # for the others, see the value of default-option-visibility
  'visible-scan-options' => {
   mode                => 1,
   compression         => 1,
   resolution          => 1,
   brightness          => 1,
   gain                => 1,
   contrast            => 1,
   threshold           => 1,
   speed               => 1,
   'batch-scan'        => 1,
   'wait-for-button'   => 1,
   'button-wait'       => 1,
   'calibration-cache' => 1,
   source              => 1,
   pagewidth           => 1,
   pageheight          => 1,
   'page-width'        => 1,
   'page-height'       => 1,
   'overscan-top'      => 1,
   'overscan-bottom'   => 1,
   adf_mode            => 1,
   'adf-mode'          => 1,
   'Paper size'        => 1,
   x                   => 1,
   y                   => 1,
   l                   => 1,
   t                   => 1,
  },
  'scan-reload-triggers' => qw(mode),
 );
 delete $SETTING{frontend}
   if (
  defined $SETTING{frontend}
  and ( $SETTING{frontend} eq 'scanimage-perl'
   or $SETTING{frontend} eq 'scanadf-perl'
   or $SETTING{frontend} eq 'scanimage.pl'
   or $SETTING{frontend} eq
   'scanadf.pl' )    # these frontends are just for testing purposes
   );
 for ( keys(%default_settings) ) {
  $SETTING{$_} = $default_settings{$_} unless ( defined $SETTING{$_} );
 }

 remove_invalid_paper( $SETTING{Paper} );

 # Delete the options cache if there is a new version of SANE
 if (
  (
   defined( $SETTING{'SANE version'} )
   and $SETTING{'SANE version'} ne join( '.', Sane->get_version )
  )
  or ( defined( $SETTING{'libsane-perl version'} )
   and $SETTING{'libsane-perl version'} ne $Sane::VERSION )
   )
 {
  delete $SETTING{cache} if ( defined $SETTING{cache} );
 }
 $SETTING{'SANE version'} = join( '.', Sane->get_version );
 $SETTING{'libsane-perl version'} = $Sane::VERSION;
 return;
}

sub remove_invalid_paper {
 my ($hashref) = @_;
 for my $paper ( keys %$hashref ) {
  if ( $paper eq '<>' or $paper eq '</>' ) {
   delete $hashref->{$paper};
  }
  else {
   for (qw(x y t l)) {
    unless ( ref( $hashref->{$paper} ) eq "HASH"
     and defined( $hashref->{$paper}{$_} ) )
    {
     delete $hashref->{$paper};
     last;
    }
   }
  }
 }
 return;
}

sub set_up_test_mode {

 # Set up test mode and make sure file has absolute path and is readable
 if ( keys %{$test} ) {
  $SETTING{frontend} = 'scanimage-perl';
  for my $file ( keys %{$test} ) {
   my $device = $test->{$file};
   delete $test->{$file};

   # Find a way of emulating the nonsense \n that some people seem to get
   $device =~ s/\\n/\n/g;
   $logger->debug("'$file','$device'");
   $file = getcwd . "/$file" if ( $file !~ /^\//x );
   if ( !-r $file ) {
    $logger->fatal( sprintf( $d->get("Cannot read file: %s"), $file ) );
    exit 1;
   }
   push @{ $test->{file} }, $file;
   $test->{output} = '' unless ( defined( $test->{output} ) );
   $test->{output} .=
     "'$#{$test->{file}}','$device','" . basename($file) . "'\n";
  }
 }

 # GetOptions leaves $test as a reference to an empty hash.
 else {
  undef $test;
 }

 if ( defined $test_image ) {
  $test_image = expand_tildes($test_image);
  if ( -r $test_image ) {
   $logger->info("Using test image $test_image");
  }
  else {
   $logger->fatal( sprintf( $d->get("Cannot read file: %s"), $test_image ) );
   exit 1;
  }
 }
 return;
}

sub create_main_window {
 my $widget = Gtk2::Window->new;
 $widget->set_title("$prog_name v$VERSION");
 $widget->signal_connect(
  'delete-event' => sub {
   if ( quit() ) {
    Gtk2->main_quit;
   }
   else {
    return TRUE;
   }
  }
 );

 # Note when the window is maximised or not.
 $widget->signal_connect(
  window_state_event => sub {
   my ( $w, $event ) = @_;
   if ( $event->new_window_state & ['maximized'] ) {
    $SETTING{'window_maximize'} = TRUE;
   }
   else {
    $SETTING{'window_maximize'} = FALSE;
   }
  }
 );

 # If defined in the config file, set the window state, size and position
 if ( $SETTING{'restore window'} ) {
  $widget->set_default_size( $SETTING{'window_width'},
   $SETTING{'window_height'} );
  $widget->move( $SETTING{'window_x'}, $SETTING{'window_y'} )
    if ( defined( $SETTING{'window_x'} ) and defined( $SETTING{'window_y'} ) );
  $widget->maximize if ( $SETTING{'window_maximize'} );
 }

 try { $widget->set_default_icon_from_file("$iconpath/gscan2pdf.svg"); }
 catch {
  $logger->warn("Unable to load icon `$iconpath/gscan2pdf.svg': $@");
 };

 return $widget;
}

# Check for presence of various packages

sub check_dependencies {
 $dependencies{perlmagick}  = try { require Image::Magick };
 $dependencies{imagemagick} = Gscan2pdf::Document::check_command('convert');
 $dependencies{scanadf}     = Gscan2pdf::Document::check_command('scanadf');
 $dependencies{xdg}         = Gscan2pdf::Document::check_command('xdg-email');
 $dependencies{gocr}        = Gscan2pdf::Document::check_command('gocr');
 $dependencies{tesseract}   = Gscan2pdf::Tesseract->setup($logger);
 $dependencies{ocropus}     = Gscan2pdf::Ocropus->setup($logger);
 $dependencies{cuneiform}   = Gscan2pdf::Cuneiform->setup($logger);
 $dependencies{djvu}        = Gscan2pdf::Document::check_command('cjb2');
 $dependencies{unpaper}     = Gscan2pdf::Unpaper->version;
 $dependencies{libtiff}     = Gscan2pdf::Document::check_command('tiffcp');
 $logger->info("Found Image::Magick") if ( $dependencies{perlmagick} );
 $logger->info("Found ImageMagick")   if ( $dependencies{imagemagick} );
 $logger->info("Found scanadf")       if ( $dependencies{scanadf} );
 $logger->info("Found xdg-email")     if ( $dependencies{xdg} );
 $logger->info("Found gocr")          if ( $dependencies{gocr} );
 $logger->info("Found tesseract")     if ( $dependencies{tesseract} );
 $logger->info("Found cuneiform v$dependencies{cuneiform}")
   if ( $dependencies{cuneiform} );
 $logger->info("Found cjb2 (djvu)") if ( $dependencies{djvu} );
 $logger->info("Found libtiff")     if ( $dependencies{libtiff} );
 $logger->info("Found unpaper $dependencies{unpaper}")
   if ( $dependencies{unpaper} );

 # OCR engine options
 push @ocr_engine,
   [ 'gocr', $d->get('GOCR'), $d->get('Process image with GOCR.') ]
   if ( $dependencies{gocr} );
 push @ocr_engine,
   [
  'tesseract', $d->get('Tesseract'),
  $d->get('Process image with Tesseract.')
   ],
   if ( $dependencies{tesseract} );
 push @ocr_engine,
   [ 'ocropus', $d->get('Ocropus'), $d->get('Process image with Ocropus.') ],
   if ( $dependencies{ocropus} );
 push @ocr_engine,
   [
  'cuneiform', $d->get('Cuneiform'),
  $d->get('Process image with Cuneiform.')
   ],
   if ( $dependencies{cuneiform} );
 return;
}

# Create the menu bar, initialize its menus, and return the menu bar.

sub create_menu_bar {

 # Create a Gtk2::UIManager instance
 $uimanager = Gtk2::UIManager->new;

 # extract the accelgroup and add it to the window
 my $accelgroup = $uimanager->get_accel_group;
 $window->add_accel_group($accelgroup);

 my @action_items = (

  # Fields for each action item:
  # [name, stock_id, value, label, accelerator, tooltip, callback]

  # File menu
  [ 'File', undef, $d->get('_File') ],
  [
   'New',                       'gtk-new',
   $d->get('_New'),             '<control>n',
   $d->get('Clears all pages'), \&new
  ],
  [
   'Open',                        'gtk-open',
   $d->get('_Open'),              '<control>o',
   $d->get('Open image file(s)'), \&open_dialog
  ],
  [
   'Open crashed session',           undef,
   $d->get('Open c_rashed session'), undef,
   $d->get('Open crashed session'),  \&open_session_action
  ],
  [
   'Scan',                   'scanner',
   $d->get('S_can'),         '<control>g',
   $d->get('Scan document'), \&scan_dialog
  ],
  [
   'Save',          'gtk-save', $d->get('Save'), '<control>s',
   $d->get('Save'), \&save_dialog
  ],
  [
   'Email as PDF',                          'gtk-edit',
   $d->get('_Email as PDF'),                '<control>e',
   $d->get('Attach as PDF to a new email'), \&email
  ],
  [
   'Print',          'gtk-print', $d->get('_Print'), '<control>p',
   $d->get('Print'), \&print_dialog
  ],
  [
   'Compress',                           undef,
   $d->get('_Compress temporary files'), undef,
   $d->get('Compress temporary files'),  \&compress_temp
  ],
  [
   'Quit',           'gtk-quit',
   $d->get('_Quit'), '<control>q',
   $d->get('Quit'), sub { Gtk2->main_quit if quit(); }
  ],

  # Edit menu
  [ 'Edit', undef, $d->get('_Edit') ],
  [
   'Undo',          'gtk-undo', $d->get('_Undo'), '<control>z',
   $d->get('Undo'), \&undo
  ],
  [
   'Redo',           'gtk-redo',
   $d->get('_Redo'), '<shift><control>z',
   $d->get('Redo'),  \&unundo
  ],
  [
   'Cut',                    'gtk-cut',
   $d->get('Cu_t'),          '<control>x',
   $d->get('Cut selection'), \&cut_selection
  ],
  [
   'Copy',                    'gtk-copy',
   $d->get('_Copy'),          '<control>c',
   $d->get('Copy selection'), \&copy_selection
  ],
  [
   'Paste',                    'gtk-paste',
   $d->get('_Paste'),          '<control>v',
   $d->get('Paste selection'), \&paste_selection
  ],
  [
   'Delete',                         'gtk-delete',
   $d->get('_Delete'),               undef,
   $d->get('Delete selected pages'), \&delete_pages
  ],
  [
   'Renumber',                'gtk-sort-ascending',
   $d->get('_Renumber'),      '<control>r',
   $d->get('Renumber pages'), \&renumber_dialog
  ],
  [ 'Select', undef, $d->get('_Select') ],
  [
   'Select All',                'gtk-select-all',
   $d->get('_All'),             '<control>a',
   $d->get('Select all pages'), \&select_all
  ],
  [
   'Select Odd', undef, $d->get('_Odd'), '<control>1',
   $d->get('Select all odd-numbered pages'),
   sub { select_odd_even(0); }
  ],
  [
   'Select Even', undef, $d->get('_Even'), '<control>2',
   $d->get('Select all evenly-numbered pages'),
   sub { select_odd_even(1); }
  ],
  [
   'Select Blank', 'gtk-select-blank', $d->get('_Blank'), '<control>b',
   $d->get('Select pages with low standard deviation'),
   \&analyse_select_blank
  ],
  [
   'Select Dark',                'gtk-select-blank',
   $d->get('_Dark'),             '<control>d',
   $d->get('Select dark pages'), \&analyse_select_dark
  ],
  [
   'Select Modified',
   'gtk-select-modified',
   $d->get('_Modified'),
   '<control>m',
   $d->get('Select modified pages since last OCR'),
   \&select_modified_since_ocr
  ],
  [
   'Select No OCR',                            undef,
   $d->get('_No OCR'),                         undef,
   $d->get('Select pages with no OCR output'), \&select_no_ocr
  ],
  [
   'Clear OCR',                                     'gtk-clear',
   $d->get('_Clear OCR'),                           undef,
   $d->get('Clear OCR output from selected pages'), \&clear_ocr
  ],
  [
   'Properties',                     'gtk-properties',
   $d->get('Propert_ies'),           undef,
   $d->get('Edit image properties'), \&properties
  ],
  [
   'Preferences',               'gtk-preferences',
   $d->get('Prefere_nces'),     undef,
   $d->get('Edit preferences'), \&preferences
  ],

  # View menu
  [ 'View', undef, $d->get('_View') ],
  [
   'Zoom 100',            'gtk-zoom-100',
   $d->get('Zoom _100%'), undef,
   $d->get('Zoom to 100%'), sub { $view->set_zoom(1.0); }
  ],
  [
   'Zoom to fit',           'gtk-zoom-fit',
   $d->get('Zoom to _fit'), undef,
   $d->get('Zoom to fit'), sub { $view->set_fitting(TRUE); }
  ],
  [
   'Zoom in',           'gtk-zoom-in',
   $d->get('Zoom _in'), 'plus',
   $d->get('Zoom in'), sub { $view->zoom_in; }
  ],
  [
   'Zoom out',           'gtk-zoom-out',
   $d->get('Zoom _out'), 'minus',
   $d->get('Zoom out'), sub { $view->zoom_out; }
  ],
  [
   'Rotate 90',
   'rotate90',
   $d->get('Rotate 90 clockwise'),
   '<control><shift>R',
   $d->get('Rotate 90 clockwise'),
   sub { rotate( 90, [ indices2pages( $slist->get_selected_indices ) ] ) }
  ],
  [
   'Rotate 180',
   'rotate180',
   $d->get('Rotate 180'),
   '<control><shift>F',
   $d->get('Rotate 180'),
   sub { rotate( 180, [ indices2pages( $slist->get_selected_indices ) ] ) }
  ],
  [
   'Rotate 270',
   'rotate270',
   $d->get('Rotate 90 anticlockwise'),
   '<control><shift>C',
   $d->get('Rotate 90 anticlockwise'),
   sub { rotate( 270, [ indices2pages( $slist->get_selected_indices ) ] ) }
  ],

  # Tools menu
  [ 'Tools', undef, $d->get('_Tools') ],
  [
   'Threshold', undef, $d->get('_Threshold'),
   undef, $d->get('Change each pixel above this threshold to black'),
   \&threshold
  ],
  [
   'Negate', undef, $d->get('_Negate'), undef,
   $d->get('Converts black to white and vice versa'), \&negate
  ],
  [
   'Unsharp',                        undef,
   $d->get('_Unsharp Mask'),         undef,
   $d->get('Apply an unsharp mask'), \&unsharp
  ],
  [
   'Crop', 'GTK_STOCK_LEAVE_FULLSCREEN', $d->get('_Crop'), undef,
   $d->get('Crop pages'), \&crop
  ],
  [
   'unpaper',                                       undef,
   $d->get('_Clean up'),                            undef,
   $d->get('Clean up scanned images with unpaper'), \&unpaper
  ],
  [
   'OCR', undef, $d->get('_OCR'), undef,
   $d->get('Optical Character Recognition'), \&OCR
  ],
  [ 'User-defined', undef, $d->get('U_ser-defined') ],

  # Help menu
  [ 'Help menu', undef, $d->get('_Help') ],
  [
   'Help',          'gtk-help', $d->get('_Help'), '<control>h',
   $d->get('Help'), \&view_pod
  ],
  [
   'About', 'gtk-about', $d->get('_About'), undef, $d->get('_About'), \&about
  ],
 );

 my @image_tools = (
  [
   'DraggerTool',                'gtk-refresh',
   $d->get('_Drag'),             undef,
   $d->get('Use the hand tool'), 10
  ],
  [
   'SelectorTool',                                'selection',
   $d->get('_Select'),                            undef,
   $d->get('Use the rectangular selection tool'), 20
  ],
  [
   'PainterTool',                   'gtk-media-play',
   $d->get('_Paint'),               undef,
   $d->get('Use the painter tool'), 30
  ]
 );

 my $ui = <<'EOS';
<ui>
 <menubar name='MenuBar'>
  <menu action='File'>
   <menuitem action='New'/>
   <menuitem action='Open'/>
   <menuitem action='Open crashed session'/>
   <menuitem action='Scan'/>
   <menuitem action='Save'/>
   <menuitem action='Email as PDF'/>
   <menuitem action='Print'/>
   <separator/>
   <menuitem action='Compress'/>
   <separator/>
   <menuitem action='Quit'/>
  </menu>
  <menu action='Edit'>
   <menuitem action='Undo'/>
   <menuitem action='Redo'/>
   <separator/>
   <menuitem action='Cut'/>
   <menuitem action='Copy'/>
   <menuitem action='Paste'/>
   <menuitem action='Delete'/>
   <separator/>
   <menuitem action='Renumber'/>
   <menu action='Select'>
    <menuitem action='Select All'/>
    <menuitem action='Select Odd'/>
    <menuitem action='Select Even'/>
    <menuitem action='Select Blank'/>
    <menuitem action='Select Dark'/>
    <menuitem action='Select Modified'/>
    <menuitem action='Select No OCR'/>
   </menu>
   <menuitem action='Clear OCR'/>
   <separator/>
   <menuitem action='Properties'/>
   <separator/>
   <menuitem action='Preferences'/>
  </menu>
  <menu action='View'>
   <menuitem action = 'DraggerTool'/>
   <menuitem action = 'SelectorTool'/>
   <separator/>
   <menuitem action='Zoom 100'/>
   <menuitem action='Zoom to fit'/>
   <menuitem action='Zoom in'/>
   <menuitem action='Zoom out'/>
   <separator/>
   <menuitem action='Rotate 90'/>
   <menuitem action='Rotate 180'/>
   <menuitem action='Rotate 270'/>
  </menu>
  <menu action='Tools'>
   <menuitem action='Threshold'/>
   <menuitem action='Negate'/>
   <menuitem action='Unsharp'/>
   <menuitem action='Crop'/>
   <separator/>
   <menuitem action='unpaper'/>
   <menuitem action='OCR'/>
   <separator/>
   <menu action='User-defined'>
   </menu>
  </menu>
  <menu action='Help menu'>
   <menuitem action='Help'/>
   <menuitem action='About'/>
  </menu>
 </menubar>
 <toolbar name='ToolBar'>
  <toolitem action='New'/>
  <toolitem action='Open'/>
  <toolitem action='Scan'/>
  <toolitem action='Save'/>
  <toolitem action='Email as PDF'/>
  <toolitem action='Print'/>
  <separator/>
  <toolitem action='Undo'/>
  <toolitem action='Redo'/>
  <separator/>
  <toolitem action='Cut'/>
  <toolitem action='Copy'/>
  <toolitem action='Paste'/>
  <toolitem action='Delete'/>
  <separator/>
  <toolitem action='Renumber'/>
  <toolitem action='Select All'/>
  <separator/>
  <toolitem action = 'DraggerTool'/>
  <toolitem action = 'SelectorTool'/>
  <separator/>
  <toolitem action='Zoom 100'/>
  <toolitem action='Zoom to fit'/>
  <toolitem action='Zoom in'/>
  <toolitem action='Zoom out'/>
  <separator/>
  <toolitem action='Rotate 90'/>
  <toolitem action='Rotate 180'/>
  <toolitem action='Rotate 270'/>
  <separator/>
  <toolitem action='Help'/>
  <toolitem action='Quit'/>
 </toolbar>
 <popup name='Detail_Popup'>
  <menuitem action = 'DraggerTool'/>
  <menuitem action = 'SelectorTool'/>
  <separator/>
  <menuitem action='Zoom 100'/>
  <menuitem action='Zoom to fit'/>
  <menuitem action='Zoom in'/>
  <menuitem action='Zoom out'/>
  <separator/>
  <menuitem action='Rotate 90'/>
  <menuitem action='Rotate 180'/>
  <menuitem action='Rotate 270'/>
  <separator/>
  <menuitem action='Cut'/>
  <menuitem action='Copy'/>
  <menuitem action='Paste'/>
  <menuitem action='Delete'/>
  <separator/>
  <menuitem action='Properties'/>
 </popup>
 <popup name='Thumb_Popup'>
  <menuitem action='Save'/>
  <menuitem action='Email as PDF'/>
  <menuitem action='Print'/>
  <separator/>
  <menuitem action='Renumber'/>
  <menuitem action='Select All'/>
  <menuitem action='Select Odd'/>
  <menuitem action='Select Even'/>
  <separator/>
  <menuitem action='Rotate 90'/>
  <menuitem action='Rotate 180'/>
  <menuitem action='Rotate 270'/>
  <separator/>
  <menuitem action='Cut'/>
  <menuitem action='Copy'/>
  <menuitem action='Paste'/>
  <menuitem action='Delete'/>
  <separator/>
  <menuitem action='Clear OCR'/>
  <separator/>
  <menuitem action='Properties'/>
 </popup>
</ui>
EOS

 # Create the basic Gtk2::ActionGroup instance
 # and fill it with Gtk2::Action instances
 my $actions_basic = Gtk2::ActionGroup->new("actions_basic");
 $actions_basic->add_actions( \@action_items, undef );
 $actions_basic->add_radio_actions( \@image_tools, 10, \&change_image_tool_cb );

 # Add the actiongroup to the uimanager
 $uimanager->insert_action_group( $actions_basic, 0 );

 # add the basic XML description of the GUI
 $uimanager->add_ui_from_string($ui);

 # extract the menubar
 $menubar = $uimanager->get_widget('/MenuBar');

 $SETTING{user_defined_tools} = [ $SETTING{user_defined_tools} ]
   if ( ref( $SETTING{user_defined_tools} ) ne 'ARRAY' );
 for ( @{ $SETTING{user_defined_tools} } ) {
  my %tool = ( cmd => $_ );
  push @user_defined_tools, \%tool;
  add_user_defined_tool_menu( \%tool );
 }

 # Check for presence of various packages
 check_dependencies();

 # Ghost save image item if imagemagick not available
 my $msg = '';
 $msg .= $d->get("Save image and Save as PDF both require imagemagick\n")
   unless ( $dependencies{imagemagick} );

 # Ghost save image item if libtiff not available
 $msg .= $d->get("Save image requires libtiff\n")
   unless ( $dependencies{libtiff} );

 # Ghost djvu item if cjb2 not available
 $msg .= $d->get("Save as DjVu requires djvulibre-bin\n")
   unless ( $dependencies{djvu} );

 # Ghost email item if xdg-email not available
 $msg .= $d->get("Email as PDF requires xdg-email\n")
   unless ( $dependencies{xdg} );

 # Undo/redo start off ghosted anyway-
 $uimanager->get_widget('/MenuBar/Edit/Undo')->set_sensitive(FALSE);
 $uimanager->get_widget('/MenuBar/Edit/Redo')->set_sensitive(FALSE);
 $uimanager->get_widget('/ToolBar/Undo')->set_sensitive(FALSE);
 $uimanager->get_widget('/ToolBar/Redo')->set_sensitive(FALSE);

 # save * start off ghosted anyway-
 $uimanager->get_widget('/MenuBar/File/Save')->set_sensitive(FALSE);
 $uimanager->get_widget('/MenuBar/File/Email as PDF')->set_sensitive(FALSE);
 $uimanager->get_widget('/MenuBar/File/Print')->set_sensitive(FALSE);
 $uimanager->get_widget('/ToolBar/Save')->set_sensitive(FALSE);
 $uimanager->get_widget('/ToolBar/Email as PDF')->set_sensitive(FALSE);
 $uimanager->get_widget('/ToolBar/Print')->set_sensitive(FALSE);
 $uimanager->get_widget('/Thumb_Popup/Save')->set_sensitive(FALSE);
 $uimanager->get_widget('/Thumb_Popup/Email as PDF')->set_sensitive(FALSE);
 $uimanager->get_widget('/Thumb_Popup/Print')->set_sensitive(FALSE);

 $uimanager->get_widget('/MenuBar/Tools/Threshold')->set_sensitive(FALSE);
 $uimanager->get_widget('/MenuBar/Tools/Negate')->set_sensitive(FALSE);
 $uimanager->get_widget('/MenuBar/Tools/Unsharp')->set_sensitive(FALSE);
 $uimanager->get_widget('/MenuBar/Tools/Crop')->set_sensitive(FALSE);
 $uimanager->get_widget('/MenuBar/Tools/User-defined')->set_sensitive(FALSE);

 # a convenient place to put these
 $dependencies{pages} = -1;

 # Ghost rotations and unpaper if perlmagick not available
 unless ( $dependencies{perlmagick} ) {
  $uimanager->get_widget('/MenuBar/View/Rotate 90')->set_sensitive(FALSE);
  $uimanager->get_widget('/MenuBar/View/Rotate 180')->set_sensitive(FALSE);
  $uimanager->get_widget('/MenuBar/View/Rotate 270')->set_sensitive(FALSE);
  $uimanager->get_widget('/ToolBar/Rotate 90')->set_sensitive(FALSE);
  $uimanager->get_widget('/ToolBar/Rotate 180')->set_sensitive(FALSE);
  $uimanager->get_widget('/ToolBar/Rotate 270')->set_sensitive(FALSE);
  $uimanager->get_widget('/Detail_Popup/Rotate 90')->set_sensitive(FALSE);
  $uimanager->get_widget('/Detail_Popup/Rotate 180')->set_sensitive(FALSE);
  $uimanager->get_widget('/Detail_Popup/Rotate 270')->set_sensitive(FALSE);
  $uimanager->get_widget('/Thumb_Popup/Rotate 90')->set_sensitive(FALSE);
  $uimanager->get_widget('/Thumb_Popup/Rotate 180')->set_sensitive(FALSE);
  $uimanager->get_widget('/Thumb_Popup/Rotate 270')->set_sensitive(FALSE);
  $uimanager->get_widget('/MenuBar/Tools/unpaper')->set_sensitive(FALSE);
  $msg .=
    $d->get("The rotating options and unpaper support require perlmagick\n");
 }

 # Ghost unpaper item if unpaper not available
 unless ( $dependencies{unpaper} ) {
  $uimanager->get_widget('/MenuBar/Tools/unpaper')->set_sensitive(FALSE);
  $msg .= $d->get("unpaper missing\n");
 }

 # Ghost ocr item if gocr and tesseract not available
 if ( not $dependencies{gocr}
  and not $dependencies{tesseract}
  and not $dependencies{ocropus}
  and not $dependencies{cuneiform} )
 {
  $uimanager->get_widget('/MenuBar/Tools/OCR')->set_sensitive(FALSE);
  $msg .= $d->get("OCR requires gocr, tesseract, ocropus, or cuneiform\n");
 }

 # Put up warning if needed
 if ( $SETTING{'startup warning'} and $msg ne '' ) {
  my $dialog = Gtk2::Dialog->new( $d->get('Warning: missing packages'),
   $window, 'modal', 'gtk-ok' => 'none' );
  my $label = Gtk2::Label->new($msg);
  $dialog->vbox->add($label);
  my $cb = Gtk2::CheckButton->new_with_label(
   $d->get("Don't show this message again") );
  $cb->set_active(TRUE);
  $dialog->vbox->add($cb);
  $dialog->show_all;
  $dialog->run;
  $SETTING{'startup warning'} = FALSE if ( $cb->get_active );
  $dialog->destroy;
 }

 # extract the toolbar
 $toolbar = $uimanager->get_widget('/ToolBar');

 # turn off labels
 my $settings = $toolbar->get_settings();
 $settings->set( 'gtk-toolbar-style', 'icons' );    # only icons

 return;
}

# ghost or unghost as necessary as # pages > 0 or not.

sub update_uimanager {
 my @widgets = (
  '/MenuBar/View/DraggerTool',
  '/MenuBar/View/SelectorTool',
  '/MenuBar/View/Zoom 100',
  '/MenuBar/View/Zoom to fit',
  '/MenuBar/View/Zoom in',
  '/MenuBar/View/Zoom out',
  '/MenuBar/View/Rotate 90',
  '/MenuBar/View/Rotate 180',
  '/MenuBar/View/Rotate 270',
  '/MenuBar/Tools/Threshold',
  '/MenuBar/Tools/Negate',
  '/MenuBar/Tools/Unsharp',
  '/MenuBar/Tools/Crop',
  '/MenuBar/Tools/unpaper',
  '/MenuBar/Tools/OCR',
  '/MenuBar/Tools/User-defined',

  '/ToolBar/DraggerTool',
  '/ToolBar/SelectorTool',
  '/ToolBar/Zoom 100',
  '/ToolBar/Zoom to fit',
  '/ToolBar/Zoom in',
  '/ToolBar/Zoom out',
  '/ToolBar/Rotate 90',
  '/ToolBar/Rotate 180',
  '/ToolBar/Rotate 270',

  '/Detail_Popup/DraggerTool',
  '/Detail_Popup/SelectorTool',
  '/Detail_Popup/Zoom 100',
  '/Detail_Popup/Zoom to fit',
  '/Detail_Popup/Zoom in',
  '/Detail_Popup/Zoom out',
  '/Detail_Popup/Rotate 90',
  '/Detail_Popup/Rotate 180',
  '/Detail_Popup/Rotate 270',

  '/Thumb_Popup/Rotate 90',
  '/Thumb_Popup/Rotate 180',
  '/Thumb_Popup/Rotate 270',
 );

 if ( $slist->get_selected_indices ) {
  foreach (@widgets) {
   $uimanager->get_widget($_)->set_sensitive(TRUE);
  }
 }
 else {
  foreach (@widgets) {
   $uimanager->get_widget($_)->set_sensitive(FALSE);
  }
 }
 if ( $#{ $slist->{data} } > -1 ) {
  if ( $dependencies{pages} == -1 ) {
   if ( $dependencies{xdg} ) {
    $uimanager->get_widget('/MenuBar/File/Email as PDF')->set_sensitive(TRUE);
    $uimanager->get_widget('/ToolBar/Email as PDF')->set_sensitive(TRUE);
    $uimanager->get_widget('/Thumb_Popup/Email as PDF')->set_sensitive(TRUE);
   }
   if ( $dependencies{imagemagick} and $dependencies{libtiff} ) {
    $uimanager->get_widget('/MenuBar/File/Save')->set_sensitive(TRUE);
    $uimanager->get_widget('/ToolBar/Save')->set_sensitive(TRUE);
    $uimanager->get_widget('/Thumb_Popup/Save')->set_sensitive(TRUE);
   }
   $uimanager->get_widget('/MenuBar/File/Print')->set_sensitive(TRUE);
   $uimanager->get_widget('/ToolBar/Print')->set_sensitive(TRUE);
   $uimanager->get_widget('/Thumb_Popup/Print')->set_sensitive(TRUE);

   $dependencies{pages} = $#{ $slist->{data} };
  }
 }
 else {
  if ( $dependencies{pages} > -1 ) {
   if ( $dependencies{xdg} ) {
    $uimanager->get_widget('/MenuBar/File/Email as PDF')->set_sensitive(FALSE);
    $uimanager->get_widget('/ToolBar/Email as PDF')->set_sensitive(FALSE);
    $uimanager->get_widget('/Thumb_Popup/Email as PDF')->set_sensitive(FALSE);
    $windowe->hide if defined $windowe;
   }
   if ( $dependencies{imagemagick} and $dependencies{libtiff} ) {
    $uimanager->get_widget('/MenuBar/File/Save')->set_sensitive(FALSE);
    $uimanager->get_widget('/ToolBar/Save')->set_sensitive(FALSE);
    $uimanager->get_widget('/Thumb_Popup/Save')->set_sensitive(FALSE);
   }
   $uimanager->get_widget('/MenuBar/File/Print')->set_sensitive(FALSE);
   $uimanager->get_widget('/ToolBar/Print')->set_sensitive(FALSE);
   $uimanager->get_widget('/Thumb_Popup/Print')->set_sensitive(FALSE);
   $windowi->hide if defined $windowi;

   $dependencies{pages} = $#{ $slist->{data} };
  }
 }

 # If the scan dialog has already been drawn, update the start page spinbutton
 update_start();
 return;
}

sub drag_data_received_callback {
 my ( $tree, $context, $x, $y, $sel ) = @_;
 my ( $path, $how ) = $tree->get_dest_row_at_pos( $x, $y );
 my $model = $tree->get_model;
 my $data = $sel->data or return;
 my $delete =
   $context->action == 'move';    ## no critic (ProhibitMismatchedOperators)

 my @rows = $tree->get_selection->get_selected_rows or return;
 my @data;
 for (@rows) {
  my $iter = $model->get_iter($_);
  my @info = $model->get($iter);
  my $suffix;
  $suffix = $1 if ( $info[2]->{filename} =~ /(\.\w*)$/x );
  my $new = File::Temp->new( DIR => $SETTING{session}, SUFFIX => $suffix );

  # quotes required to prevent File::Temp objects from being clobbered
  copy( "$info[2]->{filename}", "$new" )
    or show_message_dialog( $window, 'error', 'close',
   $d->get('Error copying page') );

  $info[2]->{filename} = $new;
  push @data, [@info];
 }

 # Block row-changed signal so that the list can be updated before the sort
 # takes over.
 $tree->get_model->signal_handler_block( $tree->{row_changed_signal} );

 if ($path) {
  if ( $how eq 'after' or $how eq 'into-or-after' ) {
   splice @{ $tree->{data} }, $path->to_string + 1, 0, @data;
  }
  else {
   splice @{ $tree->{data} }, $path->to_string, 0, @data;
  }
 }
 else {
  push @{ $tree->{data} }, @data;
 }
 $tree->renumber;
 $tree->get_model->signal_handler_unblock( $tree->{row_changed_signal} );

 # Update the start spinbutton if necessary
 update_start();

 $context->finish( 1, $delete, time );
 return;
}

sub selection_changed_callback {
 my @page = $slist->get_selected_indices;

 # Display the new image
 $scwin_buffer->remove($_) for ( $scwin_buffer->get_children );
 if (@page) {

  my $path = Gtk2::TreePath->new_from_indices( $page[0] );
  $slist->scroll_to_cell($path);
  my $sel = $selector->get_selection;
  display_image( $slist->{data}[ $page[0] ][2] );
  $selector->set_selection($sel) if ( defined $sel );
 }
 else {
  $view->set_pixbuf(undef);
  undef $current_page;
 }
 return;
}

sub drag_motion_callback {
 my ( $tree, $context, $x, $y, $t ) = @_;
 my ( $path, $how ) = $tree->get_dest_row_at_pos( $x, $y ) or return;
 my $scroll = $tree->parent;

 # Add the marker showing the drop in the tree
 $tree->set_drag_dest_row( $path, $how );

 # Make move the default
 my @action;
 if ( $context->actions == 'copy' )   ## no critic (ProhibitMismatchedOperators)
 {
  @action = ('copy');
 }
 else {
  @action = ('move');
 }
 $context->status( @action, $t );

 my $adj = $scroll->get_vadjustment;
 my ( $value, $step ) = ( $adj->value, $adj->step_increment );

 if ( $y > $adj->page_size - $step / 2 ) {
  my $v = $value + $step;
  my $m = $adj->upper - $adj->page_size;
  $adj->set_value( $v > $m ? $m : $v );
 }
 elsif ( $y < $step / 2 ) {
  my $v = $value - $step;
  my $m = $adj->lower;
  $adj->set_value( $v < $m ? $m : $v );
 }

 return FALSE;
}

sub create_temp_directory {
 find_crashed_sessions();

 # Create temporary directory if necessary
 unless ( defined $session ) {
  if ( defined( $SETTING{TMPDIR} ) and $SETTING{TMPDIR} ne '' ) {
   mkdir( $SETTING{TMPDIR} ) if ( not -d $SETTING{TMPDIR} );
   try {
    $session = File::Temp->newdir( 'gscan2pdf-XXXX', DIR => $SETTING{TMPDIR} );
   }
   catch {
    $session = File::Temp->newdir( 'gscan2pdf-XXXX', TMPDIR => 1 );
    $logger->warn(
     sprintf(
      $d->get(
"Warning: unable to use %s for temporary storage. Defaulting to %s instead."
      ),
      $SETTING{TMPDIR},
      dirname($session)
     )
    );
   };
  }
  else {
   $session = File::Temp->newdir( 'gscan2pdf-XXXX', TMPDIR => 1 );
  }
  $slist->set_dir($session);
  open $lockfh, '>',    ## no critic (RequireBriefOpen)
    File::Spec->catfile( $session, 'lockfile' )
    or die "Cannot open lockfile\n";
  flock( $lockfh, LOCK_EX ) or die "Cannot lock file\n";
  $slist->save_session;
  $logger->info("Using $session for temporary files");
 }
 return;
}

sub find_crashed_sessions {

 # Look for crashed sessions
 if ( defined( $SETTING{TMPDIR} ) and $SETTING{TMPDIR} ne '' ) {
  $tmpdir = $SETTING{TMPDIR};
 }
 else {
  $tmpdir = File::Spec->tmpdir;
 }
 $logger->info("Checking $tmpdir for crashed sessions");
 my ( @sessions, @crashed, $selected ) =
   glob File::Spec->catfile( $tmpdir, 'gscan2pdf-????' );

 # Forget those used by running sessions
 for (@sessions) {
  if (
   open $lockfh,
   '>',
   File::Spec->catfile( $_, 'lockfile' ) and flock( $lockfh, LOCK_EX | LOCK_NB )
    )
  {
   push @crashed, $_;
   flock( $lockfh, LOCK_UN ) or die "Unlocking error on $lockfh ($!)\n";
   close $lockfh or warn "Error closing $lockfh ($!)\n";
  }
 }

 # Flag those with no session file
 my @missing;
 for ( 0 .. $#crashed ) {
  if ( not -r File::Spec->catfile( $crashed[$_], 'session' ) ) {
   push @missing, $crashed[$_];
   splice @crashed, $_, 1;
  }
 }
 if (@missing) {
  my $dialog = Gtk2::Dialog->new(
   $d->get('Crashed sessions'),
   $window, 'modal',
   'gtk-delete' => 'ok',
   'gtk-cancel' => 'cancel'
  );
  my $text = Gtk2::TextView->new;
  $text->set_wrap_mode('word');
  $text->get_buffer->set_text(
   $d->get(
        'The following list of sessions cannot be restored.'
      . ' Please retrieve any images you require from them.'
      . ' Selected sessions will be deleted.'
   )
  );
  $dialog->vbox->add($text);
  my $sessionlist =
    Gtk2::Ex::Simple::List->new( $d->get('Session') => 'text', );
  push @{ $sessionlist->{data} }, @missing;
  $dialog->vbox->add($sessionlist);
  $dialog->show_all;

  if ( $dialog->run eq 'ok' ) {
   my @selected = $sessionlist->get_selected_indices;
   for (@selected) { $_ = $missing[$_] }
   if (@selected) { remove_tree(@selected) }
  }
  $dialog->destroy;
 }

 # Allow user to pick a crashed session to restore
 if (@crashed) {
  my $dialog = Gtk2::Dialog->new(
   $d->get('Pick crashed session to restore'),
   $window, 'modal',
   'gtk-ok'     => 'ok',
   'gtk-cancel' => 'cancel'
  );
  my $label = Gtk2::Label->new( $d->get('Pick crashed session to restore') );
  $dialog->vbox->add($label);
  my $sessionlist =
    Gtk2::Ex::Simple::List->new( $d->get('Session') => 'text', );
  push @{ $sessionlist->{data} }, @crashed;
  $dialog->vbox->add($sessionlist);
  $dialog->show_all;
  ($selected) = $sessionlist->get_selected_indices if ( $dialog->run eq 'ok' );
  $dialog->destroy;

  if ( defined $selected ) {
   $session = $crashed[$selected];
   open $lockfh, '>',    ## no critic (RequireBriefOpen)
     File::Spec->catfile( $session, 'lockfile' )
     or die "Cannot open lockfile\n";
   flock( $lockfh, LOCK_EX ) or die "Cannot lock file\n";
   $slist->set_dir($session);
   open_session($session);
  }
 }
 return;
}

sub display_image {
 my ($page) = @_;

 $current_page = $page;

 # quotes required to prevent File::Temp object being clobbered
 my $pixbuf = Gtk2::Gdk::Pixbuf->new_from_file("$current_page->{filename}");
 $view->set_pixbuf($pixbuf);

 # Get image dimensions to constrain selector spinbuttons on crop dialog
 ( $current_page->{w}, $current_page->{h} ) =
   ( $pixbuf->get_width, $pixbuf->get_height );

 # Update the ranges on the crop dialog
 if ( defined($sb_selector_w) and defined($current_page) ) {
  $sb_selector_w->set_range( 0,
   $current_page->{w} - $sb_selector_x->get_value );
  $sb_selector_h->set_range( 0,
   $current_page->{h} - $sb_selector_y->get_value );
  $sb_selector_x->set_range( 0,
   $current_page->{w} - $sb_selector_w->get_value );
  $sb_selector_y->set_range( 0,
   $current_page->{h} - $sb_selector_h->get_value );

  $SETTING{selection}[0] = $sb_selector_x->get_value;
  $SETTING{selection}[1] = $sb_selector_y->get_value;
  $SETTING{selection}[2] = $sb_selector_w->get_value;
  $SETTING{selection}[3] = $sb_selector_h->get_value;

  $selector->set_selection(
   Gtk2::Gdk::Rectangle->new( @{ $SETTING{selection} } ) );
 }

 # Convert hocr output to Goo:Canvas if defined
 create_canvas($current_page) if ( defined $current_page->{hocr} );
 return;
}

sub create_canvas {
 my ($page) = @_;
 $current_page = $page;

 # Set up the canvas
 $canvas = Goo::Canvas->new;
 my $root = $canvas->get_root_item;
 unless ( defined $page->{w} ) {

  # quotes required to prevent File::Temp object being clobbered
  my $pixbuf = Gtk2::Gdk::Pixbuf->new_from_file("$page->{filename}");
  $page->{w} = $pixbuf->get_width;
  $page->{h} = $pixbuf->get_height;
 }
 $canvas->set_bounds( 0, 0, $page->{w}, $page->{h} );

 # Attach the text to the canvas
 for my $box ( $page->boxes ) {
  my ( $x1, $y1, $x2, $y2, $text ) = @$box;
  boxed_text(
   text => $text,
   x1   => $x1,
   y1   => $y1,
   x2   => $x2,
   y2   => $y2,
   size => abs( $y2 - $y1 )
  );
 }

 # Remove the existing contents
 $scwin_buffer->remove($_) for ( $scwin_buffer->get_children );

 $scwin_buffer->add($canvas);
 $canvas->set_scale( $view->get_zoom );
 $canvas->show;
 return;
}

# Deletes all scans after warning.

sub new {

 # Check that all pages have been saved
 for ( my $i = 0 ; $i < @{ $slist->{data} } ; $i++ ) {
  if ( not $slist->{data}[$i][2]{saved} ) {
   my $response = show_message_dialog(
    $window,
    'question',
    'ok-cancel',
    $d->get(
     "Some pages have not been saved.\nDo you really want to clear all pages?")
   );
   if ( $response ne 'ok' ) {
    return FALSE;
   }
   else {
    last;
   }
  }
 }

 # Update undo/redo buffers
 take_snapshot();

 # Deselect everything to prevent error removing selected thumbs
 $slist->get_selection->unselect_all;

 # Depopulate the thumbnail list
 @{ $slist->{data} } = ();

 # Reset start page in scan dialog
 reset_start();

 update_uimanager();
 return;
}

# Create a file filter to show only supported file types in FileChooser dialog

sub add_filter {
 my ( $file_chooser, $name, @file_extensions ) = @_;
 my $filter = Gtk2::FileFilter->new;
 for my $extension (@file_extensions) {
  my @filter_pattern;

  # Create case insensitive pattern
  for my $byte ( split //, $extension ) {
   push( @filter_pattern, '[' . uc($byte) . lc($byte) . ']' );
  }
  my $new_filter_pattern = join( '', @filter_pattern );
  $filter->add_pattern( '*.' . $new_filter_pattern );
 }
 my $types;
 for (@file_extensions) {
  if ( defined $types ) {
   $types .= ", *.$_";
  }
  else {
   $types = "*.$_";
  }
 }
 $filter->set_name("$name ($types)");
 $file_chooser->add_filter($filter);
 $filter = Gtk2::FileFilter->new;
 $filter->add_pattern('*');
 $filter->set_name('All files');
 $file_chooser->add_filter($filter);
 return;
}

sub error_callback {
 my ($message) = @_;
 show_message_dialog( $window, 'error', 'close', $message );
 return;
}

sub open_session_file {
 my ($filename) = @_;
 $logger->info("Restoring session in $session");
 $slist->open_session_file( $filename, \&error_callback );
 update_uimanager();
 return;
}

sub open_session_action {
 my ($action) = @_;
 my $file_chooser = Gtk2::FileChooserDialog->new(
  $d->get('Open crashed session'),
  $window, 'select-folder',
  'gtk-cancel' => 'cancel',
  'gtk-ok'     => 'ok'
 );
 $file_chooser->set_default_response('ok');
 $file_chooser->set_current_folder( $SETTING{'cwd'} );

 if ( 'ok' eq $file_chooser->run ) {

  # Update undo/redo buffers
  take_snapshot();

  my @filename = $file_chooser->get_filenames;
  open_session( $filename[0] );
 }
 $file_chooser->destroy;
 return;
}

sub open_session {
 my ($sesdir) = @_;
 $logger->info("Restoring session in $session");
 $slist->open_session( $sesdir, FALSE, \&error_callback );
 update_uimanager();
 return;
}

# Helper function to set up thread progress bar

sub setup_tpbar {
 my ( $thread, $process, $completed, $total, $pid ) = @_;
 if ( $total and defined($process) ) {
  $tpbar->set_text(
   sprintf(
    $d->get("Process %i of %i (%s)"), $completed + 1, $total, $process
   )
  );
  $tpbar->set_fraction( ( $completed + 0.5 ) / $total );
  $thbox->show_all;

# Pass the signal back to:
# 1. be able to cancel it when the process has finished
# 2. flag that the progress bar has been set up
#    and avoid the race condition where the callback is entered before the $completed and $total variables have caught up
  return $tcbutton->signal_connect(
   clicked => sub {
    $slist->cancel($pid);
    $thbox->hide;
   }
  );
 }
 return;
}

# Helper function to update thread progress bar

sub update_tpbar {
 my (%options) = @_;
 if ( $options{total} ) {
  if ( defined $options{process_name} ) {
   $options{process_name} .= " - $options{message}"
     if ( defined $options{message} );
   $tpbar->set_text(
    sprintf(
     $d->get("Process %i of %i (%s)"),
     $options{jobs_completed} + 1, $options{jobs_total},
     $options{process_name}
    )
   );
  }
  else {
   $tpbar->set_text(
    sprintf(
     $d->get("Process %i of %i"),
     $options{jobs_completed} + 1,
     $options{jobs_total}
    )
   );
  }
  if ( defined $options{progress} ) {
   $tpbar->set_fraction(
    ( $options{jobs_completed} + $options{progress} ) / $options{jobs_total} );
  }
  else {
   $tpbar->set_fraction(
    ( $options{jobs_completed} + 0.5 ) / $options{jobs_total} );
  }
  $thbox->show_all;
  return TRUE;
 }
 return;
}

# Throw up file selector and open selected file

sub open_dialog {

 # cd back to cwd to get filename
 chdir $SETTING{'cwd'};

 my $file_chooser = Gtk2::FileChooserDialog->new(
  $d->get('Open image'),
  $window, 'open',
  'gtk-cancel' => 'cancel',
  'gtk-ok'     => 'ok'
 );
 $file_chooser->set_select_multiple(TRUE);
 $file_chooser->set_default_response('ok');
 $file_chooser->set_current_folder( $SETTING{'cwd'} );
 add_filter( $file_chooser, $d->get('Image files'),
  'jpg', 'png', 'pnm', 'gif', 'tif', 'tiff', 'pdf', 'djvu', 'ps', 'gs2p' );

 if ( 'ok' eq $file_chooser->run ) {

  # cd back to tempdir to import
  chdir $session;

  # Update undo/redo buffers
  take_snapshot();

  my @filename = $file_chooser->get_filenames;
  $file_chooser->destroy;

  # Update cwd
  $SETTING{'cwd'} = dirname( $filename[0] );

  for (@filename) {
   my ( $signal, $pid );
   $pid = $slist->get_file_info(
    path            => $_,
    queued_callback => sub {
     return update_tpbar(@_);
    },
    started_callback => sub {
     my ( $thread, $process, $completed, $total ) = @_;
     $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
     return TRUE if ( defined $signal );
    },
    finished_callback => sub {
     my ( $info, $pending ) = @_;
     if ( $info->{format} eq 'session file' ) {
      open_session_file( $info->{path} );
     }
     elsif ( $info->{pages} > 1 ) {
      my $windowq = Gscan2pdf::Dialog->new(
       'transient-for' => $window,
       title           => $d->get('Pages to extract'),
       border_width    => $border_width
      );
      my $vbox = $windowq->get('vbox');
      my $hbox = Gtk2::HBox->new;
      $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
      my $label = Gtk2::Label->new( $d->get('First page to extract') );
      $hbox->pack_start( $label, FALSE, FALSE, 0 );
      my $spinbuttonf =
        Gtk2::SpinButton->new_with_range( 1, $info->{pages}, 1 );
      $hbox->pack_end( $spinbuttonf, FALSE, FALSE, 0 );
      $hbox = Gtk2::HBox->new;
      $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
      $label = Gtk2::Label->new( $d->get('Last page to extract') );
      $hbox->pack_start( $label, FALSE, FALSE, 0 );
      my $spinbuttonl =
        Gtk2::SpinButton->new_with_range( 1, $info->{pages}, 1 );
      $spinbuttonl->set_value( $info->{pages} );
      $hbox->pack_end( $spinbuttonl, FALSE, FALSE, 0 );

      # HBox for buttons
      my $hboxb = Gtk2::HBox->new;
      $vbox->pack_start( $hboxb, FALSE, TRUE, 0 );

      # OK button
      my $obutton = Gtk2::Button->new_from_stock('gtk-ok');
      $hboxb->pack_start( $obutton, TRUE, TRUE, 0 );
      $obutton->signal_connect(
       clicked => sub {
        $windowq->destroy;
        import_file( $info, $spinbuttonf->get_value, $spinbuttonl->get_value );
       }
      );

      # Cancel button
      my $cbutton = Gtk2::Button->new_from_stock('gtk-cancel');
      $hboxb->pack_start( $cbutton, TRUE, TRUE, 0 );
      $cbutton->signal_connect(
       clicked => sub {
        $windowq->destroy;
        return;
       }
      );
      $windowq->show_all;
     }
     else {
      import_file( $info, 1, 1 );
     }
    },
    error_callback => \&error_callback
   );
  }
 }
 else {
  $file_chooser->destroy;
 }

 # cd back to tempdir
 chdir $session;
 return;
}

# Wrapper for $slist->import_file

sub import_file {
 my ( $info, $first_page, $last_page ) = @_;
 my ( $signal, $pid );
 $pid = $slist->import_file(
  info            => $info,
  first           => $first_page,
  last            => $last_page,
  queued_callback => sub {
   return update_tpbar(@_);
  },
  started_callback => sub {
   my ( $thread, $process, $completed, $total ) = @_;
   $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
   return TRUE if ( defined $signal );
  },
  running_callback => sub {
   return update_tpbar(@_);
  },
  finished_callback => sub {
   my ( $new_page, $pending ) = @_;
   $thbox->hide_all unless $pending;
   $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
   update_uimanager();
  },
  error_callback => \&error_callback
 );
 return;
}

# Add a frame and radio buttons to $vbox,
sub add_page_range {
 my ($vbox) = @_;
 my $frame = Gtk2::Frame->new( $d->get('Page Range') );
 $vbox->pack_start( $frame, FALSE, FALSE, 0 );

 my $pr = Gscan2pdf::PageRange->new;
 $pr->set_active( $SETTING{'Page range'} )
   if ( defined $SETTING{'Page range'} );
 $pr->signal_connect(
  changed => sub {
   $SETTING{'Page range'} = $pr->get_active;
  }
 );
 $frame->add($pr);
 push @prlist, $pr;
 return;
}

# return string of filenames depending on which radiobutton is active

sub get_pagelist {
 my $n;
 my $pagelist;
 if ( $SETTING{'Page range'} eq 'all' ) {
  $n        = $#{ $slist->{data} };
  $pagelist = $slist->{data}[0][2]{filename};
  my $i = 1;
  while ( $i < @{ $slist->{data} } ) {
   $pagelist = $pagelist . " " . $slist->{data}[$i][2]{filename};
   ++$i;
  }
 }
 elsif ( $SETTING{'Page range'} eq 'selected' ) {
  my @page = $slist->get_selected_indices;
  $n        = $#page;
  $pagelist = $slist->{data}[ $page[0] ][2]{filename};
  my $i = 1;
  while ( $i < @page ) {
   $pagelist = $pagelist . " " . $slist->{data}[ $page[$i] ][2]{filename};
   ++$i;
  }
 }

 return ( $pagelist, $n );
}

# Add PDF options to $vbox

sub add_pdf_metadata {
 my ($vbox) = @_;

 # Frame for metadata
 my $frame = Gtk2::Frame->new( $d->get('Metadata') );
 $vbox->pack_start( $frame, TRUE, TRUE, 0 );
 my $vboxm = Gtk2::VBox->new;
 $vboxm->set_border_width($border_width);
 $frame->add($vboxm);

 # Date/time
 my $hboxe = Gtk2::HBox->new;
 $vboxm->pack_start( $hboxe, TRUE, TRUE, 0 );
 my $labele = Gtk2::Label->new( $d->get('Date') );
 $hboxe->pack_start( $labele, FALSE, FALSE, 0 );
 my ( $day, $month, $year ) =
   ( localtime( time + $SETTING{'date offset'} * 24 * 60 * 60 ) )[ 3, 4, 5 ];
 $year  += 1900;
 $month += 1;

 my $button =
   Gtk2::Button->new( sprintf( "%04d-%02d-%02d", $year, $month, $day ) );
 $button->signal_connect(
  clicked => sub {
   my $window_date = Gscan2pdf::Dialog->new(
    'transient-for' => $windowi,
    title           => $d->get('Select Date'),
    border_width    => $border_width
   );
   my $vbox_date = $window_date->get('vbox');
   $window_date->set_resizable(FALSE);

   my $calendar = Gtk2::Calendar->new;
   $calendar->select_day($day);
   $calendar->select_month( $month - 1, $year );
   $calendar->signal_connect(
    day_selected_double_click => sub {
     ( $year, $month, $day ) = $calendar->get_date;
     $month += 1;
     $button->set_label( sprintf( "%04d-%02d-%02d", $year, $month, $day ) );
     use Time::Local;
     $SETTING{'date offset'} = int(
      ( timelocal( 0, 0, 0, $day, $month - 1, $year ) - time ) / 60 / 60 / 24 );
     $window_date->destroy;
    }
   );
   $vbox_date->pack_start( $calendar, TRUE, TRUE, 0 );

   my $today = Gtk2::Button->new( $d->get('Today') );
   $today->signal_connect(
    clicked => sub {
     ( $day, $month, $year ) = ( localtime() )[ 3, 4, 5 ];
     $year += 1900;
     $calendar->select_day($day);
     $calendar->select_month( $month, $year );
    }
   );
   $vbox_date->pack_start( $today, TRUE, TRUE, 0 );

   $window_date->show_all;
  }
 );
 $tooltips->set_tip( $button, $d->get('Year-Month-Day') );
 $hboxe->pack_end( $button, TRUE, TRUE, 0 );

 # Document author
 my $hboxa = Gtk2::HBox->new;
 $vboxm->pack_start( $hboxa, TRUE, TRUE, 0 );
 my $labela = Gtk2::Label->new( $d->get('Document author') );
 $hboxa->pack_start( $labela, FALSE, FALSE, 0 );
 my $entrya = Gtk2::Entry->new;
 $hboxa->pack_end( $entrya, TRUE, TRUE, 0 );
 $entrya->set_text( $SETTING{author} ) if ( defined( $SETTING{author} ) );

 # Title
 my $hboxt = Gtk2::HBox->new;
 $vboxm->pack_start( $hboxt, TRUE, TRUE, 0 );
 my $labelt = Gtk2::Label->new( $d->get('Title') );
 $hboxt->pack_start( $labelt, FALSE, FALSE, 0 );
 my $entryt = Gtk2::Entry->new;
 $hboxt->pack_end( $entryt, TRUE, TRUE, 0 );
 $entryt->set_text( $SETTING{title} ) if ( defined( $SETTING{title} ) );

 # Subject
 my $hboxs = Gtk2::HBox->new;
 $vboxm->pack_start( $hboxs, TRUE, TRUE, 0 );
 my $labels = Gtk2::Label->new( $d->get('Subject') );
 $hboxs->pack_start( $labels, FALSE, FALSE, 0 );
 my $entrys = Gtk2::Entry->new;
 $hboxs->pack_end( $entrys, TRUE, TRUE, 0 );
 $entrys->set_text( $SETTING{subject} ) if ( defined( $SETTING{subject} ) );

 # Keywords
 my $hboxk = Gtk2::HBox->new;
 $vboxm->pack_start( $hboxk, TRUE, TRUE, 0 );
 my $labelk = Gtk2::Label->new( $d->get('Keywords') );
 $hboxk->pack_start( $labelk, FALSE, FALSE, 0 );
 my $entryk = Gtk2::Entry->new;
 $hboxk->pack_end( $entryk, TRUE, TRUE, 0 );
 $entryk->set_text( $SETTING{keywords} ) if ( defined( $SETTING{keywords} ) );

 return ( $entrya, $entryt, $entrys, $entryk );
}

sub update_PDF_settings {
 my ( $entrya, $entryt, $entrys, $entryk ) = @_;

 # Get metadata
 $SETTING{author}   = $entrya->get_text;
 $SETTING{title}    = $entryt->get_text;
 $SETTING{subject}  = $entrys->get_text;
 $SETTING{keywords} = $entryk->get_text;
 return;
}

sub get_pdf_metadata {
 my %h;
 $h{Author} = $SETTING{author} if defined $SETTING{author};
 my ( $day, $month, $year ) =
   ( localtime( time + $SETTING{'date offset'} * 24 * 60 * 60 ) )[ 3, 4, 5 ];
 $year  += 1900;
 $month += 1;
 $h{CreationDate} =
   sprintf( "D:%4i%02i%02i000000+00'00'", $year, $month, $day );
 $h{ModDate}  = sprintf( "D:%4i%02i%02i000000+00'00'", $year, $month, $day );
 $h{Creator}  = "$prog_name v$VERSION";
 $h{Producer} = "PDF::API2";
 $h{Title}    = $SETTING{title} if defined $SETTING{title};
 $h{Subject}  = $SETTING{subject} if defined $SETTING{subject};
 $h{Keywords} = $SETTING{keywords} if defined $SETTING{keywords};
 return \%h;
}

# Draw text on the canvas with a box around it

sub boxed_text {
 my (%options) = @_;
 my $root      = $canvas->get_root_item;
 my $g         = Goo::Canvas::Group->new($root);
 $g->translate( $options{x1}, $options{y1} );

# draw the rect first to make sure the text goes on top and receives any mouse clicks
 my $rect = Goo::Canvas::Rect->new(
  $g, 0, 0,
  $options{x2} - $options{x1},
  $options{y2} - $options{y1}
 );
 my $text =
   Goo::Canvas::Text->new( $g, $options{text}, 0, 0,
  $options{x2} - $options{x1},
  'nw', 'height' => $options{size}, );

 # clicking text box produces a dialog to edit the text
 $text->signal_connect(
  'button-press-event' => sub {
   my ( $widget, $target, $ev ) = @_;
   my $dialog = Gtk2::Dialog->new(
    $d->get('Editing text') . "...", $window,
    'modal',
    'gtk-ok'     => 'ok',
    'gtk-cancel' => 'cancel'
   );
   my $textview   = Gtk2::TextView->new;
   my $textbuffer = $textview->get_buffer;
   $textbuffer->set( text => $widget->get('text') );
   $dialog->vbox->add($textview);
   $dialog->set_default_response('ok');
   $dialog->show_all;

   if ( $dialog->run eq 'ok' ) {
    $widget->set( text => $textbuffer->get('text') );
    canvas2hocr();
   }
   $dialog->destroy;
   $canvas->pointer_ungrab( $widget, $ev->time );
   return TRUE;
  }
 );

 # $rect->signal_connect(
 #  'button-press-event' => sub {
 #   my ( $widget, $target, $ev ) = @_;
 #   print "rect button-press-event\n";
 #   #  return TRUE;
 #  }
 # );
 # $g->signal_connect(
 #  'button-press-event' => sub {
 #   my ( $widget, $target, $ev ) = @_;
 #   print "group $widget button-press-event\n";
 #   my $n = $widget->get_n_children;
 #   for ( my $i = 0 ; $i < $n ; $i++ ) {
 #    my $item = $widget->get_child($i);
 #    if ( $item->isa('Goo::Canvas::Text') ) {
 #     print "contains $item\n", $item->get('text'), "\n";
 #     last;
 #    }
 #   }
 #   #  return TRUE;
 #  }
 # );
 return;
}

# Convert the canvas into hocr

sub canvas2hocr {
 my ( $x, $y, $w, $h ) = $canvas->get_bounds;

 $current_page->{hocr} = <<"EOS";
<!DOCTYPE html
 PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN
 http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
 <meta content="ocr_line ocr_page" name="ocr-capabilities"/>
 <meta content="en" name="ocr-langs"/>
 <meta content="Latn" name="ocr-scripts"/>
 <meta content="" name="ocr-microformats"/>
 <title>OCR Output</title>
</head>
<body>
 <div class="ocr_page" title="bbox $x $y $w $h>
 <p class="ocr_par">
EOS

 my $root = $canvas->get_root_item;
 for my $i ( 0 .. $root->get_n_children - 1 ) {
  my $group  = $root->get_child($i);
  my $bounds = $group->get_bounds;
  my ( $x1, $y1, $x2, $y2 ) =
    ( $bounds->x1 + 1, $bounds->y1 + 1, $bounds->x2 - 1, $bounds->y2 - 1 );

#  my $matrix = $group->get_transform; FIXME: use this as soon as Goo::Canvas 0.07 is in generally available
#  my ( $x1, $y1 ) = ( $matrix->x0, $matrix->y0 );
#  my ( $x2, $y2, $text );
  my ($text);
  for my $i ( 0 .. $group->get_n_children - 1 ) {
   my $item = $group->get_child($i);
   if ( $item->isa('Goo::Canvas::Rect') ) {

  #    ( $x2, $y2 ) = ( $x1 + $item->get('width'), $y1 + $item->get('height') );
   }
   elsif ( $item->isa('Goo::Canvas::Text') ) {
    $text = $item->get('text');
   }
   $current_page->{hocr} .=
     "  <span class=\"ocr_line\" title=\"bbox $x1 $y1 $x2 $y2\">$text</span>\n"
     if ( defined($x2) and defined($text) );
  }
 }
 $current_page->{hocr} .= "</p></div></body></html>";
 return;
}

# Save selected pages as PDF under given name.

sub save_PDF {
 my ($filename) = @_;

 # Compile list of pages
 my @list_of_pages;
 my @pagelist =
   $slist->get_page_index( $SETTING{'Page range'}, \&error_callback );
 if ( not @pagelist ) { return }
 for (@pagelist) {
  push @list_of_pages, $slist->{data}[$_][2];
 }

 # Compile options
 my %options = (
  compression      => $SETTING{'pdf compression'},
  downsample       => $SETTING{downsample},
  'downsample dpi' => $SETTING{'downsample dpi'},
  quality          => $SETTING{quality},
  font             => $SETTING{'pdf font'},
 );

 # Create the PDF
 $logger->debug("Started saving $filename");
 my ( $signal, $pid );
 $pid = $slist->save_pdf(
  path            => $filename,
  list_of_pages   => \@list_of_pages,
  metadata        => get_pdf_metadata(),
  options         => \%options,
  queued_callback => sub {
   return update_tpbar(@_);
  },
  started_callback => sub {
   my ( $thread, $process, $completed, $total ) = @_;
   $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
   return TRUE if ( defined $signal );
  },
  running_callback => sub {
   return update_tpbar(@_);
  },
  finished_callback => sub {
   my ( $new_page, $pending ) = @_;
   $thbox->hide_all unless $pending;
   $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
   mark_pages(@pagelist);
   system("xdg-open \"$filename\" &")
     if ( defined( $SETTING{'view files toggle'} )
    and $SETTING{'view files toggle'} );
   $logger->debug("Finished saving $filename");
  },
  error_callback => \&error_callback
 );
 return;
}

# Set up quality spinbutton here so that it can be shown or hidden by callback

sub add_quality_spinbutton {

 my ($vbox) = @_;
 my $hbox = Gtk2::HBox->new;
 $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
 my $label = Gtk2::Label->new( $d->get('JPEG Quality') );
 $hbox->pack_start( $label, FALSE, FALSE, 0 );
 my $spinbutton = Gtk2::SpinButton->new_with_range( 1, 100, 1 );
 $spinbutton->set_value( $SETTING{'quality'} );
 $hbox->pack_end( $spinbutton, FALSE, FALSE, 0 );
 return ( $hbox, $spinbutton );
}

sub add_pdf_compression {
 my ($vbox) = @_;

 # Downsample options
 my $hboxd = Gtk2::HBox->new;
 $vbox->pack_start( $hboxd, FALSE, FALSE, 0 );
 my $button = Gtk2::CheckButton->new( $d->get('Downsample to') );
 $button->set_active(TRUE);
 $hboxd->pack_start( $button, FALSE, FALSE, 0 );
 my $spinbutton = Gtk2::SpinButton->new_with_range( 9, 2400, 1 );
 $spinbutton->set_value( $SETTING{'downsample dpi'} );
 my $label = Gtk2::Label->new( $d->get('PPI') );
 $hboxd->pack_end( $label,      FALSE, FALSE, 0 );
 $hboxd->pack_end( $spinbutton, FALSE, FALSE, 0 );
 $button->signal_connect(
  toggled => sub {

   if ( $button->get_active ) {
    $spinbutton->set_sensitive(TRUE);
   }
   else {
    $spinbutton->set_sensitive(FALSE);
   }
  }
 );
 $button->set_active( $SETTING{'downsample'} );

 # Compression options
 my @compression = (
  [
   'auto',
   $d->get('Automatic'),
   $d->get('Let gscan2pdf which type of compression to use.')
  ],
  [
   'lzw', $d->get('LZW'),
   $d->get('Compress output with Lempel-Ziv & Welch encoding.')
  ],
  [ 'zip', $d->get('Zip'), $d->get('Compress output with deflate encoding.') ],
  [
   'packbits', $d->get('Packbits'),
   $d->get('Compress output with Packbits encoding.')
  ],

# g3 and 4 give an error message
#  [ 'g3', $d->get('G3'), $d->get('Compress output with CCITT Group 3 encoding.') ],
#  [ 'g4', $d->get('G4'), $d->get('Compress output with CCITT Group 4 encoding.') ],
  [ 'png', $d->get('PNG'),  $d->get('Compress output with PNG encoding.') ],
  [ 'jpg', $d->get('JPEG'), $d->get('Compress output with JPEG encoding.') ],
  [
   'none', $d->get('None'),
   $d->get('Use no compression algorithm on output.')
  ],
 );

 # Compression ComboBox
 my $hbox = Gtk2::HBox->new;
 $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
 $label = Gtk2::Label->new( $d->get('Compression') );
 $hbox->pack_start( $label, FALSE, FALSE, 0 );

 # Set up quality spinbutton here so that it can be shown or hidden by callback
 my ( $hboxq, $spinbuttonq ) = add_quality_spinbutton($vbox);
 my $combob = combobox_from_array(@compression);
 $combob->signal_connect(
  changed => sub {
   if ( $compression[ $combob->get_active ][0] eq 'jpg' ) {
    $hboxq->show_all;
   }
   else {
    $hboxq->hide_all;
   }
  }
 );
 combobox_set_active( $combob, $SETTING{'pdf compression'}, @compression );
 $hbox->pack_end( $combob, FALSE, FALSE, 0 );

 return ( $button, $spinbutton, $combob, $hboxq, $spinbuttonq, @compression );
}

# Display page selector and on save a fileselector.

sub save_dialog {

 # $SETTING{'Page range'} = 'selected' if $SETTING{'RMB'};

 if ( defined $windowi ) {
  $windowi->present;
  return;
 }

 $windowi = Gscan2pdf::Dialog->new(
  'transient-for'  => $window,
  title            => $d->get('Save'),
  'hide-on-delete' => TRUE,
  border_width     => $border_width
 );
 my $vbox = $windowi->get('vbox');

 # Frame for page range
 add_page_range($vbox);

 # Image type ComboBox
 my $hboxi = Gtk2::HBox->new;
 $vbox->pack_start( $hboxi, TRUE, TRUE, 0 );
 my $label = Gtk2::Label->new( $d->get('Image type') );
 $hboxi->pack_start( $label, FALSE, FALSE, 0 );

 my @type = (
  [ 'pdf', $d->get('PDF'), $d->get('Portable Document Format') ],
  [ 'gif', $d->get('GIF'), $d->get('CompuServe graphics interchange format') ],
  [
   'jpg', $d->get('JPEG'),
   $d->get('Joint Photographic Experts Group JFIF format')
  ],
  [ 'png',     $d->get('PNG'),     $d->get('Portable Network Graphics') ],
  [ 'pnm',     $d->get('PNM'),     $d->get('Portable anymap') ],
  [ 'ps',      $d->get('PS'),      $d->get('Postscript') ],
  [ 'tif',     $d->get('TIFF'),    $d->get('Tagged Image File Format') ],
  [ 'txt',     $d->get('Text'),    $d->get('Plain text') ],
  [ 'session', $d->get('Session'), $d->get('gscan2pdf session file') ],
 );
 push @type, [ 'djvu', $d->get('DjVu'), $d->get('Deja Vu') ]
   if $dependencies{djvu};

 my @tiff_compression = (
  [
   'lzw', $d->get('LZW'),
   $d->get('Compress output with Lempel-Ziv & Welch encoding.')
  ],
  [ 'zip', $d->get('Zip'), $d->get('Compress output with deflate encoding.') ],

  # jpeg rather than jpg needed here because tiffcp uses -c jpeg
  [ 'jpeg', $d->get('JPEG'), $d->get('Compress output with JPEG encoding.') ],
  [
   'packbits', $d->get('Packbits'),
   $d->get('Compress output with Packbits encoding.')
  ],
  [
   'g3', $d->get('G3'),
   $d->get('Compress output with CCITT Group 3 encoding.')
  ],
  [
   'g4', $d->get('G4'),
   $d->get('Compress output with CCITT Group 4 encoding.')
  ],
  [
   'none', $d->get('None'),
   $d->get('Use no compression algorithm on output.')
  ],
 );

 # Compression ComboBox
 my $hboxc = Gtk2::HBox->new;
 $vbox->pack_start( $hboxc, FALSE, FALSE, 0 );
 $label = Gtk2::Label->new( $d->get('Compression') );
 $hboxc->pack_start( $label, FALSE, FALSE, 0 );

 # Set up quality spinbutton here so that it can be shown or hidden by callback
 my ( $hboxtq, $spinbuttontq ) = add_quality_spinbutton($vbox);
 $label = Gtk2::Label->new( $d->get('tiff') );
 $hboxtq->pack_start( $label, FALSE, FALSE, 0 );

 # Fill compression ComboBox
 my $combobtc = combobox_from_array(@tiff_compression);
 $combobtc->signal_connect(
  changed => sub {
   if ( $tiff_compression[ $combobtc->get_active ][0] eq 'jpeg' ) {
    $hboxtq->show_all;
   }
   else {
    $hboxtq->hide_all;
    $windowi->resize( 100, 100 );    # Doesn't matter that 200x200 is too small
   }
  }
 );
 combobox_set_active( $combobtc, $SETTING{'tiff compression'},
  @tiff_compression );
 $hboxc->pack_end( $combobtc, FALSE, FALSE, 0 );

 # PDF options
 my $vboxp = Gtk2::VBox->new;
 $vbox->pack_start( $vboxp, FALSE, FALSE, 0 );
 my ( $entrya, $entryt, $entrys, $entryk ) = add_pdf_metadata($vboxp);

 # Compression options
 my ( $buttond, $spinbuttond, $combob, $hboxpq, $spinbuttonpq,
  @pdf_compression ) = add_pdf_compression($vboxp);

 # Font for non-ASCII text
 my $scwinf = Gtk2::ScrolledWindow->new;
 $scwinf->set_policy( 'automatic', 'automatic' );
 $vbox->pack_start( $scwinf, TRUE, TRUE, 0 );
 my $hboxf = Gtk2::HBox->new;
 $scwinf->add_with_viewport($hboxf);
 $scwinf->get_child->set_shadow_type('none');
 $label = Gtk2::Label->new( $d->get('Font for non-ASCII text') );
 $hboxf->pack_start( $label, FALSE, FALSE, 0 );
 my @fonts;
 my ($stdout) = Gscan2pdf::Document::open_three('fc-list : family style file');

 for ( split /\n/, $stdout ) {
  if (/ttf:\ /x) {
   my ( $file, $family, $style ) = split ':';
   chomp $style;
   $family =~ s/^\ //x;
   $style =~ s/^style=//x;
   $style =~ s/,.*$//x;
   my $font = "$family $style";
   push @fonts, [ $file, $font, $font ];
   $SETTING{'pdf font'} = $file
     if ( not defined( $SETTING{'pdf font'} )
    and $font eq 'Times New Roman Regular' );
  }
 }
 @fonts = sort { $a->[1] cmp $b->[1] } @fonts;
 my $combof = combobox_from_array(@fonts);
 combobox_set_active( $combof, $SETTING{'pdf font'}, @fonts );
 $hboxf->pack_start( $combof, FALSE, FALSE, 0 );

 # Fill image type ComboBox
 my $combobi = combobox_from_array(@type);
 $combobi->signal_connect(
  changed => \&image_type_changed_callback,
  [
   \@type,    $vboxp,  $hboxf, \@pdf_compression,
   $combob,   $hboxpq, $hboxc, \@tiff_compression,
   $combobtc, $hboxtq
  ]
 );
 combobox_set_active( $combobi, $SETTING{'image type'}, @type );
 $hboxi->pack_end( $combobi, FALSE, FALSE, 0 );

 # HBox for buttons
 my $hboxb = Gtk2::HBox->new;
 $vbox->pack_start( $hboxb, FALSE, TRUE, 0 );

 # Save button
 my $sbutton = Gtk2::Button->new_from_stock('gtk-save');
 $hboxb->pack_start( $sbutton, TRUE, TRUE, 0 );
 $sbutton->signal_connect(
  clicked => \&save_button_clicked_callback,
  [
   \@type,        $combobi,           $buttond,
   $spinbuttond,  \@pdf_compression,  $combob,
   $spinbuttonpq, \@fonts,            $combof,
   $entrya,       $entryt,            $entrys,
   $entryk,       \@tiff_compression, $combobtc,
   $spinbuttontq
  ]
 );

 # Cancel button
 my $cbutton = Gtk2::Button->new_from_stock('gtk-cancel');
 $hboxb->pack_end( $cbutton, FALSE, FALSE, 0 );
 $cbutton->signal_connect( clicked => sub { $windowi->hide; } );

 $windowi->show_all;
 $hboxpq->hide_all if ( $pdf_compression[ $combob->get_active ][0] ne 'jpg' );
 $hboxtq->hide_all
   if (
  $type[ $combobi->get_active ][0] ne 'jpg'
  or ( $type[ $combobi->get_active ][0] eq 'tif'
   and $tiff_compression[ $combobtc->get_active ][0] ne 'jpg' )
   );
 $hboxc->hide_all
   if ( $type[ $combobi->get_active ][0] ne 'tif'
  and $type[ $combobi->get_active ][0] ne 'ps' );

 if ( $type[ $combobi->get_active ][0] ne 'pdf' ) {
  $vboxp->hide_all;
  $hboxf->hide_all;
 }
 $windowi->resize( 100, 100 );    # Doesn't matter that 200x200 is too small
 return;
}

sub image_type_changed_callback {
 my ( $widget, $data ) = @_;
 my (
  $type,     $vboxp,  $hboxf, $pdf_compression,
  $combob,   $hboxpq, $hboxc, $tiff_compression,
  $combobtc, $hboxtq
 ) = @$data;
 if ( $type->[ $widget->get_active ][0] eq 'pdf' ) {
  $vboxp->show_all;
  $hboxf->show_all;
  if ( $pdf_compression->[ $combob->get_active ][0] eq 'jpg' ) {
   $hboxpq->show_all;
  }
  else {
   $hboxpq->hide_all;
   $windowi->resize( 100, 100 );    # Doesn't matter that 200x200 is too small
  }
 }
 else {
  $vboxp->hide_all;
  $hboxf->hide_all;
  $windowi->resize( 100, 100 );     # Doesn't matter that 200x200 is too small
 }
 if ($type->[ $widget->get_active ][0] eq 'tif'
  or $type->[ $widget->get_active ][0] eq 'ps' )
 {
  $hboxc->show_all;
 }
 else {
  $hboxc->hide_all;
  $windowi->resize( 100, 100 );     # Doesn't matter that 200x200 is too small
 }
 if (
  $type->[ $widget->get_active ][0] eq 'jpg'
  or ( $type->[ $widget->get_active ][0] eq 'tif'
   and $tiff_compression->[ $combobtc->get_active ][0] eq 'jpeg' )
   )
 {
  $hboxtq->show_all;
 }
 else {
  $hboxtq->hide_all;
  $windowi->resize( 100, 100 );     # Doesn't matter that 200x200 is too small
 }
 return;
}

sub save_button_clicked_callback {
 my ( $widget, $data ) = @_;
 my (
  $type,            $combobi,          $buttond,      $spinbuttond,
  $pdf_compression, $combob,           $spinbuttonpq, $fonts,
  $combof,          $entrya,           $entryt,       $entrys,
  $entryk,          $tiff_compression, $combobtc,     $spinbuttontq
 ) = @$data;

 # dig out the image type, compression and quality
 $SETTING{'image type'} = $type->[ $combobi->get_active ][0];

 given ( $SETTING{'image type'} ) {
  when ('pdf') {

   # dig out the compression
   $SETTING{'downsample'}      = $buttond->get_active;
   $SETTING{'downsample dpi'}  = $spinbuttond->get_value;
   $SETTING{'pdf compression'} = $pdf_compression->[ $combob->get_active ][0];
   $SETTING{'quality'}         = $spinbuttonpq->get_value;

   $SETTING{'pdf font'} = $fonts->[ $combof->get_active ][0];

   update_PDF_settings( $entrya, $entryt, $entrys, $entryk );

   # cd back to cwd to save
   chdir $SETTING{'cwd'};

   # Set up file selector
   my $file_chooser = Gtk2::FileChooserDialog->new(
    $d->get('PDF filename'),
    $windowi, 'save',
    'gtk-cancel' => 'cancel',
    'gtk-save'   => 'ok'
   );

   my ( $dday, $dmonth, $dyear ) =
     ( localtime( time + $SETTING{'date offset'} * 24 * 60 * 60 ) )[ 3, 4, 5 ];
   $dyear += 1900;
   $dmonth = sprintf( "%02d", $dmonth + 1 );
   $dday   = sprintf( "%02d", $dday );
   my ( $tday, $tmonth, $tyear ) = ( localtime(time) )[ 3, 4, 5 ];
   $tyear += 1900;
   $tmonth = sprintf( "%02d", $tmonth + 1 );
   $tday   = sprintf( "%02d", $tday );

   my $filename = $SETTING{'default filename'};
   $filename =~ s/%a/$SETTING{author}/g;
   $filename =~ s/%t/$SETTING{title}/g;
   $filename =~ s/%y/$dyear/g;
   $filename =~ s/%Y/$tyear/g;
   $filename =~ s/%m/$dmonth/g;
   $filename =~ s/%M/$tmonth/g;
   $filename =~ s/%d/$dday/g;
   $filename =~ s/%D/$tday/g;

   $file_chooser->set_current_name($filename);
   $file_chooser->set_default_response('ok');
   $file_chooser->set_current_folder( $SETTING{'cwd'} );
   add_filter( $file_chooser, $d->get('PDF files'), 'pdf' );
   $file_chooser->set_do_overwrite_confirmation(TRUE);
   $file_chooser->signal_connect(
    response => \&file_chooser_response_callback,
    'pdf'
   );
   $file_chooser->show;

   # cd back to tempdir
   chdir $session;
  }
  when ('djvu') {

   # cd back to cwd to save
   chdir $SETTING{'cwd'};

   # Set up file selector
   my $file_chooser = Gtk2::FileChooserDialog->new(
    $d->get('DjVu filename'),
    $windowi, 'save',
    'gtk-cancel' => 'cancel',
    'gtk-save'   => 'ok'
   );
   $file_chooser->set_default_response('ok');
   $file_chooser->set_current_folder( $SETTING{'cwd'} );
   add_filter( $file_chooser, $d->get('DjVu files'), 'djvu' );
   $file_chooser->set_do_overwrite_confirmation(TRUE);
   $file_chooser->signal_connect(
    response => \&file_chooser_response_callback,
    'djvu'
   );
   $file_chooser->show;

   # cd back to tempdir
   chdir $session;
  }
  when ('tif') {
   $SETTING{'tiff compression'} =
     $tiff_compression->[ $combobtc->get_active ][0];
   $SETTING{'quality'} = $spinbuttontq->get_value;

   # cd back to cwd to save
   chdir $SETTING{'cwd'};

   # Set up file selector
   my $file_chooser = Gtk2::FileChooserDialog->new(
    $d->get('TIFF filename'),
    $windowi, 'save',
    'gtk-cancel' => 'cancel',
    'gtk-save'   => 'ok'
   );
   $file_chooser->set_default_response('ok');
   $file_chooser->set_current_folder( $SETTING{'cwd'} );
   add_filter( $file_chooser, $d->get('Image files'), $SETTING{'image type'} );
   $file_chooser->set_do_overwrite_confirmation(TRUE);
   $file_chooser->signal_connect(
    response => \&file_chooser_response_callback,
    'tif'
   );
   $file_chooser->show;

   # cd back to tempdir
   chdir $session;
  }
  when ('txt') {

   # cd back to cwd to save
   chdir $SETTING{'cwd'};

   # Set up file selector
   my $file_chooser = Gtk2::FileChooserDialog->new(
    $d->get('Text filename'),
    $windowi, 'save',
    'gtk-cancel' => 'cancel',
    'gtk-save'   => 'ok'
   );
   $file_chooser->set_default_response('ok');
   $file_chooser->set_current_folder( $SETTING{'cwd'} );
   $file_chooser->set_do_overwrite_confirmation(TRUE);
   add_filter( $file_chooser, $d->get('Text files'), 'txt' );
   $file_chooser->signal_connect(
    response => \&file_chooser_response_callback,
    'txt'
   );
   $file_chooser->show;

   # cd back to tempdir
   chdir $session;
  }
  when ('ps') {
   $SETTING{'tiff compression'} =
     $tiff_compression->[ $combobtc->get_active ][0];
   $SETTING{'quality'} = $spinbuttontq->get_value;

   # cd back to cwd to save
   chdir $SETTING{'cwd'};

   # Set up file selector
   my $file_chooser = Gtk2::FileChooserDialog->new(
    $d->get('PS filename'),
    $windowi, 'save',
    'gtk-cancel' => 'cancel',
    'gtk-save'   => 'ok'
   );
   $file_chooser->set_default_response('ok');
   $file_chooser->set_current_folder( $SETTING{'cwd'} );
   add_filter( $file_chooser, $d->get('Postscript files'), 'ps' );
   $file_chooser->set_do_overwrite_confirmation(TRUE);
   $file_chooser->signal_connect(
    response => \&file_chooser_response_callback,
    'ps'
   );
   $file_chooser->show;

   # cd back to tempdir
   chdir $session;
  }
  when ('session') {

   # cd back to cwd to save
   chdir $SETTING{'cwd'};

   # Set up file selector
   my $file_chooser = Gtk2::FileChooserDialog->new(
    $d->get('gscan2pdf session filename'),
    $windowi, 'save',
    'gtk-cancel' => 'cancel',
    'gtk-save'   => 'ok'
   );
   $file_chooser->set_default_response('ok');
   $file_chooser->set_current_folder( $SETTING{'cwd'} );
   add_filter( $file_chooser, $d->get('gscan2pdf session files'), 'gs2p' );
   $file_chooser->set_do_overwrite_confirmation(TRUE);
   $file_chooser->signal_connect(
    response => \&file_chooser_response_callback,
    'gs2p'
   );
   $file_chooser->show;

   # cd back to tempdir
   chdir $session;
  }
  default {
   save_image();
  }
 }
 return;
}

sub file_chooser_response_callback {
 my ( $dialog, $response, $type ) = @_;
 $logger->debug("PDF save filename dialog returned $response");
 if ( $response eq 'ok' ) {
  my $filename = $dialog->get_filename;
  if ( $filename !~ /\.$type$/ix ) {
   $filename = "$filename.$type";
   if ( -f $filename ) {

    # File exists; get the file chooser to ask the user to confirm.
    $dialog->set_filename($filename);

    # Give the name change time to take effect.
    Glib::Idle->add( sub { $dialog->response('ok'); } );
    return;
   }
  }

  # Check that the file can be written
  if ( not -w dirname($filename)
   or ( -f $filename and not -w $filename ) )
  {
   show_message_dialog( $dialog, 'error', 'close',
    sprintf( $d->get("File %s is read-only"), $filename ) );
   return;
  }

  # Update cwd
  $SETTING{'cwd'} = dirname($filename);

  given ($type) {
   when ('pdf') {
    save_PDF($filename);
   }
   when ('djvu') {
    save_djvu($filename);
   }
   when ('tif') {
    save_TIFF($filename);
   }
   when ('txt') {
    save_txt($filename);
   }
   when ('ps') {
    my $tif = File::Temp->new( DIR => $session, SUFFIX => '.tif' );
    save_TIFF( $tif->filename, $filename );
   }
   when ('gs2p') {
    $slist->save_session($filename);
   }
  }
  $windowi->hide if defined $windowi;
 }
 $dialog->destroy;
 return;
}

sub file_exists {
 my ( $file_chooser, $filename ) = @_;
 if ( not -w dirname($filename) or ( -f $filename and not -w $filename ) ) {
  show_message_dialog( $file_chooser, 'error', 'close',
   sprintf( $d->get("File %s is read-only"), $filename ) );
  return TRUE;
 }
 elsif ( -e $filename ) {
  my $response =
    show_message_dialog( $file_chooser, 'question', 'ok-cancel',
   sprintf( $d->get("File %s exists.\nReally overwrite?"), $filename ) );
  return TRUE if ( $response ne 'ok' );
 }
 return FALSE;
}

sub save_image {

 # cd back to cwd to save
 chdir $SETTING{'cwd'};

 # Set up file selector
 my $file_chooser = Gtk2::FileChooserDialog->new(
  $d->get('Image filename'),
  $windowi, 'save',
  'gtk-cancel' => 'cancel',
  'gtk-save'   => 'ok'
 );
 $file_chooser->set_default_response('ok');
 $file_chooser->set_current_folder( $SETTING{'cwd'} );
 add_filter( $file_chooser, $d->get('Image files'),
  'jpg', 'png', 'pnm', 'gif', 'tif', 'tiff', 'pdf', 'djvu', 'ps' );

 if ( 'ok' eq $file_chooser->run ) {
  my $filename = $file_chooser->get_filename;

  # Update cwd
  $SETTING{'cwd'} = dirname($filename);

  # cd back to tempdir
  chdir $session;

  # Compile list of pages
  my @list_of_pages;
  my @pagelist =
    $slist->get_page_index( $SETTING{'Page range'}, \&error_callback );
  if ( not @pagelist ) { return }
  for (@pagelist) {
   push @list_of_pages, $slist->{data}[$_][2];
  }

  if ( @list_of_pages > 1 ) {
   my $w = length( scalar @list_of_pages );
   for ( my $i = 1 ; $i <= @list_of_pages ; $i++ ) {
    my $current_filename = sprintf "${filename}_%0${w}d.$SETTING{'image type'}",
      $i;
    return if ( file_exists( $file_chooser, $current_filename ) );
   }
   $filename = "${filename}_%0${w}d.$SETTING{'image type'}";
  }
  else {
   $filename = $filename . ".$SETTING{'image type'}"
     if ( $filename !~ /\.$SETTING{'image type'}$/ix );
   return if ( file_exists( $file_chooser, $filename ) );
  }

  # Create the image
  $logger->debug("Started saving $filename");
  my ( $signal, $pid );
  $pid = $slist->save_image(
   path            => $filename,
   list_of_pages   => \@list_of_pages,
   queued_callback => sub {
    return update_tpbar(@_);
   },
   started_callback => sub {
    my ( $thread, $process, $completed, $total ) = @_;
    $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
    return TRUE if ( defined $signal );
   },
   running_callback => sub {
    return update_tpbar(@_);
   },
   finished_callback => sub {
    my ( $new_page, $pending ) = @_;
    $thbox->hide_all unless $pending;
    $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
    mark_pages(@pagelist);
    system("xdg-open \"$filename\" &")
      if ( defined( $SETTING{'view files toggle'} )
     and $SETTING{'view files toggle'} );
    $logger->debug("Finished saving $filename");
   },
   error_callback => \&error_callback
  );

  $windowi->hide if defined $windowi;
 }
 $file_chooser->destroy;
 return;
}

sub save_TIFF {
 my ( $filename, $ps ) = @_;

 # Compile list of pages
 my @list_of_pages;
 my @pagelist =
   $slist->get_page_index( $SETTING{'Page range'}, \&error_callback );
 if ( not @pagelist ) { return }
 for (@pagelist) {
  push @list_of_pages, $slist->{data}[$_][2];
 }

 # Compile options
 my %options = (
  compression => $SETTING{'tiff compression'},
  quality     => $SETTING{quality},
 );

 my ( $signal, $pid );
 $pid = $slist->save_tiff(
  path            => $filename,
  list_of_pages   => \@list_of_pages,
  options         => \%options,
  ps              => $ps,
  queued_callback => sub {
   return update_tpbar(@_);
  },
  started_callback => sub {
   my ( $thread, $process, $completed, $total ) = @_;
   $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
   return TRUE if ( defined $signal );
  },
  running_callback => sub {
   return update_tpbar(@_);
  },
  finished_callback => sub {
   my ( $new_page, $pending ) = @_;
   $thbox->hide_all unless $pending;
   $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
   mark_pages(@pagelist);
   my $file = defined($ps) ? $ps : $filename;
   system("xdg-open \"$filename\" &")
     if ( defined( $SETTING{'view files toggle'} )
    and $SETTING{'view files toggle'} );
   $logger->debug("Finished saving $filename");
  },
  error_callback => \&error_callback
 );
 return;
}

sub save_djvu {
 my ($filename) = @_;

 # Compile list of pages
 my @list_of_pages;
 my @pagelist =
   $slist->get_page_index( $SETTING{'Page range'}, \&error_callback );
 if ( not @pagelist ) { return }
 for (@pagelist) {
  push @list_of_pages, $slist->{data}[$_][2];
 }

 # cd back to tempdir
 chdir $session;

 # Create the DjVu
 $logger->debug("Started saving $filename");
 my ( $signal, $pid );
 $pid = $slist->save_djvu(
  path            => $filename,
  list_of_pages   => \@list_of_pages,
  queued_callback => sub {
   return update_tpbar(@_);
  },
  started_callback => sub {
   my ( $thread, $process, $completed, $total ) = @_;
   $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
   return TRUE if ( defined $signal );
  },
  running_callback => sub {
   return update_tpbar(@_);
  },
  finished_callback => sub {
   my ( $new_page, $pending ) = @_;
   $thbox->hide_all unless $pending;
   $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
   mark_pages(@pagelist);
   system("xdg-open \"$filename\" &")
     if ( defined( $SETTING{'view files toggle'} )
    and $SETTING{'view files toggle'} );
   $logger->debug("Finished saving $filename");
  },
  error_callback => \&error_callback
 );

 return;
}

sub save_txt {
 my ($filename) = @_;

 # Compile list of pages
 my @list_of_pages;
 my @pagelist =
   $slist->get_page_index( $SETTING{'Page range'}, \&error_callback );
 if ( not @pagelist ) { return }
 for (@pagelist) {
  push @list_of_pages, $slist->{data}[$_][2];
 }

 my ( $signal, $pid );
 $pid = $slist->save_text(
  path            => $filename,
  list_of_pages   => \@list_of_pages,
  queued_callback => sub {
   return update_tpbar(@_);
  },
  started_callback => sub {
   my ( $thread, $process, $completed, $total ) = @_;
   $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
   return TRUE if ( defined $signal );
  },
  running_callback => sub {
   return update_tpbar(@_);
  },
  finished_callback => sub {
   my ( $new_page, $pending ) = @_;
   $thbox->hide_all unless $pending;
   $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
   mark_pages(@pagelist);
   system("xdg-open \"$filename\" &")
     if ( defined( $SETTING{'view files toggle'} )
    and $SETTING{'view files toggle'} );
   $logger->debug("Finished saving $filename");
  },
  error_callback => \&error_callback
 );
 return;
}

# Display page selector and email.

sub email {

 if ( defined $windowe ) {
  $windowe->present;
  return;
 }

 # $SETTING{'Page range'} = 'selected' if $SETTING{'RMB'};
 $windowe = Gscan2pdf::Dialog->new(
  'transient-for'  => $window,
  title            => $d->get('Email as PDF'),
  'hide-on-delete' => TRUE,
  border_width     => $border_width
 );
 my $vbox = $windowe->get('vbox');

 # PDF options
 my ( $entrya, $entryt, $entrys, $entryk ) = add_pdf_metadata($vbox);

 # Frame for page range
 add_page_range($vbox);

 # Compression options
 my ( $buttond, $spinbuttond, $combob, $hboxq, $spinbuttonq, @compression ) =
   add_pdf_compression($vbox);

 # HBox for buttons
 my $hboxb = Gtk2::HBox->new;
 $vbox->pack_start( $hboxb, FALSE, TRUE, 0 );

 # OK button
 my $sbutton = Gtk2::Button->new_from_stock('gtk-ok');
 $hboxb->pack_start( $sbutton, TRUE, TRUE, 0 );
 $sbutton->signal_connect(
  clicked => sub {

   # Set options
   update_PDF_settings( $entrya, $entryt, $entrys, $entryk );

   # Compile list of pages
   my @list_of_pages;
   my @pagelist =
     $slist->get_page_index( $SETTING{'Page range'}, \&error_callback );
   if ( not @pagelist ) { return }
   for (@pagelist) {
    push @list_of_pages, $slist->{data}[$_][2];
   }

   # dig out the compression
   $SETTING{'downsample'}      = $buttond->get_active;
   $SETTING{'downsample dpi'}  = $spinbuttond->get_value;
   $SETTING{'pdf compression'} = $compression[ $combob->get_active ][0];
   $SETTING{'quality'}         = $spinbuttonq->get_value;

   # Compile options
   my %options = (
    compression      => $SETTING{'pdf compression'},
    downsample       => $SETTING{downsample},
    'downsample dpi' => $SETTING{'downsample dpi'},
    quality          => $SETTING{quality},
    font             => $SETTING{'pdf font'},
   );

   $pdf = File::Temp->new( DIR => $session, SUFFIX => '.pdf' );

   # Check for thunderbird
   my ($client);
   if ( defined( $ENV{KDE_FULL_SESSION} ) and $ENV{KDE_FULL_SESSION} eq 'true' )
   {
    ($client) = Gscan2pdf::Document::open_three(
"kreadconfig --file emaildefaults --group PROFILE_Default --key EmailClient| cut -d ' ' -f 1"
    );
   }
   elsif (
    (
     defined( $ENV{GNOME_DESKTOP_SESSION_ID} )
     and $ENV{GNOME_DESKTOP_SESSION_ID} ne ''
    )
    or ( defined( $ENV{XDG_CURRENT_DESKTOP} )
     and $ENV{XDG_CURRENT_DESKTOP} eq 'XFCE' )
     )
   {
    ($client) = Gscan2pdf::Document::open_three(
"gconftool --get /desktop/gnome/url-handlers/mailto/command | cut -d ' ' -f 1"
    );
   }

   my $mua_string;
   if ( $client =~ /thunderbird/ ) {
    $mua_string = "thunderbird -compose attachment=file://$pdf";
   }
   else {
    $mua_string = "xdg-email --attach $pdf 'x\@y'";
   }

   # Create the PDF
   my ( $signal, $pid );
   $pid = $slist->save_pdf(
    path            => $pdf->filename,
    list_of_pages   => \@list_of_pages,
    metadata        => get_pdf_metadata(),
    options         => \%options,
    queued_callback => sub {
     return update_tpbar(@_);
    },
    started_callback => sub {
     my ( $thread, $process, $completed, $total ) = @_;
     $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
     return TRUE if ( defined $signal );
    },
    running_callback => sub {
     return update_tpbar(@_);
    },
    finished_callback => sub {
     my ( $new_page, $pending ) = @_;
     $thbox->hide_all unless $pending;
     $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
     mark_pages(@pagelist);
     system("xdg-open \"$pdf\" &")
       if ( defined( $SETTING{'view files toggle'} )
      and $SETTING{'view files toggle'} );
     $logger->info($mua_string);
     show_message_dialog( $window, 'error', 'close',
      $d->get('Error creating email') )
       if ( system($mua_string) );
    },
    error_callback => \&error_callback
   );

   $windowe->hide;

  }
 );

 # Cancel button
 my $cbutton = Gtk2::Button->new_from_stock('gtk-cancel');
 $hboxb->pack_end( $cbutton, FALSE, FALSE, 0 );
 $cbutton->signal_connect( clicked => sub { $windowe->hide; } );

 $windowe->show_all;
 $hboxq->hide_all if ( $compression[ $combob->get_active ][0] ne 'jpg' );
 return;
}

# Scan

sub scan_dialog {
 my ($hidden) = @_;

 if ( defined $windows ) {
  $windows->show_all;
  return;
 }

 # If device not set by config and there is a default device, then set it
 $SETTING{device} = $ENV{'SANE_DEFAULT_DEVICE'}
   if ( not defined( $SETTING{device} )
  and defined( $ENV{'SANE_DEFAULT_DEVICE'} ) );

 $SETTING{'pages to scan'} = 0 if ( $SETTING{'pages to scan'} eq 'all' );

 # scan pop-up window
 my %options = (
  'transient-for'     => $window,
  title               => $d->get('Scan Document'),
  border_width        => $border_width,
  logger              => $logger,
  dir                 => $session,
  'hide-on-delete'    => TRUE,
  'num-pages'         => $SETTING{'pages to scan'},
  'page-number-start' => $#{ $slist->{data} } > -1
  ? $slist->{data}[ $#{ $slist->{data} } ][0] + 1
  : 1,
 );
 if ( $SETTING{frontend} eq 'libsane-perl' ) {
  $windows = Gscan2pdf::Dialog::Scan::Sane->new(%options);
 }
 else {
  $windows = Gscan2pdf::Dialog::Scan::CLI->new(
   %options,
   prefix                 => $SETTING{'scan prefix'},
   frontend               => $SETTING{'frontend'},
   'visible-scan-options' => $SETTING{'visible-scan-options'},
   'reload-triggers'      => $SETTING{'scan-reload-triggers'},
   'cache-options'        => $SETTING{'cache options'},
   'options-cache'        => $SETTING{cache},
  );
  $windows->signal_connect(
   'changed-options-cache' => sub {
    my ( $widget, $cache ) = @_;
    $SETTING{cache} = $cache;
   }
  );
 }

 # Can't set the device when creating the window,
 # as the list does not exist then
 $windows->signal_connect(
  'changed-device-list' => \&changed_device_list_callback );

 # Update default device
 $windows->signal_connect( 'changed-device' => \&changed_device_callback );

 # Set default
 $windows->signal_connect(
  'changed-num-pages' => sub {
   ( my $widget, $SETTING{'pages to scan'} ) = @_;

   # Check that there is room in the list for the number of pages
   update_number();
  }
 );
 $windows->signal_connect(
  'changed-page-number-start' => sub {
   my ( $widget, $value ) = @_;
   $windows->set( 'max-pages',
    $slist->pages_possible( $value, $windows->get('page-number-increment') ) );
  }
 );
 $windows->signal_connect(
  'changed-page-number-increment' => sub {
   my ( $widget, $step ) = @_;
   $windows->set( 'max-pages',
    $slist->pages_possible( $windows->get('page-number-start'), $step ) );
  }
 );

 $windows->signal_connect(
  'changed-side-to-scan' => \&changed_side_to_scan_callback );

 my $signal;
 $windows->signal_connect(
  'started-process' => sub {
   my ( $widget, $message ) = @_;
   $spbar->set_fraction(0);
   $spbar->set_text($message);
   $shbox->show_all;
   $signal = $scbutton->signal_connect(
    clicked => sub {
     $windows->cancel_scan;
    }
   );
  }
 );

 $windows->signal_connect( 'changed-progress' => \&changed_progress_callback );

 $windows->signal_connect(
  'finished-process' => sub {
   my ( $widget, $process, $button_signal ) = @_;
   $logger->info("signal 'finished-process' emitted with data: $process");
   $scbutton->signal_handler_disconnect($button_signal)
     if ( defined $button_signal );
   $shbox->hide;

   $widget->scan_options( $SETTING{device} )
     if ( $SETTING{'cycle sane handle'} and $process eq 'scan_pages' );
  }
 );

 $windows->signal_connect(
  'process-error' => sub {
   my ( $widget, $process, $msg ) = @_;
   $logger->info("signal 'process-error' emitted with data: $process $msg");
   $scbutton->signal_handler_disconnect($signal) if ( defined $signal );
   $shbox->hide;
   if ( $process =~ /^(?:get_devices|find_scan_options)$/x ) {
    $widget->hide;
    undef $widget;
    show_message_dialog( $window, 'error', 'close', $msg );
   }
   else {
    show_message_dialog( $widget, 'error', 'close', $d_sane->get($msg) );
   }
  }
 );

 $windows->signal_connect(
  'changed-scan-option' => \&changed_scan_option_callback );

 # Profiles
 for my $profile ( keys %{ $SETTING{profile} } ) {
  $windows->add_profile( $profile, $SETTING{profile}{$profile} );
 }
 $windows->signal_connect(
  'changed-profile' => sub {
   my ( $widget, $profile ) = @_;
   $SETTING{'default profile'} = $profile;
  }
 );
 $windows->signal_connect(
  'added-profile' => sub {
   my ( $widget, $name, $profile ) = @_;
   $SETTING{profile}{$name} = $profile;
  }
 );
 $windows->signal_connect(
  'removed-profile' => sub {
   my ( $widget, $profile ) = @_;
   delete $SETTING{profile}{$profile};
  }
 );

 # Update the default profile when the scan options change
 $windows->signal_connect(
  'changed-current-scan-options' => sub {
   my ( $widget, $option_array ) = @_;
   $SETTING{'default-scan-options'} = $option_array;
  }
 );

 # Can't set the paper or profile until the options have been loaded
 $windows->signal_connect(
  'reloaded-scan-options' => \&reloaded_scan_options_callback );
 $windows->signal_connect(
  'changed-paper-formats' => sub {
   my ( $widget, $formats ) = @_;
   $SETTING{Paper} = $formats;
  }
 );
 $windows->signal_connect(
  'changed-paper' => sub {
   my ( $widget, $paper ) = @_;
   $SETTING{'Paper size'} = $paper;
  }
 );

 $windows->signal_connect( 'new-scan' => \&new_scan_callback );

 add_postprocessing_options($windows);

 $windows->show_all unless $hidden;

 if (@device_list) {
  $windows->set( 'device-list', \@device_list );
 }
 else {
  $windows->get_devices;
 }
 return;
}

sub changed_device_callback {
 my ( $widget, $device ) = @_;
 $logger->info("signal 'changed-device' emitted with data: '$device'");
 $SETTING{device} = $device
   if ( defined($device) and $device ne '' );
 return;
}

sub changed_device_list_callback {
 my ( $widget, $device_list ) = @_;
 $logger->info(
  "signal 'changed-device-list' emitted with data: " . Dumper($device_list) );
 if ( defined $device_list ) {

  # Only set default device if it hasn't been specified on the command line
  # and it is in the the device list
  if ( defined( $SETTING{device} ) and not @device_list ) {
   for (@$device_list) {
    if ( $SETTING{device} eq $_->{name} ) {
     $windows->set( 'device', $SETTING{device} );
     return;
    }
   }
  }
  $windows->set( 'device', $device_list->[0]{name} );
 }
 return;
}

sub changed_side_to_scan_callback {
 my ( $widget, $side ) = @_;
 if ( $#{ $slist->{data} } > -1 ) {
  $windows->set( 'page-number-start',
   $slist->{data}[ $#{ $slist->{data} } ][0] + 1 );
 }
 else {
  $windows->set( 'page-number-start', 1 );
 }
 return;
}

sub reloaded_scan_options_callback {
 $windows->set( 'paper-formats', $SETTING{Paper} );
 $windows->set( 'paper',         $SETTING{'Paper size'} )
   if ( defined $SETTING{'Paper size'} );

 my @profiles = keys %{ $SETTING{profile} };
 if ( defined $SETTING{'default profile'} ) {
  $windows->set( 'profile', $SETTING{'default profile'} );
 }
 elsif ( defined $SETTING{'default-scan-options'} ) {
  $windows->set( 'current-scan-options', $SETTING{'default-scan-options'} );
 }
 elsif (@profiles) {
  $windows->set( 'profile', $profiles[0] );
 }
 return;
}

sub changed_scan_option_callback {
 my ( $widget, $option, $value ) = @_;

 # Note resolution
 $SETTING{resolution} = $value
   if ( $option eq SANE_NAME_SCAN_RESOLUTION );
 return;
}

sub changed_progress_callback {
 my ( $widget, $progress, $message ) = @_;
 if ( defined $progress ) {
  $spbar->set_fraction($progress);
 }
 else {
  $spbar->pulse;
 }
 $spbar->set_text($message) if ( defined $message );
 return;
}

sub new_scan_callback {
 my ( $widget, $n ) = @_;

 # Update undo/redo buffers
 take_snapshot();

 # If the scan can't be loaded then blow the scanning dialog away and
 # show an error
 my $rotate = $n % 2 ? $SETTING{'rotate facing'} : $SETTING{'rotate reverse'};
 if ( defined $test_image ) {
  import_scan(
   filename => $test_image,
   page     => $n,
   delete   => FALSE,
   rotate   => $rotate,
   unpaper  => $SETTING{'unpaper on scan'},
   ocr      => $SETTING{'OCR on scan'}
  );
 }
 else {
  my $options = $windows->get('available-scan-options');
  my $opt     = $options->by_name(SANE_NAME_SCAN_RESOLUTION);
  $SETTING{resolution} =
    ( defined($opt) and defined( $opt->{val} ) )
    ? $opt->{val}
    : $Gscan2pdf::Document::POINTS_PER_INCH;
  $logger->info("Importing scan with resolution=$SETTING{resolution}");
  import_scan(
   filename   => File::Spec->catdir( $session, "out$n.pnm" ),
   page       => $n,
   resolution => $SETTING{resolution},
   delete     => TRUE,
   rotate     => $rotate,
   unpaper    => $SETTING{'unpaper on scan'},
   ocr        => $SETTING{'OCR on scan'}
  );
 }
 return;
}

sub add_postprocessing_rotate {
 my ($vbox) = @_;
 my $hboxr = Gtk2::HBox->new;
 $vbox->pack_start( $hboxr, FALSE, FALSE, 0 );
 my $rbutton = Gtk2::CheckButton->new( $d->get('Rotate') );
 $tooltips->set_tip( $rbutton, $d->get('Rotate image after scanning') );
 $hboxr->pack_start( $rbutton, TRUE, TRUE, 0 );
 my @side = (
  [ 'both',    $d->get('Both sides'),   $d->get('Both sides.') ],
  [ 'facing',  $d->get('Facing side'),  $d->get('Facing side.') ],
  [ 'reverse', $d->get('Reverse side'), $d->get('Reverse side.') ],
 );
 my $comboboxs = combobox_from_array(@side);
 $tooltips->set_tip( $comboboxs, $d->get('Select side to rotate') );
 $hboxr->pack_start( $comboboxs, TRUE, TRUE, 0 );
 my @rotate = (
  [ 90,  $d->get('90'),  $d->get('Rotate image 90 degrees clockwise.') ],
  [ 180, $d->get('180'), $d->get('Rotate image 180 degrees clockwise.') ],
  [ 270, $d->get('270'), $d->get('Rotate image 90 degrees anticlockwise.') ],
 );
 my $comboboxr = combobox_from_array(@rotate);
 $tooltips->set_tip( $comboboxr, $d->get('Select direction of rotation') );
 $hboxr->pack_end( $comboboxr, TRUE, TRUE, 0 );

 $hboxr = Gtk2::HBox->new;
 $vbox->pack_start( $hboxr, FALSE, FALSE, 0 );
 my $r2button = Gtk2::CheckButton->new( $d->get('Rotate') );
 $tooltips->set_tip( $r2button, $d->get('Rotate image after scanning') );
 $hboxr->pack_start( $r2button, TRUE, TRUE, 0 );
 my @side2;
 my $comboboxs2 = Gtk2::ComboBox->new_text;
 $tooltips->set_tip( $comboboxs2, $d->get('Select side to rotate') );
 $hboxr->pack_start( $comboboxs2, TRUE, TRUE, 0 );
 my $comboboxr2 = combobox_from_array(@rotate);
 $tooltips->set_tip( $comboboxr2, $d->get('Select direction of rotation') );
 $hboxr->pack_end( $comboboxr2, TRUE, TRUE, 0 );

 $rbutton->signal_connect(
  toggled => sub {
   if ( $rbutton->get_active ) {
    $hboxr->set_sensitive(TRUE)
      if ( $side[ $comboboxs->get_active ]->[0] ne 'both' );
   }
   else {
    $hboxr->set_sensitive(FALSE);
   }
  }
 );
 $comboboxs->signal_connect(
  changed => sub {
   if ( $side[ $comboboxs->get_active ]->[0] eq 'both' ) {
    $hboxr->set_sensitive(FALSE);
    $r2button->set_active(FALSE);
   }
   else {
    $hboxr->set_sensitive(TRUE) if ( $rbutton->get_active );

    # Empty combobox
    while ( $comboboxs2->get_active > -1 ) {
     $comboboxs2->remove_text(0);
     $comboboxs2->set_active(0);
    }
    @side2 = ();
    foreach (@side) {
     push @side2, $_
       unless ( $_->[0] eq 'both'
      or $_->[0] eq $side[ $comboboxs->get_active ]->[0] );
    }
    $comboboxs2->append_text( $side2[0]->[1] );
    $comboboxs2->set_active(0);
   }
  }
 );

 # In case it isn't set elsewhere
 combobox_set_active( $comboboxr2, 90, @rotate );

 if ( $SETTING{'rotate facing'} or $SETTING{'rotate reverse'} ) {
  $rbutton->set_active(TRUE);
 }
 if ( $SETTING{'rotate facing'} == $SETTING{'rotate reverse'} ) {
  combobox_set_active( $comboboxs, 'both', @side );
  combobox_set_active( $comboboxr, $SETTING{'rotate facing'}, @rotate );
 }
 elsif ( $SETTING{'rotate facing'} ) {
  combobox_set_active( $comboboxs, 'facing', @side );
  combobox_set_active( $comboboxr, $SETTING{'rotate facing'}, @rotate );
  if ( $SETTING{'rotate reverse'} ) {
   $r2button->set_active(TRUE);
   combobox_set_active( $comboboxs2, 'reverse', @side2 );
   combobox_set_active( $comboboxr2, $SETTING{'rotate reverse'}, @rotate );
  }
 }
 else {
  combobox_set_active( $comboboxs, 'reverse', @side );
  combobox_set_active( $comboboxr, $SETTING{'rotate reverse'}, @rotate );
 }
 return (
  \@rotate,   \@side,      \@side2,    $rbutton, $r2button,
  $comboboxs, $comboboxs2, $comboboxr, $comboboxr2
 );
}

sub add_postprocessing_ocr {
 my ($vbox) = @_;
 my $hboxo = Gtk2::HBox->new;
 $vbox->pack_start( $hboxo, FALSE, FALSE, 0 );
 my $obutton = Gtk2::CheckButton->new( $d->get('OCR scanned pages') );
 $tooltips->set_tip( $obutton, $d->get('OCR scanned pages') );
 if ( not $dependencies{gocr}
  and not $dependencies{tesseract}
  and not $dependencies{ocropus}
  and not $dependencies{cuneiform} )
 {
  $hboxo->set_sensitive(FALSE);
  $obutton->set_active(FALSE);
 }
 elsif ( $SETTING{'OCR on scan'} ) {
  $obutton->set_active(TRUE);
 }
 $hboxo->pack_start( $obutton, TRUE, TRUE, 0 );
 my $comboboxe = combobox_from_array(@ocr_engine);
 $tooltips->set_tip( $comboboxe, $d->get('Select OCR engine') );
 $hboxo->pack_end( $comboboxe, TRUE, TRUE, 0 );
 my ( $comboboxtl, $hboxtl, @tesslang, $comboboxcl, $hboxcl, @cflang );
 if ( $dependencies{tesseract} ) {
  ( $hboxtl, $comboboxtl, @tesslang ) = add_tess_languages($vbox);
  $comboboxe->signal_connect(
   changed => sub {
    if ($ocr_engine[ $comboboxe->get_active ]->[0] eq 'tesseract'
     or $ocr_engine[ $comboboxe->get_active ]->[0] eq 'ocropus' )
    {
     $hboxtl->show_all;
    }
    else {
     $hboxtl->hide_all;
    }
   }
  );
  if ( $dependencies{cuneiform} ) {
   ( $hboxcl, $comboboxcl, @cflang ) = add_cf_languages($vbox);
   $comboboxe->signal_connect(
    changed => sub {
     if ( $ocr_engine[ $comboboxe->get_active ]->[0] eq 'cuneiform' ) {
      $hboxcl->show_all;
     }
     else {
      $hboxcl->hide_all;
     }
    }
   );
  }
  $hboxtl->set_sensitive(FALSE) unless ( $obutton->get_active );
  $obutton->signal_connect(
   toggled => sub {
    if ( $obutton->get_active ) {
     $hboxtl->set_sensitive(TRUE);
    }
    else {
     $hboxtl->set_sensitive(FALSE);
    }
   }
  );
 }
 combobox_set_active( $comboboxe, $SETTING{'ocr engine'}, @ocr_engine );
 return (
  $obutton, $comboboxe,  $hboxtl,    $comboboxtl,
  $hboxcl,  $comboboxcl, \@tesslang, \@cflang
 );
}

sub add_postprocessing_options {
 my ($self) = @_;
 my $vbox = $self->get('vbox');

 # Frame for post-processing
 my $framep = Gtk2::Frame->new( $d->get('Post-processing') );
 $vbox->pack_start( $framep, FALSE, FALSE, 0 );
 my $vboxp = Gtk2::VBox->new;
 $vboxp->set_border_width($border_width);
 $framep->add($vboxp);

 # Rotate
 my (
  $rotate,    $side,       $side2,     $rbutton, $r2button,
  $comboboxs, $comboboxs2, $comboboxr, $comboboxr2
 ) = add_postprocessing_rotate($vboxp);

 # CheckButton for unpaper
 my $hboxu = Gtk2::HBox->new;
 $vboxp->pack_start( $hboxu, FALSE, FALSE, 0 );
 my $ubutton = Gtk2::CheckButton->new( $d->get('Clean up images') );
 $tooltips->set_tip( $ubutton,
  $d->get('Clean up scanned images with unpaper') );
 $hboxu->pack_start( $ubutton, TRUE, TRUE, 0 );
 if ( not $dependencies{unpaper} ) {
  $ubutton->set_sensitive(FALSE);
  $ubutton->set_active(FALSE);
 }
 elsif ( $SETTING{'unpaper on scan'} ) {
  $ubutton->set_active(TRUE);
 }
 my $button = Gtk2::Button->new( $d->get('Options') );
 $tooltips->set_tip( $button, $d->get('Set unpaper options') );
 $hboxu->pack_end( $button, TRUE, TRUE, 0 );
 $button->signal_connect(
  clicked => sub {
   my $windowuo = Gscan2pdf::Dialog->new(
    'transient-for' => $window,
    title           => $d->get('unpaper options'),
    border_width    => $border_width
   );
   my $vbox1 = $windowuo->get('vbox');
   $unpaper->add_options($vbox1);

   # HBox for buttons
   my $hboxb = Gtk2::HBox->new;
   $vbox1->pack_start( $hboxb, FALSE, TRUE, 0 );

   # OK button
   my $sbutton = Gtk2::Button->new_from_stock('gtk-ok');
   $hboxb->pack_start( $sbutton, TRUE, TRUE, 0 );
   $sbutton->signal_connect(
    clicked => sub {

     # Update $SETTING
     $SETTING{'unpaper options'} = $unpaper->get_options;

     $windowuo->destroy;
    }
   );

   # Cancel button
   my $cbutton = Gtk2::Button->new_from_stock('gtk-cancel');
   $hboxb->pack_end( $cbutton, FALSE, FALSE, 0 );
   $cbutton->signal_connect( clicked => sub { $windowuo->destroy; } );

   $windowuo->show_all;
  }
 );

 my (
  $obutton, $comboboxe,  $hboxtl,   $comboboxtl,
  $hboxcl,  $comboboxcl, $tesslang, $cflang
 ) = add_postprocessing_ocr($vboxp);

 $self->signal_connect(
  'clicked-scan-button' => sub {
   $SETTING{'rotate facing'}  = 0;
   $SETTING{'rotate reverse'} = 0;
   if ( $rbutton->get_active ) {
    if ( $side->[ $comboboxs->get_active ][0] eq 'both' ) {
     $SETTING{'rotate facing'}  = $rotate->[ $comboboxr->get_active ][0];
     $SETTING{'rotate reverse'} = $SETTING{'rotate facing'};
    }
    elsif ( $side->[ $comboboxs->get_active ][0] eq 'facing' ) {
     $SETTING{'rotate facing'} = $rotate->[ $comboboxr->get_active ][0];
    }
    else {
     $SETTING{'rotate reverse'} = $rotate->[ $comboboxr->get_active ][0];
    }
    if ( $r2button->get_active ) {
     if ( $side2->[ $comboboxs2->get_active ][0] eq 'facing' ) {
      $SETTING{'rotate facing'} = $rotate->[ $comboboxr2->get_active ][0];
     }
     else {
      $SETTING{'rotate reverse'} = $rotate->[ $comboboxr2->get_active ][0];
     }
    }
   }
   $logger->info("rotate facing $SETTING{'rotate facing'}");
   $logger->info("rotate reverse $SETTING{'rotate reverse'}");

   $SETTING{'unpaper on scan'} = $ubutton->get_active;
   $logger->info("unpaper $SETTING{'unpaper on scan'}");
   $SETTING{'OCR on scan'} = $obutton->get_active;
   $logger->info("OCR $SETTING{'OCR on scan'}");
   if ( $SETTING{'OCR on scan'} ) {
    $SETTING{'ocr engine'}   = $ocr_engine[ $comboboxe->get_active ]->[0];
    $SETTING{'ocr language'} = $tesslang->[ $comboboxtl->get_active ][0]
      if ( $SETTING{'ocr engine'} eq 'tesseract'
     or $SETTING{'ocr engine'} eq 'ocropus' );
    $SETTING{'ocr language'} = $cflang->[ $comboboxcl->get_active ][0]
      if ( $SETTING{'ocr engine'} eq 'cuneiform' );
   }

  }
 );

 $self->signal_connect(
  show => sub {
   $hboxtl->hide_all
     if (
    defined($hboxtl)
    and not( $ocr_engine[ $comboboxe->get_active ]->[0] eq 'tesseract'
     or $ocr_engine[ $comboboxe->get_active ]->[0] eq 'ocropus' )
     );
   $hboxcl->hide_all
     if ( defined($hboxcl)
    and not( $ocr_engine[ $comboboxe->get_active ]->[0] eq 'cuneiform' ) );
  }
 );

 return;
}

# Called either from changed-value signal of spinbutton,
# or row-changed signal of simplelist

sub update_start {
 return unless ( defined $windows );
 my $value = $windows->get('page-number-start');
 $start = $windows->get('page-number-start') unless ( defined $start );
 my $step = $value - $start;
 $step = $windows->get('page-number-increment') if ( $step == 0 );
 my $exists = TRUE;
 my $i = $step > 0 ? 0 : $#{ $slist->{data} };
 $start = $value;

 while ($exists) {
  if ($i < 0
   or $i > $#{ $slist->{data} }
   or ( $slist->{data}[$i][0] > $value and $step > 0 )
   or ( $slist->{data}[$i][0] < $value and $step < 0 ) )
  {
   $exists = FALSE;
  }
  elsif ( $slist->{data}[$i][0] == $value ) {
   $value += $step;
   if ( $value < 1 ) {
    $value = 1;
    $step  = 1;
   }
  }
  else {
   $i += $step > 0 ? 1 : -1;
  }
 }
 $windows->set( 'page-number-start', $value );
 $start = $value;

 update_number();
 return;
}

# Update the number of pages to scan spinbutton if necessary

sub update_number {
 return unless ( defined $windows );
 my $n = $slist->pages_possible( $windows->get('page-number-start'),
  $windows->get('page-number-increment') );
 $windows->set( 'num-pages', $n )
   if ( $n > 0 and $n < $windows->get('num-pages') );
 return;
}

sub post_process_scan {
 my ( $pages, $rotate, $unpaper_flag, $ocr ) = @_;

 if ($rotate) {
  rotate(
   $rotate, $pages,
   sub {    # finished callback
    my ($page) = @_;
    post_process_scan( $page, undef, $unpaper_flag, $ocr );
   }
  );
 }
 elsif ($unpaper_flag) {
  unpaper_page(
   $pages,
   $unpaper->get_cmdline,
   sub {    # finished callback
    my ($page) = @_;
    post_process_scan( $page, undef, undef, $ocr );
   }
  );
 }
 elsif ($ocr) {
  ocr_page($pages);
 }
 return;
}

# Take new scan and display it

sub import_scan {
 my %options = @_;

 # Interface to frontend
 open my $fh, '<', $options{filename}    ## no critic (RequireBriefOpen)
   or die "can't open $options{filename}: $!\n";

 # Read without blocking
 my $size = 0;
 Glib::IO->add_watch(
  fileno($fh),
  [ 'in', 'hup' ],
  sub {
   my ( $fileno, $condition ) = @_;
   if ( $condition & 'in' ) {    # bit field operation. >= would also work
    if ( $size == 0 ) {
     $size = Gscan2pdf::NetPBM::file_size_from_header( $options{filename} );
     $logger->info("Header suggests $size");
     return Glib::SOURCE_CONTINUE if ( $size == 0 );
     close($fh);
    }
    my $filesize = -s $options{filename};
    $logger->info("Expecting $size, found $filesize");
    if ( $size > $filesize ) {
     my $pad = $size - $filesize;
     open( my $fh, ">>", $options{filename} )
       or die "cannot open >> $options{filename}: $!\n";
     my $data = '';
     for ( 1 .. $pad * 8 ) {
      $data .= '1';
     }
     printf $fh pack( sprintf( "b%d", length $data ), $data );
     close $fh;
     $logger->info("Padded $pad bytes");
    }
    my $gpage = Gscan2pdf::Page->new(
     filename   => $options{filename},
     resolution => $options{resolution},
     format     => 'Portable anymap',
     delete     => $options{delete},
     dir        => $session,
     window     => $window,
    );
    my $index = $slist->add_page( $gpage, $options{page}, &update_uimanager );
    if ( $index == -1 ) {
     show_message_dialog( $windows, 'error', 'close',
      $d->get('Unable to load image') );
    }
    else {
     update_uimanager();
     post_process_scan( [$gpage], $options{rotate}, $options{unpaper},
      $options{ocr} );
    }
    return Glib::SOURCE_REMOVE;
   }
   return Glib::SOURCE_CONTINUE;
  }
 );

 return;
}

# print

sub print_dialog {
 chdir $SETTING{'cwd'};
 my $print_op = Gtk2::PrintOperation->new;

 $print_op->set_print_settings($print_settings)
   if ( defined $print_settings );

 $print_op->signal_connect(
  begin_print => sub {
   my ( $op, $context ) = @_;

   my $settings = $op->get_print_settings;
   my $pages    = $settings->get('print-pages');
   my @page_list;
   if ( $pages eq 'ranges' ) {
    my $page_set = Set::IntSpan->new;
    my $ranges   = $settings->get('page-ranges');
    for ( split ',', $ranges ) {
     $page_set->I($_);
    }
    for ( 0 .. $#{ $slist->{data} } ) {
     push @page_list, $_ if ( $page_set->member( $slist->{data}[$_][0] ) );
    }
   }
   else {
    @page_list = ( 0 .. $#{ $slist->{data} } );
   }
   $op->set_n_pages( scalar(@page_list) );
  }
 );

 $print_op->signal_connect(
  draw_page => sub {
   my ( $op, $context, $page_number ) = @_;

   my $cr = $context->get_cairo_context;

   # Context dimensions
   my $pwidth  = $context->get_width;
   my $pheight = $context->get_height;

   # Image dimensions
   my $pixbuf = Gtk2::Gdk::Pixbuf->new_from_file(
    "$slist->{data}[$page_number][2]{filename}")
     ;    # quotes required to prevent File::Temp object being clobbered
   my $iwidth  = $pixbuf->get_width;
   my $iheight = $pixbuf->get_height;

   # Scale context to fit image
   my $scale = $pwidth / $iwidth;
   $scale = $pheight / $iheight if ( $pheight / $iheight < $scale );
   $cr->scale( $scale, $scale );

   # Set source pixbuf
   Gtk2::Gdk::Cairo::Context::set_source_pixbuf( $cr, $pixbuf, 0, 0 );

   # Paint
   $cr->paint;

   return;
  }
 );

 my $res = $print_op->run( 'print-dialog', $window );

 $print_settings = $print_op->get_print_settings if ( $res eq 'apply' );
 chdir $session;
 return;
}

# Cut the selection

sub cut_selection {
 if ( $slist->has_focus ) {
  copy_selection();
  delete_pages();
 }

 # elsif ($textview -> has_focus) {
 #  my $clipboard = Gtk2::Clipboard->get(Gtk2::Gdk->SELECTION_CLIPBOARD);
 #  $textbuffer->cut_clipboard ($clipboard, TRUE)
 # }
 return;
}

# Copy the selection

sub copy_selection {
 if ( $slist->has_focus ) {
  undef @clipboard;
  my @pages = $slist->get_selected_indices;
  for my $page (@pages) {
   my @copy = map { [@$_] } $slist->{data}[$page];
   push @clipboard, @copy;
  }
 }

 # elsif ($textview -> has_focus) {
 #  my $clipboard = Gtk2::Clipboard->get(Gtk2::Gdk->SELECTION_CLIPBOARD);
 #  $textbuffer->copy_clipboard ($clipboard)
 # }
 return;
}

# Paste the selection

sub paste_selection {

 # We only add pages if the list of thumbnails has the focus
 if ( $window->get_focus eq $slist ) {
  my @page = $slist->get_selected_indices;

  # Create a new image file for each page in the clipboard
  for (@clipboard) {
   my $suffix;
   $suffix = $1 if ( $_->[2]{filename} =~ /(\.\w*)$/x );
   my $new = File::Temp->new( DIR => $session, SUFFIX => $suffix );

   # stringify filename to prevent copy from mangling it
   unless ( copy( "$_->[2]{filename}", $new ) ) {
    show_message_dialog( $window, 'error', 'close',
     $d->get( sprintf("Error pasting image - %s"), $! ) );
    return;
   }
   $_->[2]{filename} = $new;
  }

  # Block the row-changed signal whilst adding the scan (row) and sorting it.
  $slist->get_model->signal_handler_block( $slist->{row_changed_signal} );

  # If a page is selected, then insert the pasted page immediately afterwards
  if (@page) {
   splice @{ $slist->{data} }, $page[0] + 1, 0, @clipboard;
   @page = ( $page[0] + 1 );
  }

  # Otherwise append the page to the end
  else {
   push @{ $slist->{data} }, @clipboard;
   @page = ( $#{ $slist->{data} } - $#clipboard );
  }

  $slist->renumber;
  $slist->get_model->signal_handler_unblock( $slist->{row_changed_signal} );

  # Select new page, deselecting others. This fires the select callback,
  # displaying the page
  $slist->get_selection->unselect_all;
  $slist->select(@page);

  update_uimanager();
 }

 # elsif ($textview -> has_focus) {
 #  my $clipboard = Gtk2::Clipboard->get(Gtk2::Gdk->SELECTION_CLIPBOARD);
 #  $textbuffer->paste_clipboard ($clipboard, undef, TRUE)
 # }
 return;
}

# Delete the selected scans

sub delete_pages {

 # Update undo/redo buffers
 take_snapshot();

 my @pages = $slist->get_selected_indices;
 my @page  = @pages;
 $slist->get_selection->signal_handler_block(
  $slist->{selection_changed_signal} );
 while (@pages) {
  splice @{ $slist->{data} }, $pages[0], 1;
  @pages = $slist->get_selected_indices;
 }
 $slist->get_selection->signal_handler_unblock(
  $slist->{selection_changed_signal} );

 # Select nearest page to last current page
 if ( @{ $slist->{data} } and @page ) {

  # Select just the first one
  @page = ( $page[0] );
  $page[0] = $#{ $slist->{data} } if ( $page[0] > $#{ $slist->{data} } );
  $slist->select(@page);
 }

 # Select nothing
 elsif ( @{ $slist->{data} } ) {
  $slist->select;
 }

# No pages left, and having blocked the selection_changed_signal, we've got to clear the image
 else {
  $view->set_pixbuf(undef);
  $scwin_buffer->remove($_) for ( $scwin_buffer->get_children );
 }

 # Reset start page in scan dialog
 reset_start();

 update_uimanager();
 return;
}

# Reset start page number after delete or new

sub reset_start {
 if ( $#{ $slist->{data} } > -1 ) {
  my $start_page = $windows->get('page-number-start');
  my $step       = $windows->get('page-number-increment');
  $windows->set( 'page-number-start',
   $slist->{data}[ $#{ $slist->{data} } ][0] + $step )
    if ( $start_page > $slist->{data}[ $#{ $slist->{data} } ][0] + $step );
 }
 else {
  $windows->set( 'page-number-start', 1 );
 }
 return;
}

# Select all scans

sub select_all {

 # if ($textview -> has_focus) {
 #  my ($start, $end) = $textbuffer->get_bounds;
 #  $textbuffer->select_range ($start, $end);
 # }
 # else {
 $slist->get_selection->select_all;

 # }
 return;
}

# Select all odd(0) or even(1) scans

sub select_odd_even {
 my $odd = shift;
 my @selection;
 for ( 0 .. $#{ $slist->{data} } ) {
  push @selection, $_ if ( $slist->{data}[$_][0] % 2 xor $odd );
 }

 $slist->get_selection->unselect_all;
 $slist->select(@selection);
 return;
}

sub select_modified_since_ocr {
 my @selection;
 foreach my $page ( 0 .. $#{ $slist->{data} } ) {
  my $dirty_time = $slist->{data}[$page][2]{dirty_time};
  my $ocr_flag   = $slist->{data}[$page][2]{ocr_flag};
  my $ocr_time   = $slist->{data}[$page][2]{ocr_time};
  $dirty_time = defined($dirty_time) ? $dirty_time : 0;
  $ocr_time   = defined($ocr_time)   ? $ocr_time   : 0;
  push @selection, $_ if ( $ocr_flag and ( $ocr_time le $dirty_time ) );
 }

 $slist->get_selection->unselect_all;
 $slist->select(@selection);
 return;
}

# Select pages with no ocr output

sub select_no_ocr {
 my @selection;
 for ( 0 .. $#{ $slist->{data} } ) {
  push @selection, $_ unless ( defined $slist->{data}[$_][2]{hocr} );
 }

 $slist->get_selection->unselect_all;
 $slist->select(@selection);
 return;
}

# Clear the OCR output from selected pages

sub clear_ocr {

 # Update undo/redo buffers
 take_snapshot();

 # Remove the existing canvas
 $scwin_buffer->remove($_) for ( $scwin_buffer->get_children );

 my @selection = $slist->get_selected_indices;
 for (@selection) {
  delete $slist->{data}[$_][2]{hocr};
 }
 return;
}

# Analyse and select blank pages

sub analyse_select_blank {
 analyse( 1, 0 );
 return;
}

# Select blank pages

sub select_blank_pages {
 $slist->get_selection->unselect_all;
 foreach my $page ( 0 .. $#{ $slist->{data} } ) {
  $logger->info("smp page: $page");
  my $dirty_time   = $slist->{data}[$page][2]{dirty_time};
  my $analyse_time = $slist->{data}[$page][2]{analyse_time};
  $dirty_time   = defined($dirty_time)   ? $dirty_time   : 0;
  $analyse_time = defined($analyse_time) ? $analyse_time : 0;
  if ( $analyse_time le $dirty_time ) {
   $logger->warn(
    $d->get(
     sprintf(
      "Page %d probably needs to be re-Analysed.  Try Update or Analyse.",
      $page + 1 )
    )
   );
   next;
  }

  #compare Std Dev to threshold
  if ( $slist->{data}[$page][2]{std_dev} <= $SETTING{'Blank threshold'} ) {
   $slist->select($page);
   $logger->info("Selecting blank page");
  }
  $logger->info( "StdDev: "
     . $slist->{data}[$page][2]{std_dev}
     . " threshold: "
     . $SETTING{'Blank threshold'} );
 }
 return;
}

# Analyse and select dark pages

sub analyse_select_dark {
 analyse( 0, 1 );
 return;
}

# Select dark pages

sub select_dark_pages {
 $slist->get_selection->unselect_all;
 $logger->info("Checking for dark pages now, is analysis done?");
 foreach my $page ( 0 .. $#{ $slist->{data} } ) {
  $logger->info("smp page: $page");
  my $dirty_time   = $slist->{data}[$page][2]{dirty_time};
  my $analyse_time = $slist->{data}[$page][2]{analyse_time};
  $dirty_time   = defined($dirty_time)   ? $dirty_time   : 0;
  $analyse_time = defined($analyse_time) ? $analyse_time : 0;
  if ( $analyse_time le $dirty_time ) {
   $logger->warn(
    $d->get(
     sprintf(
      "Page %d probably needs to be re-Analysed.  Try Update or Analyse.",
      $page + 1 )
    )
   );
   next;
  }

  #compare Mean to threshold
  if ( $slist->{data}[$page][2]{mean} <= $SETTING{'Dark threshold'} ) {
   $slist->select($page);
   $logger->info("Selecting dark page");
  }
 }
 return;
}

# Display about dialog

sub about {
 use utf8;
 my $about = Gtk2::AboutDialog->new;

 # Gtk2::AboutDialog->set_url_hook ($func, $data=undef);
 # Gtk2::AboutDialog->set_email_hook ($func, $data=undef);
 $about->set_program_name($prog_name);
 $about->set_version($VERSION);
 my $authors = <<'EOS';
Frederik Elwert
Klaus Ethgen
Andy Fingerhut
Leon Fisk
John Goerzen
David Hampton
Sascha Hunold
Matthijs Kooijman
Chris Mayo
Pablo Saratxaga
Torsten Schönfeld
Roy Shahbazian
Jarl Stefansson
Jakub Wilk
Hiroshi Miura
EOS
 $about->set_authors( "Jeff Ratcliffe\n\n"
    . $d->get('Patches gratefully received from:')
    . "\n$authors" );
 $about->set_comments( $d->get('To aid the scan-to-PDF process') );
 $about->set_copyright( $d->get('Copyright 2006--2013 Jeffrey Ratcliffe') );
 my $licence = <<'EOS';
gscan2pdf --- to aid the scan to PDF or DjVu process
Copyright 2006 -- 2013 Jeffrey Ratcliffe <Jeffrey.Ratcliffe@gmail.com>

This program is free software: you can redistribute it and/or modify
it under the terms of the version 3 GNU General Public License as
published by the Free Software Foundation.

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 program.  If not, see <http://www.gnu.org/licenses/>.
EOS
 $about->set_license($licence);
 $about->set_website('http://gscan2pdf.sf.net');
 my $translators =
   <<'EOS'; # inverted commas required around EOS because of UTF-8 in $translators
Prescott_SK
Rodrigo Zimmermann
peteradleralberti
mavosaure
Arthur Rodrigues
Utku BERBEROĞLU
Piotr Strębski
Aleksey Kabanov
Сергій Дубик
zdpo
Eric Spierings
Milo Casagrande
Raúl González Duque
R120X
NSV
Alexandre Prokoudine
Paul Wohlhart
Pierre Slamich
Tiago Silva
Igor Zubarev
EOS
 $about->set_translator_credits($translators);
 $about->set_artists('lodp');
 $about->run;
 $about->destroy;
 return;
}

# Check that a command exists

sub check_command {
 return system("which $_[0] >/dev/null 2>/dev/null") == 0 ? TRUE : FALSE;
}

# Dialog for renumber

sub renumber_dialog {
 if ( defined $windowrn ) {
  $windowrn->present;
  return;
 }

 $windowrn = Gscan2pdf::Dialog::Renumber->new(
  'transient-for'  => $window,
  document         => $slist,
  'hide-on-delete' => FALSE,
  border_width     => $border_width
 );

 # Update undo/redo buffers
 $windowrn->signal_connect(
  'before-renumber' => sub {
   take_snapshot();
  }
 );

 $windowrn->show_all;
 return;
}

# Helper function to convert an array of indices into an array of Gscan2pdf::Page objects

sub indices2pages {
 my @indices = @_;
 my @pages;
 for (@indices) {
  push @pages, $slist->{data}[$_][2];
 }
 return @pages;
}

# Rotate selected images

sub rotate {
 my ( $angle, $pagelist, $callback ) = @_;

 # Update undo/redo buffers
 take_snapshot();

 for my $page ( @{$pagelist} ) {
  my ( $signal, $pid );
  $pid = $slist->rotate(
   angle           => $angle,
   page            => $page,
   queued_callback => sub {
    return update_tpbar(@_);
   },
   started_callback => sub {
    my ( $thread, $process, $completed, $total ) = @_;
    $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
    return TRUE if ( defined $signal );
   },
   finished_callback => sub {
    my ( $new_page, $pending ) = @_;
    $callback->($new_page) if $callback;
    $thbox->hide_all unless $pending;
    $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
   },
   error_callback   => \&error_callback,
   display_callback => sub {
    my ($new_page) = @_;
    display_image($new_page);
   },
  );
 }
 return;
}

# Analyse selected images

sub analyse {
 my ( $select_blank, $select_dark ) = @_;

 # Update undo/redo buffers
 take_snapshot();

 foreach my $i ( 0 .. $#{ $slist->{data} } ) {
  my $dirty_time   = $slist->{data}[$i][2]{dirty_time};
  my $analyse_time = $slist->{data}[$i][2]{analyse_time};
  $dirty_time   = defined($dirty_time)   ? $dirty_time   : 0;
  $analyse_time = defined($analyse_time) ? $analyse_time : 0;
  if ( $analyse_time le $dirty_time ) {
   $logger->info(
"Updating: $slist->{data}[$i][0] analyse_time: $analyse_time dirty_time: $dirty_time"
   );
   my ( $signal, $pid );
   $pid = $slist->analyse(
    page            => $slist->{data}[$i][2],
    queued_callback => sub {
     return update_tpbar(@_);
    },
    started_callback => sub {
     my ( $thread, $process, $completed, $total ) = @_;
     $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
     return TRUE if ( defined $signal );
    },
    finished_callback => sub {
     my ( $new_page, $pending ) = @_;
     $thbox->hide_all unless $pending;
     $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
     select_blank_pages()                          if $select_blank;
     select_dark_pages()                           if $select_dark;
    },
    error_callback => \&error_callback,
   );
  }
 }
 return;
}

# Handle right-clicks

sub handle_clicks {
 my ( $widget, $event ) = @_;

 # $SETTING{'RMB'} = ($event->button == 3);
 #warn "rmb $SETTING{'RMB'}\n";

 if ( $event->button == 3 ) {
  if ( $widget->isa('Gtk2::ImageView') ) {    # main image
   $uimanager->get_widget('/Detail_Popup')
     ->popup( undef, undef, undef, undef, $event->button, $event->time );
  }
  else {                                      # Thumbnail simplelist
   $uimanager->get_widget('/Thumb_Popup')
     ->popup( undef, undef, undef, undef, $event->button, $event->time );
  }

  # block event propagation
  return TRUE;
 }

 # allow event propagation
 return FALSE;
}

# Display page selector and on apply threshold accordingly

sub threshold {

 # $SETTING{'Page range'} = 'selected' if $SETTING{'RMB'};

 my $windowt = Gscan2pdf::Dialog->new(
  'transient-for' => $window,
  title           => $d->get('Threshold'),
  border_width    => $border_width
 );
 my $vbox = $windowt->get('vbox');

 # Frame for page range
 add_page_range($vbox);

 # SpinButton for threshold
 my $hboxt = Gtk2::HBox->new;
 $vbox->pack_start( $hboxt, FALSE, TRUE, 0 );
 my $label = Gtk2::Label->new( $d->get('Threshold') );
 $hboxt->pack_start( $label, FALSE, TRUE, 0 );
 my $labelp = Gtk2::Label->new( $d->get('%') );
 $hboxt->pack_end( $labelp, FALSE, TRUE, 0 );
 my $spinbutton = Gtk2::SpinButton->new_with_range( 0, 100, 1 );
 $spinbutton->set_value( $SETTING{'threshold tool'} );
 $hboxt->pack_end( $spinbutton, FALSE, TRUE, 0 );

 # HBox for buttons
 my $hboxb = _hbox_with_apply_cancel_buttons(
  sub {

   # Update undo/redo buffers
   take_snapshot();

   $SETTING{'threshold tool'} = $spinbutton->get_value;

   my @pagelist =
     $slist->get_page_index( $SETTING{'Page range'}, \&error_callback );
   if ( not @pagelist ) { return }
   my $page = 0;
   for my $i (@pagelist) {
    $page++;
    my ( $signal, $pid );
    $pid = $slist->threshold(
     threshold       => $SETTING{'threshold tool'},
     page            => $slist->{data}[$i][2],
     queued_callback => sub {
      return update_tpbar(@_);
     },
     started_callback => sub {
      my ( $thread, $process, $completed, $total ) = @_;
      $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
      return TRUE if ( defined $signal );
     },
     finished_callback => sub {
      my ( $new_page, $pending ) = @_;
      $thbox->hide_all unless $pending;
      $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
     },
     error_callback   => \&error_callback,
     display_callback => sub {
      my ($new_page) = @_;
      display_image($new_page);
     },
    );
   }
  },
  sub { $windowt->destroy }
 );
 $vbox->pack_start( $hboxb, FALSE, TRUE, 0 );
 $windowt->show_all;
 return;
}

# Display page selector and on apply negate accordingly

sub negate {

 # $SETTING{'Page range'} = 'selected' if $SETTING{'RMB'};

 my $windowt = Gscan2pdf::Dialog->new(
  'transient-for' => $window,
  title           => $d->get('Negate'),
  border_width    => $border_width
 );
 my $vbox = $windowt->get('vbox');

 # Frame for page range
 add_page_range($vbox);

 # HBox for buttons
 my $hboxb = _hbox_with_apply_cancel_buttons(
  sub {

   # Update undo/redo buffers
   take_snapshot();

   my @pagelist =
     $slist->get_page_index( $SETTING{'Page range'}, \&error_callback );
   if ( not @pagelist ) { return }
   for my $i (@pagelist) {
    my ( $signal, $pid );
    $pid = $slist->negate(
     page            => $slist->{data}[$i][2],
     queued_callback => sub {
      return update_tpbar(@_);
     },
     started_callback => sub {
      my ( $thread, $process, $completed, $total ) = @_;
      $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
      return TRUE if ( defined $signal );
     },
     finished_callback => sub {
      my ( $new_page, $pending ) = @_;
      $thbox->hide_all unless $pending;
      $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
     },
     error_callback   => \&error_callback,
     display_callback => sub {
      my ($new_page) = @_;
      display_image($new_page);
     },
    );
   }
  },
  sub { $windowt->destroy }
 );
 $vbox->pack_start( $hboxb, FALSE, TRUE, 0 );
 $windowt->show_all;
 return;
}

# Display page selector and on apply unsharp accordingly

sub unsharp {

 # $SETTING{'Page range'} = 'selected' if $SETTING{'RMB'};

 my $windowum = Gscan2pdf::Dialog->new(
  'transient-for' => $window,
  title           => $d->get('Unsharp mask'),
  border_width    => $border_width
 );
 my $vbox = $windowum->get('vbox');

 # Frame for page range
 add_page_range($vbox);

 # SpinButton for radius
 my $hbox = Gtk2::HBox->new;
 $vbox->pack_start( $hbox, FALSE, TRUE, 0 );
 my $label = Gtk2::Label->new( $d->get('Radius') );
 $hbox->pack_start( $label, FALSE, TRUE, 0 );
 $label = Gtk2::Label->new( $d->get('pixels') );
 $hbox->pack_end( $label, FALSE, TRUE, 0 );
 my $spinbuttonr = Gtk2::SpinButton->new_with_range( 0, 100, 1 );
 $spinbuttonr->set_value( $SETTING{'unsharp radius'} );
 $tooltips->set_tip(
  $spinbuttonr,
  $d->get(
'The radius of the Gaussian, in pixels, not counting the center pixel (0 = automatic).'
  )
 );
 $hbox->pack_end( $spinbuttonr, FALSE, TRUE, 0 );

 # SpinButton for sigma
 $hbox = Gtk2::HBox->new;
 $vbox->pack_start( $hbox, FALSE, TRUE, 0 );
 $label = Gtk2::Label->new( $d->get('Sigma') );
 $hbox->pack_start( $label, FALSE, TRUE, 0 );
 $label = Gtk2::Label->new( $d->get('pixels') );
 $hbox->pack_end( $label, FALSE, TRUE, 0 );
 my $spinbuttons = Gtk2::SpinButton->new_with_range( 0, 5, .1 );
 $spinbuttons->set_value( $SETTING{'unsharp sigma'} );
 $tooltips->set_tip( $spinbuttons,
  $d->get('The standard deviation of the Gaussian.') );
 $hbox->pack_end( $spinbuttons, FALSE, TRUE, 0 );

 # SpinButton for amount
 $hbox = Gtk2::HBox->new;
 $vbox->pack_start( $hbox, FALSE, TRUE, 0 );
 $label = Gtk2::Label->new( $d->get('Amount') );
 $hbox->pack_start( $label, FALSE, TRUE, 0 );
 $label = Gtk2::Label->new( $d->get('%') );
 $hbox->pack_end( $label, FALSE, TRUE, 0 );
 my $spinbuttona = Gtk2::SpinButton->new_with_range( 0, 100, 1 );
 $spinbuttona->set_value( $SETTING{'unsharp amount'} );
 $tooltips->set_tip(
  $spinbuttona,
  $d->get(
'The percentage of the difference between the original and the blur image that is added back into the original.'
  )
 );
 $hbox->pack_end( $spinbuttona, FALSE, TRUE, 0 );

 # SpinButton for threshold
 $hbox = Gtk2::HBox->new;
 $vbox->pack_start( $hbox, FALSE, TRUE, 0 );
 $label = Gtk2::Label->new( $d->get('Threshold') );
 $hbox->pack_start( $label, FALSE, TRUE, 0 );
 my $spinbuttont = Gtk2::SpinButton->new_with_range( 0, 1, 0.01 );
 $spinbuttont->set_value( $SETTING{'unsharp threshold'} );
 $tooltips->set_tip(
  $spinbuttont,
  $d->get(
'The threshold, as a fraction of QuantumRange, needed to apply the difference amount.'
  )
 );
 $hbox->pack_end( $spinbuttont, FALSE, TRUE, 0 );

 # HBox for buttons
 my $hboxb = _hbox_with_apply_cancel_buttons(
  sub {

   # Update undo/redo buffers
   take_snapshot();

   $SETTING{'unsharp radius'}    = $spinbuttonr->get_value;
   $SETTING{'unsharp sigma'}     = $spinbuttons->get_value;
   $SETTING{'unsharp amount'}    = $spinbuttona->get_value;
   $SETTING{'unsharp threshold'} = $spinbuttont->get_value;

   my @pagelist =
     $slist->get_page_index( $SETTING{'Page range'}, \&error_callback );
   if ( not @pagelist ) { return }
   for my $i (@pagelist) {
    my ( $signal, $pid );
    $pid = $slist->unsharp(
     page            => $slist->{data}[$i][2],
     radius          => $SETTING{'unsharp radius'},
     sigma           => $SETTING{'unsharp sigma'},
     amount          => $SETTING{'unsharp amount'},
     threshold       => $SETTING{'unsharp threshold'},
     queued_callback => sub {
      return update_tpbar(@_);
     },
     started_callback => sub {
      my ( $thread, $process, $completed, $total ) = @_;
      $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
      return TRUE if ( defined $signal );
     },
     finished_callback => sub {
      my ( $new_page, $pending ) = @_;
      $thbox->hide_all unless $pending;
      $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
     },
     error_callback   => \&error_callback,
     display_callback => sub {
      my ($new_page) = @_;
      display_image($new_page);
     },
    );
   }
  },
  sub { $windowum->destroy }
 );
 $windowum->show_all;
 return;
}

# Callback for change Gtk2::ImageView::Tool

sub change_image_tool_cb {
 my ( $action, $current ) = @_;
 my $value = $current->get_current_value();
 my $tool  = $selector;
 if ( $value == 10 ) {
  $tool = $dragger;
 }
 elsif ( $value == 30 ) {
  $tool = $painter;
 }
 $view->set_tool($tool);
 if ( $value == 20
  and defined( $SETTING{selection} )
  and @{ $SETTING{selection} } == 4 )
 {
  $selector->signal_handler_block( $selector->{selection_changed_signal} );
  $selector->set_selection(
   Gtk2::Gdk::Rectangle->new( @{ $SETTING{selection} } ) );
  $selector->signal_handler_unblock( $selector->{selection_changed_signal} );
 }
 return;
}

# Display page selector and on apply crop accordingly

sub crop {

 if ( defined $windowc ) {
  $windowc->present;
  return;
 }

 # $SETTING{'Page range'} = 'selected' if $SETTING{'RMB'};

 $windowc = Gscan2pdf::Dialog->new(
  'transient-for'  => $window,
  title            => $d->get('Crop'),
  'hide-on-delete' => TRUE,
  border_width     => $border_width
 );
 my $vbox = $windowc->get('vbox');

 # Frame for page range
 add_page_range($vbox);

 # SpinButton for x
 my $hbox = Gtk2::HBox->new;
 $vbox->pack_start( $hbox, FALSE, TRUE, 0 );
 my $label = Gtk2::Label->new( $d->get('x') );
 $hbox->pack_start( $label, FALSE, TRUE, 0 );
 $label = Gtk2::Label->new( $d->get('pixels') );
 $hbox->pack_end( $label, FALSE, TRUE, 0 );
 $sb_selector_x = Gtk2::SpinButton->new_with_range( 0, $current_page->{w}, 1 );
 $tooltips->set_tip( $sb_selector_x,
  $d->get('The x-position of the left hand edge of the crop.') );
 $hbox->pack_end( $sb_selector_x, FALSE, TRUE, 0 );

 # SpinButton for y
 $hbox = Gtk2::HBox->new;
 $vbox->pack_start( $hbox, FALSE, TRUE, 0 );
 $label = Gtk2::Label->new( $d->get('y') );
 $hbox->pack_start( $label, FALSE, TRUE, 0 );
 $label = Gtk2::Label->new( $d->get('pixels') );
 $hbox->pack_end( $label, FALSE, TRUE, 0 );
 $sb_selector_y = Gtk2::SpinButton->new_with_range( 0, $current_page->{h}, 1 );
 $tooltips->set_tip( $sb_selector_y,
  $d->get('The y-position of the top edge of the crop.') );
 $hbox->pack_end( $sb_selector_y, FALSE, TRUE, 0 );

 # SpinButton for w
 $hbox = Gtk2::HBox->new;
 $vbox->pack_start( $hbox, FALSE, TRUE, 0 );
 $label = Gtk2::Label->new( $d->get('Width') );
 $hbox->pack_start( $label, FALSE, TRUE, 0 );
 $label = Gtk2::Label->new( $d->get('pixels') );
 $hbox->pack_end( $label, FALSE, TRUE, 0 );
 $sb_selector_w = Gtk2::SpinButton->new_with_range( 0, $current_page->{w}, 1 );
 $tooltips->set_tip( $sb_selector_w, $d->get('The width of the crop.') );
 $hbox->pack_end( $sb_selector_w, FALSE, TRUE, 0 );

 # SpinButton for h
 $hbox = Gtk2::HBox->new;
 $vbox->pack_start( $hbox, FALSE, TRUE, 0 );
 $label = Gtk2::Label->new( $d->get('Height') );
 $hbox->pack_start( $label, FALSE, TRUE, 0 );
 $label = Gtk2::Label->new( $d->get('pixels') );
 $hbox->pack_end( $label, FALSE, TRUE, 0 );
 $sb_selector_h = Gtk2::SpinButton->new_with_range( 0, $current_page->{h}, 1 );
 $tooltips->set_tip( $sb_selector_h, $d->get('The height of the crop.') );
 $hbox->pack_end( $sb_selector_h, FALSE, TRUE, 0 );

 # Callbacks if the spinbuttons change
 $sb_selector_x->signal_connect(
  'value-changed' => sub {
   $SETTING{selection}[0] = $sb_selector_x->get_value;
   $sb_selector_w->set_range( 0, $current_page->{w} - $SETTING{selection}[0] );
   update_selector();
  }
 );
 $sb_selector_y->signal_connect(
  'value-changed' => sub {
   $SETTING{selection}[1] = $sb_selector_y->get_value;
   $sb_selector_h->set_range( 0, $current_page->{h} - $SETTING{selection}[1] );
   update_selector();
  }
 );
 $sb_selector_w->signal_connect(
  'value-changed' => sub {
   $SETTING{selection}[2] = $sb_selector_w->get_value;
   $sb_selector_x->set_range( 0, $current_page->{w} - $SETTING{selection}[2] );
   update_selector();
  }
 );
 $sb_selector_h->signal_connect(
  'value-changed' => sub {
   $SETTING{selection}[3] = $sb_selector_h->get_value;
   $sb_selector_y->set_range( 0, $current_page->{h} - $SETTING{selection}[3] );
   update_selector();
  }
 );

 $sb_selector_x->set_value( $SETTING{selection}[0] )
   if ( defined $SETTING{selection}[0] );
 $sb_selector_y->set_value( $SETTING{selection}[1] )
   if ( defined $SETTING{selection}[1] );
 $sb_selector_w->set_value( $SETTING{selection}[2] )
   if ( defined $SETTING{selection}[2] );
 $sb_selector_h->set_value( $SETTING{selection}[3] )
   if ( defined $SETTING{selection}[3] );

 # HBox for buttons
 my $hboxb = _hbox_with_apply_cancel_buttons(
  sub {

   return unless ( @{ $SETTING{selection} } );

   # Update undo/redo buffers
   take_snapshot();

   my @pagelist =
     $slist->get_page_index( $SETTING{'Page range'}, \&error_callback );
   if ( not @pagelist ) { return }
   for my $i (@pagelist) {
    my ( $signal, $pid );
    $pid = $slist->crop(
     page            => $slist->{data}[$i][2],
     x               => $SETTING{selection}[0],
     y               => $SETTING{selection}[1],
     w               => $SETTING{selection}[2],
     h               => $SETTING{selection}[3],
     queued_callback => sub {
      return update_tpbar(@_);
     },
     started_callback => sub {
      my ( $thread, $process, $completed, $total ) = @_;
      $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
      return TRUE if ( defined $signal );
     },
     finished_callback => sub {
      my ( $new_page, $pending ) = @_;
      $thbox->hide_all unless $pending;
      $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
     },
     error_callback   => \&error_callback,
     display_callback => sub {
      my ($new_page) = @_;
      display_image($new_page);
     },
    );
   }

  },
  sub { $windowc->hide }
 );
 $vbox->pack_start( $hboxb, FALSE, TRUE, 0 );
 $windowc->show_all;
 return;
}

sub update_selector {
 my $sel = $selector->get_selection;
 $selector->signal_handler_block( $selector->{selection_changed_signal} );
 $selector->set_selection(
  Gtk2::Gdk::Rectangle->new( @{ $SETTING{selection} } ) )
   if ( defined $sel );
 $selector->signal_handler_unblock( $selector->{selection_changed_signal} );
 return;
}

# Run a user-defined tool on the selected images

sub user_defined_tool {
 my ($action) = @_;
 my @pages = $slist->get_selected_indices;

 # Update undo/redo buffers
 take_snapshot();

 my $cmd = $action->get('label');
 for my $i (@pages) {
  my ( $signal, $pid );
  $pid = $slist->user_defined(
   page            => $slist->{data}[$i][2],
   command         => $cmd,
   queued_callback => sub {
    return update_tpbar(@_);
   },
   started_callback => sub {
    my ( $thread, $process, $completed, $total ) = @_;
    $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
    return TRUE if ( defined $signal );
   },
   finished_callback => sub {
    my ( $new_page, $pending ) = @_;
    $thbox->hide_all unless $pending;
    $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
   },
   error_callback   => \&error_callback,
   display_callback => sub {
    my ($new_page) = @_;
    display_image($new_page);
   },
  );
 }
 return;
}

# queue $page to be processed by unpaper

sub unpaper_page {
 my ( $pages, $options, $callback ) = @_;
 $options = '' unless ( defined $options );

 # Update undo/redo buffers
 take_snapshot();

 for my $pageobject ( @{$pages} ) {
  my ( $signal, $pid );
  $pid = $slist->unpaper(
   page            => $pageobject,
   options         => $options,
   queued_callback => sub {
    return update_tpbar(@_);
   },
   started_callback => sub {
    my ( $thread, $process, $completed, $total ) = @_;
    $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
    return TRUE if ( defined $signal );
   },
   finished_callback => sub {
    my ( $new_page, $pending ) = @_;
    $callback->($new_page) if $callback;
    $thbox->hide_all unless $pending;
    $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
   },
   error_callback   => \&error_callback,
   display_callback => sub {
    my ($new_page) = @_;
    display_image($new_page);
   },
  );
 }

 return;
}

# Run unpaper to clean up scan.

sub unpaper {

 if ( defined $windowu ) {
  $windowu->present;
  return;
 }

 $windowu = Gscan2pdf::Dialog->new(
  'transient-for'  => $window,
  title            => $d->get('unpaper'),
  'hide-on-delete' => TRUE,
  border_width     => $border_width
 );
 my $vbox = $windowu->get('vbox');
 $unpaper->add_options($vbox);

 # Frame for page range
 add_page_range($vbox);

 # HBox for buttons
 my $hboxb = Gtk2::HBox->new;
 $vbox->pack_start( $hboxb, FALSE, TRUE, 0 );

 # OK button
 my $sbutton = Gtk2::Button->new_from_stock('gtk-ok');
 $hboxb->pack_start( $sbutton, TRUE, TRUE, 0 );
 $sbutton->signal_connect(
  clicked => sub {

   # Update undo/redo buffers
   take_snapshot();

   # Update $SETTING
   $SETTING{'unpaper options'} = $unpaper->get_options;

   # run unpaper
   my @pagelist = indices2pages(
    $slist->get_page_index( $SETTING{'Page range'}, \&error_callback ) );
   if ( not @pagelist ) { return }
   unpaper_page( \@pagelist, $unpaper->get_cmdline );

   $windowu->hide;
  }
 );

 # Cancel button
 my $cbutton = Gtk2::Button->new_from_stock('gtk-cancel');
 $hboxb->pack_end( $cbutton, FALSE, FALSE, 0 );
 $cbutton->signal_connect( clicked => sub { $windowu->hide; } );

 $windowu->show_all;
 return;
}

# Add $page to the OCR stack, setting it off if not running.

sub ocr_page {
 my ($pages) = @_;

 my ( $signal, $pid );
 my %options = (
  queued_callback => sub {
   return update_tpbar(@_);
  },
  started_callback => sub {
   my ( $thread, $process, $completed, $total ) = @_;
   $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
   return TRUE if ( defined $signal );
  },
  finished_callback => sub {
   my ( $new_page, $pending ) = @_;
   $thbox->hide_all unless $pending;
   $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
  },
  error_callback   => \&error_callback,
  display_callback => sub {
   my ($new_page) = @_;
   my @page = $slist->get_selected_indices;
   create_canvas($new_page)
     if ( @page and $new_page == $slist->{data}[ $page[0] ][2] );
  }
 );
 $options{language} = $SETTING{'ocr language'}
   unless ( $SETTING{'ocr engine'} eq 'gocr' );
 for my $page ( @{$pages} ) {
  $options{page} = $page;
  if ( $SETTING{'ocr engine'} eq 'gocr' ) {
   $pid = $slist->gocr(%options);
  }
  elsif ( $SETTING{'ocr engine'} eq 'tesseract' ) {
   $pid = $slist->tesseract(%options);
  }
  elsif ( $SETTING{'ocr engine'} eq 'ocropus' ) {
   $pid = $slist->ocropus(%options);
  }
  else {    # cuneiform
   $pid = $slist->cuneiform(%options);
  }
 }
 return;
}

# Create a combobox from an array and set the default

sub combobox_from_array {
 my (@array) = @_;

 # Fill ComboBox
 my $combobox = Gtk2::ComboBox->new_text;
 foreach (@array) {
  $combobox->append_text( $_->[1] );
 }
 return $combobox;
}

# Create a combobox from an array and set the default

sub combobox_set_active {
 my ( $combobox, $default, @array ) = @_;

 # Fill ComboBox
 my $i = 0;
 my $o;
 if ( defined($default) ) {
  foreach (@array) {
   $o = $i if ( defined( $_->[0] ) and $_->[0] eq $default );
   ++$i;
  }
 }
 $o = 0 unless ( defined $o );
 $combobox->set_active($o);
 return;
}

# Add hbox for tesseract languages

sub add_tess_languages {
 my ($vbox) = @_;

 my $hbox = Gtk2::HBox->new;
 $vbox->pack_start( $hbox, FALSE, FALSE, 0 );
 my $label = Gtk2::Label->new( $d->get('Language to recognise') );
 $hbox->pack_start( $label, TRUE, TRUE, 0 );

 # Tesseract language files
 my @tesslang;
 for ( sort { $a cmp $b } keys %{ Gscan2pdf::Tesseract->languages } ) {
  push @tesslang, [ $_, $d->get( ${ Gscan2pdf::Tesseract->languages }{$_} ) ];
 }

 # If there are no language files, then we have tesseract-1.0, i.e. English
 unless (@tesslang) {
  push @tesslang, [ undef, $d->get('English') ];
  $logger->info("No tesseract languages found");
 }

 my $combobox = combobox_from_array(@tesslang);
 combobox_set_active( $combobox, $SETTING{'ocr language'}, @tesslang );
 $hbox->pack_end( $combobox, TRUE, TRUE, 0 );
 return $hbox, $combobox, @tesslang;
}

# Add hbox for cuneiform languages

sub add_cf_languages {
 my ($vbox) = @_;

 my $hbox = Gtk2::HBox->new;
 $vbox->pack_start( $hbox, FALSE, FALSE, 0 );
 my $label = Gtk2::Label->new( $d->get('Language to recognise') );
 $hbox->pack_start( $label, TRUE, TRUE, 0 );

 # Tesseract language files
 my @lang;
 for ( sort { $a cmp $b } keys %{ Gscan2pdf::Cuneiform->languages } ) {
  push @lang, [ $_, $d->get( ${ Gscan2pdf::Cuneiform->languages }{$_} ) ];
 }

 my $combobox = combobox_from_array(@lang);
 combobox_set_active( $combobox, $SETTING{'ocr language'}, @lang );
 $hbox->pack_end( $combobox, TRUE, TRUE, 0 );
 return $hbox, $combobox, @lang;
}

# Run OCR on current page and display result

sub OCR {

 if ( defined $windowo ) {
  $windowo->present;
  return;
 }

 $windowo = Gscan2pdf::Dialog->new(
  'transient-for'  => $window,
  title            => $d->get('OCR'),
  'hide-on-delete' => TRUE,
  border_width     => $border_width
 );
 my $vbox = $windowo->get('vbox');

 # OCR engine selection
 my $hboxe = Gtk2::HBox->new;
 $vbox->pack_start( $hboxe, TRUE, TRUE, 0 );
 my $label = Gtk2::Label->new( $d->get('OCR Engine') );
 $hboxe->pack_start( $label, FALSE, FALSE, 0 );
 my $combobe = combobox_from_array(@ocr_engine);
 combobox_set_active( $combobe, $SETTING{'ocr engine'}, @ocr_engine );
 $hboxe->pack_end( $combobe, FALSE, FALSE, 0 );
 my ( $comboboxtl, $hboxtl, @tesslang, $comboboxcl, $hboxcl, @cflang );

 if ( $dependencies{tesseract} ) {
  ( $hboxtl, $comboboxtl, @tesslang ) = add_tess_languages($vbox);
  $combobe->signal_connect(
   changed => sub {
    if ($ocr_engine[ $combobe->get_active ]->[0] eq 'tesseract'
     or $ocr_engine[ $combobe->get_active ]->[0] eq 'ocropus' )
    {
     $hboxtl->show_all;
    }
    else {
     $hboxtl->hide_all;
    }
   }
  );
 }
 if ( $dependencies{cuneiform} ) {
  ( $hboxcl, $comboboxcl, @cflang ) = add_cf_languages($vbox);
  $combobe->signal_connect(
   changed => sub {
    if ( $ocr_engine[ $combobe->get_active ]->[0] eq 'cuneiform' ) {
     $hboxcl->show_all;
    }
    else {
     $hboxcl->hide_all;
    }
   }
  );
 }

 # Frame for page range
 add_page_range($vbox);

 # HBox for buttons
 my $hbox = Gtk2::HBox->new;
 $vbox->pack_start( $hbox, FALSE, TRUE, 0 );

 # Start button
 my $obutton = Gtk2::Button->new( $d->get('Start OCR') );
 $hbox->pack_start( $obutton, TRUE, TRUE, 0 );
 $obutton->signal_connect(
  clicked => sub {
   $SETTING{'ocr engine'}   = $ocr_engine[ $combobe->get_active ]->[0];
   $SETTING{'ocr language'} = $tesslang[ $comboboxtl->get_active ]->[0]
     if ( $SETTING{'ocr engine'} eq 'tesseract'
    or $SETTING{'ocr engine'} eq 'ocropus' );
   $SETTING{'ocr language'} = $cflang[ $comboboxcl->get_active ]->[0]
     if ( $SETTING{'ocr engine'} eq 'cuneiform' );

   # fill $pagelist with filenames depending on which radiobutton is active
   my @pagelist = indices2pages(
    $slist->get_page_index( $SETTING{'Page range'}, \&error_callback ) );
   if ( not @pagelist ) { return }
   ocr_page( \@pagelist );
   $windowo->hide;
  }
 );

 # Close button
 my $cbutton = Gtk2::Button->new_from_stock('gtk-close');
 $hbox->pack_end( $cbutton, FALSE, FALSE, 0 );
 $cbutton->signal_connect( clicked => sub { $windowo->hide; } );

 $windowo->show_all;
 $hboxtl->hide_all
   if (
  defined($hboxtl)
  and not( $ocr_engine[ $combobe->get_active ]->[0] eq 'tesseract'
   or $ocr_engine[ $combobe->get_active ]->[0] eq 'ocropus' )
   );
 $hboxcl->hide_all
   if ( defined($hboxcl)
  and not( $ocr_engine[ $combobe->get_active ]->[0] eq 'cuneiform' ) );
 return;
}

# Remove temporary files, note window state, save settings and quit.

sub quit {

 # Check that all pages have been saved
 for ( my $i = 0 ; $i < @{ $slist->{data} } ; $i++ ) {
  if ( not $slist->{data}[$i][2]{saved} ) {
   my $response =
     show_message_dialog( $window, 'question', 'ok-cancel',
    $d->get("Some pages have not been saved.\nDo you really want to quit?") );
   if ( $response ne 'ok' ) {
    return FALSE;
   }
   else {
    last;
   }
  }
 }

 # Make sure that we are back in the start directory, otherwise we can't delete
 # the temp dir.
 chdir $SETTING{'cwd'};

 # Remove temporary files (for some reason File::Temp wasn't doing its job here)
 unlink <$session/*>;
 rmdir $session;

 # Write window state to settings
 ( $SETTING{'window_width'}, $SETTING{'window_height'} ) = $window->get_size;
 ( $SETTING{'window_x'}, $SETTING{'window_y'} ) = $window->get_position;
 $SETTING{'thumb panel'} = $hpaned->get_position;

 # delete $SETTING{'RMB'};

 # Write config file
 $conf->save_file( $rc, \%SETTING );
 $logger->info("Wrote config to $ENV{'HOME'}/.$prog_name");

 kill_threads();
 kill_subs();
 $logger->debug("Quitting");
 return TRUE;
}

# kill all threads

sub kill_threads {
 $logger->info("Killing Sane thread(s)");
 Gscan2pdf::Frontend::Sane->quit();
 $logger->info("Killing document thread(s)");
 Gscan2pdf::Document->quit();
 return;
}

# View POD

sub view_pod {

 if ( defined $windowh ) {
  $windowh->present;
  return;
 }

 try { require Gtk2::Ex::PodViewer }
 catch {
  show_message_dialog(
   $window, 'error', 'close',
   sprintf(
    $d->get(
         "The help viewer requires module Gtk2::Ex::PodViewer\n"
       . "Alternatively, try: %s %s\n\n"
    ),
    $prog_name,
    "--help"
   )
  );
  return;
 };

 # Window
 $windowh = Gtk2::Window->new;
 $windowh->set_transient_for($window);    # Assigns parent
 $windowh->signal_connect(
  delete_event => sub {
   $windowh->hide;
   return TRUE;    # ensures that the window is not destroyed
  }
 );
 $windowh->set_default_size( 800, 600 );

 # Vertical divider between index and viewer
 my $pane = Gtk2::HPaned->new;
 $pane->set_position(200);
 $windowh->add($pane);

 # Index list
 my $index = Gtk2::Ex::Simple::List->new(
  'icon'  => 'pixbuf',
  'title' => 'text',
  'link'  => 'hstring'
 );
 $index->set_headers_visible(FALSE);
 $index->get_column(1)->set_sizing('autosize');

 # Index
 my $index_scrwin = Gtk2::ScrolledWindow->new;
 $index_scrwin->set_shadow_type('in');
 $index_scrwin->set_policy( 'automatic', 'automatic' );
 $index_scrwin->add_with_viewport($index);
 $index_scrwin->get_child->set_shadow_type('none');

 # Viewer
 my $viewer = Gtk2::Ex::PodViewer->new;
 $viewer->set_border_width($border_width);
 $viewer->set_cursor_visible(FALSE);
 $index->get_selection->signal_connect(
  'changed',
  sub {
   my $idx  = ( $index->get_selected_indices )[0];
   my $mark = $index->{data}[$idx][2];
   $viewer->jump_to($mark);
   return TRUE;
  }
 );

 my $viewer_scrwin = Gtk2::ScrolledWindow->new;
 $viewer_scrwin->set_shadow_type('in');
 $viewer_scrwin->set_policy( 'automatic', 'automatic' );
 $viewer_scrwin->add($viewer);

 $pane->add1($index_scrwin);
 $pane->add2($viewer_scrwin);

 $viewer->load($0);

 # Index contents
 my $idx_pbf = Gtk2::Image->new->render_icon( 'gtk-jump-to', 'menu' );
 map { push( @{ $index->{data} }, [ $idx_pbf, strippod($_), $_ ] ) }
   $viewer->get_marks;

 $windowh->show_all;
 return;
}

# Remove formatting characters

sub strippod {
 my $text = shift;
 $text =~ s/B<([^<]*)>/$1/gx;
 $text =~ s/E<gt>/>/gx;
 return $text;
}

# Update undo/redo buffers before doing something

sub take_snapshot {

 # Deep copy the tied data. Otherwise, very bad things happen.
 @undo_buffer = map { [@$_] } @{ $slist->{data} };
 @undo_selection = $slist->get_selected_indices;
 $logger->debug( Dumper( \@undo_buffer ) );

 # Unghost Undo/redo
 $uimanager->get_widget('/MenuBar/Edit/Undo')->set_sensitive(TRUE);
 $uimanager->get_widget('/ToolBar/Undo')->set_sensitive(TRUE);

 # Save session
 $slist->save_session;
 return;
}

# Put things back to last snapshot after updating redo buffer

sub undo {
 $logger->info("Undoing");

 # Deep copy the tied data. Otherwise, very bad things happen.
 @redo_buffer = map { [@$_] } @{ $slist->{data} };
 @redo_selection = $slist->get_selected_indices;
 $logger->debug("redo_selection, undo_selection:");
 $logger->debug( Dumper( \@redo_selection, \@undo_selection ) );
 $logger->debug("redo_buffer, undo_buffer:");
 $logger->debug( Dumper( \@redo_buffer, \@undo_buffer ) );

 # Block slist signals whilst updating
 $slist->get_model->signal_handler_block( $slist->{row_changed_signal} );
 @{ $slist->{data} } = @undo_buffer;

 # Unblock slist signals now finished
 $slist->get_model->signal_handler_unblock( $slist->{row_changed_signal} );

 # Reselect the pages to display the detail view
 $slist->select(@undo_selection);

 # Update menus/buttons
 update_uimanager();
 $uimanager->get_widget('/MenuBar/Edit/Undo')->set_sensitive(FALSE);
 $uimanager->get_widget('/MenuBar/Edit/Redo')->set_sensitive(TRUE);
 $uimanager->get_widget('/ToolBar/Undo')->set_sensitive(FALSE);
 $uimanager->get_widget('/ToolBar/Redo')->set_sensitive(TRUE);
 return;
}

# Put things back to last snapshot after updating redo buffer

sub unundo {
 $logger->info("Redoing");

 # Deep copy the tied data. Otherwise, very bad things happen.
 @undo_buffer = map { [@$_] } @{ $slist->{data} };
 @undo_selection = $slist->get_selected_indices;
 $logger->debug("redo_selection, undo_selection:");
 $logger->debug( Dumper( \@redo_selection, \@undo_selection ) );
 $logger->debug("redo_buffer, undo_buffer:");
 $logger->debug( Dumper( \@redo_buffer, \@undo_buffer ) );

 # Block slist signals whilst updating
 $slist->get_model->signal_handler_block( $slist->{row_changed_signal} );
 @{ $slist->{data} } = @redo_buffer;

 # Unblock slist signals now finished
 $slist->get_model->signal_handler_unblock( $slist->{row_changed_signal} );

 # Reselect the pages to display the detail view
 $slist->select(@redo_selection);

 # Update menus/buttons
 update_uimanager();
 $uimanager->get_widget('/MenuBar/Edit/Undo')->set_sensitive(TRUE);
 $uimanager->get_widget('/MenuBar/Edit/Redo')->set_sensitive(FALSE);
 $uimanager->get_widget('/ToolBar/Undo')->set_sensitive(TRUE);
 $uimanager->get_widget('/ToolBar/Redo')->set_sensitive(FALSE);
 return;
}

# Initialise IconFactory

sub init_icons {
 my @icons = @_;
 return if defined $IconFactory;

 $IconFactory = Gtk2::IconFactory->new();
 $IconFactory->add_default();

 foreach (@icons) {
  register_icon( $_->[0], $_->[1] );
 }
 return;
}

# Add icons

sub register_icon {
 my ( $stock_id, $path ) = @_;

 return unless defined $IconFactory;

 my $icon;
 try { $icon = Gtk2::Gdk::Pixbuf->new_from_file($path); }
 catch {
  $logger->warn("Unable to load icon `$path': $_");
  return;
 };
 $IconFactory->add( $stock_id, Gtk2::IconSet->new_from_pixbuf($icon) );
 return;
}

# We should clean up after ourselves so that we don't
# leave dead processes flying around.
sub kill_subs {

 foreach ( keys %helperTag ) {
  $logger->info("Sending TERM signal to PID $_ and its children");
  killfam 'TERM', ($_);
 }
 return;
}

# marked page list as saved

sub mark_pages {
 my @pages = @_;
 $slist->get_model->signal_handler_block( $slist->{row_changed_signal} );
 foreach (@pages) {
  $slist->{data}[$_][2]{saved} = TRUE;
 }
 $slist->get_model->signal_handler_unblock( $slist->{row_changed_signal} );
 return;
}

# Convert all files in temp that are not jpg, png, or tiff to png,

sub compress_temp {
 return
   if (
  show_message_dialog(
   $window,     'question',
   'ok-cancel', $d->get('This operation cannot be undone. Are you sure?')
  ) ne 'ok'
   );
 @undo_buffer    = ();
 @undo_selection = ();
 $uimanager->get_widget('/MenuBar/Edit/Undo')->set_sensitive(FALSE);
 $uimanager->get_widget('/ToolBar/Undo')->set_sensitive(FALSE);

 for ( @{ $slist->{data} } ) {
  my ( $signal, $pid );
  $pid = $slist->to_png(
   page            => $_->[2],
   queued_callback => sub {
    return update_tpbar(@_);
   },
   started_callback => sub {
    my ( $thread, $process, $completed, $total ) = @_;
    $signal = setup_tpbar( $thread, $process, $completed, $total, $pid );
    return TRUE if ( defined $signal );
   },
   finished_callback => sub {
    my ( $new_page, $pending ) = @_;
    $thbox->hide_all unless $pending;
    $tcbutton->signal_handler_disconnect($signal) if ( defined $signal );
   },
   error_callback => \&error_callback,
  );
 }
 return;
}

# Expand tildes in the filename

sub expand_tildes {
 my ($filename) = @_;
 $filename =~ s{ ^ ~ ( [^/]* ) } {
  $1 ? (getpwnam($1))[7] : ( $ENV{HOME} || $ENV{LOGDIR} || (getpwuid($>))[7] )
 }ex;
 return $filename;
}

# Preferences dialog

sub preferences {    ## no critic (ProhibitExcessComplexity)

 if ( defined $windowr ) {
  $windowr->present;
  return;
 }

 $windowr = Gscan2pdf::Dialog->new(
  'transient-for'  => $window,
  title            => $d->get('Preferences'),
  'hide-on-delete' => TRUE,
  border_width     => $border_width
 );
 my $vbox = $windowr->get('vbox');

 # Notebook for scan and general options
 my $notebook = Gtk2::Notebook->new;
 $vbox->pack_start( $notebook, TRUE, TRUE, 0 );
 my $vbox1 = Gtk2::VBox->new;
 $notebook->append_page( $vbox1, $d->get('Scan options') );
 $vbox1->set_border_width($border_width);

 my $cbo =
   Gtk2::CheckButton->new_with_label(
  $d->get('Open scan dialog at program start') );
 $tooltips->set_tip(
  $cbo,
  $d->get(
       "Automatically open the scan dialog in the background at program start. "
     . "This saves time clicking the scan button and waiting for the program to find the list of scanners"
  )
 );
 $cbo->set_active( $SETTING{'auto-open-scan-dialog'} )
   if ( defined $SETTING{'auto-open-scan-dialog'} );
 $vbox1->pack_start( $cbo, TRUE, TRUE, 0 );

 # Frontends
 my $hbox = Gtk2::HBox->new;
 $vbox1->pack_start( $hbox, FALSE, FALSE, 0 );
 my $label = Gtk2::Label->new( $d->get('Frontend') );
 $hbox->pack_start( $label, FALSE, FALSE, 0 );
 my @frontends = (
  [
   'libsane-perl', $d->get('libsane-perl'),
   $d->get('Scan using the perl bindings for SANE.')
  ],
  [
   'scanimage', $d->get('scanimage'),
   $d->get('Scan using the scanimage frontend.')
  ],
  [
   'scanimage-perl',
   $d->get('scanimage-perl'),
   $d->get('Scan using the scanimage-perl frontend.')
  ],
  [
   'scanadf-perl', $d->get('scanadf-perl'),
   $d->get('Scan using the scanadf-perl frontend.')
  ],
 );
 push @frontends,
   [
  'scanadf', $d->get('scanadf'),
  $d->get('Scan using the scanadf frontend.')
   ],
   if ( $dependencies{scanadf} );
 my $combob = combobox_from_array(@frontends);
 my $cbcsh =
   Gtk2::CheckButton->new_with_label( $d->get('Cycle SANE handle after scan') );
 $hbox->pack_end( $combob, TRUE, TRUE, 0 );
 $tooltips->set_tip( $hbox, $d->get("Interface used for scanner access") );

 # Cycle SANE handle after scan
 $cbcsh->set_active( $SETTING{'cycle sane handle'} )
   if ( defined $SETTING{'cycle sane handle'} );
 $vbox1->pack_start( $cbcsh, FALSE, FALSE, 0 );

 # scan command prefix
 my $hboxp = Gtk2::HBox->new;
 $vbox1->pack_start( $hboxp, FALSE, FALSE, 0 );
 $label = Gtk2::Label->new( $d->get('Scan command prefix') );
 $hboxp->pack_start( $label, FALSE, FALSE, 0 );
 my $preentry = Gtk2::Entry->new;
 $hboxp->add($preentry);
 $preentry->set_text( $SETTING{'scan prefix'} )
   if defined( $SETTING{'scan prefix'} );

 # Cache options?
 my $cbc = Gtk2::CheckButton->new_with_label(
  $d->get('Cache device-dependent options') );
 $cbc->set_active(TRUE) if ( $SETTING{'cache options'} );
 $vbox1->pack_start( $cbc, FALSE, FALSE, 0 );

 # Clear options cache
 my $buttonc =
   Gtk2::Button->new( $d->get('Clear device-dependent options cache') );
 $vbox1->pack_start( $buttonc, FALSE, FALSE, 0 );
 $buttonc->signal_connect(
  clicked => sub {
   delete $SETTING{cache} if ( defined $SETTING{cache} );
   $windows->set( 'options-cache', undef ) if ( defined $windows );
  }
 );

 # Option visibility
 my $oframe = Gtk2::Frame->new( $d->get('Option visibility & control') );
 $vbox1->pack_start( $oframe, TRUE, TRUE, 0 );
 my $vvbox = Gtk2::VBox->new;
 $vvbox->set_border_width($border_width);
 $oframe->add($vvbox);
 my $scwin = Gtk2::ScrolledWindow->new;
 $vvbox->pack_start( $scwin, TRUE, TRUE, 0 );
 $scwin->set_policy(qw/never automatic/);
 $option_visibility_list = Gtk2::Ex::Simple::List->new(
  $d->get('Title')  => 'text',
  $d->get('Type')   => 'text',
  $d->get('Show')   => 'bool',
  $d->get('Reload') => 'bool'
 );
 $option_visibility_list->get_selection->set_mode('multiple');
 $scwin->add($option_visibility_list);
 my $bhbox = Gtk2::HBox->new;
 $vvbox->pack_start( $bhbox, FALSE, FALSE, 0 );
 my $sbutton = Gtk2::Button->new( $d->get('Show') );
 $sbutton->signal_connect(
  clicked => sub {

   for ( $option_visibility_list->get_selected_indices ) {
    $option_visibility_list->{data}[$_][2] = TRUE;
   }
  }
 );
 $bhbox->pack_start( $sbutton, TRUE, TRUE, 0 );
 my $hbutton = Gtk2::Button->new( $d->get('Hide') );
 $hbutton->signal_connect(
  clicked => sub {
   for ( $option_visibility_list->get_selected_indices ) {
    $option_visibility_list->{data}[$_][2] = FALSE;
   }
  }
 );
 $bhbox->pack_start( $hbutton, TRUE, TRUE, 0 );
 my $fbutton = Gtk2::Button->new( $d->get('List current optionts') );
 $fbutton->signal_connect(
  clicked => sub {
   if ( defined $windows ) {
    @{ $option_visibility_list->{data} } = ();
    my $options = $windows->get('available-scan-options');
    for ( my $i = 1 ; $i < $options->num_options ; ++$i ) {
     my $opt = $options->by_index($i);
     push @{ $option_visibility_list->{data} },
       [ $opt->{title}, $opt->{type}, TRUE, FALSE ];
    }
    push @{ $option_visibility_list->{data} },
      [ 'Paper size', undef, TRUE, FALSE ];
   }
   else {
    show_message_dialog( $windowr, 'error',
     'close',
     $d->get('No scanner currently open with command line frontend.') );
   }
   return;
  }
 );
 $bhbox->pack_start( $fbutton, TRUE, TRUE, 0 );
 my $show_not_listed =
   Gtk2::CheckButton->new_with_label( $d->get('Show options not listed') );
 $vvbox->pack_start( $show_not_listed, FALSE, FALSE, 0 );

 # fill the list
 if ( defined $SETTING{'visible-scan-options'} ) {
  my %reload;
  $SETTING{'scan-reload-triggers'} = [ $SETTING{'scan-reload-triggers'} ]
    if ( ref( $SETTING{'scan-reload-triggers'} ) ne 'ARRAY' );
  for ( @{ $SETTING{'scan-reload-triggers'} } ) {
   $reload{$_} = 1;
  }
  for ( sort { $a cmp $b } keys %{ $SETTING{'visible-scan-options'} } ) {
   push @{ $option_visibility_list->{data} },
     [ $_, undef, $SETTING{'visible-scan-options'}{$_},
    defined( $reload{$_} ) ];
  }
 }

 $combob->signal_connect(
  changed => sub {
   if ( $frontends[ $combob->get_active ][0] eq 'libsane-perl' ) {
    $cbcsh->set_sensitive(TRUE);
    $hboxp->set_sensitive(FALSE);
    $cbc->set_sensitive(FALSE);
    $buttonc->set_sensitive(FALSE);
    $oframe->set_sensitive(FALSE);
   }
   else {
    $cbcsh->set_sensitive(FALSE);
    $hboxp->set_sensitive(TRUE);
    $cbc->set_sensitive(TRUE);
    $buttonc->set_sensitive(TRUE);
    $oframe->set_sensitive(TRUE);
   }
  }
 );
 combobox_set_active( $combob, $SETTING{frontend}, @frontends );

 my $vbox2 = Gtk2::VBox->new;
 $notebook->append_page( $vbox2, $d->get('General options') );
 $vbox2->set_border_width($border_width);

 # Restore window setting
 my $cbw = Gtk2::CheckButton->new_with_label(
  $d->get('Restore window settings on startup') );
 $cbw->set_active(TRUE) if ( $SETTING{'restore window'} );
 $vbox2->pack_start( $cbw, TRUE, TRUE, 0 );

 # View saved files
 my $cbv = Gtk2::CheckButton->new_with_label( $d->get('View files on saving') );
 $cbv->set_active( $SETTING{'view files toggle'} )
   if ( defined $SETTING{'view files toggle'} );
 $vbox2->pack_start( $cbv, TRUE, TRUE, 0 );

 # Default filename
 $hbox = Gtk2::HBox->new;
 $vbox2->pack_start( $hbox, TRUE, TRUE, 0 );
 $label = Gtk2::Label->new( $d->get('Default filename for PDFs') );
 $hbox->pack_start( $label, FALSE, FALSE, 0 );
 my $fileentry = Gtk2::Entry->new;
 $tooltips->set_tip(
  $fileentry,
  $d->get(
"\%a\t author\n\%t\t title\n\%y\t document's year\n\%Y\t today's year\n\%m\t document's month\n\%M\t today's month\n\%d\t document's day\n\%D\t today's day"
  )
 );
 $hbox->add($fileentry);
 $fileentry->set_text( $SETTING{'default filename'} )
   if defined( $SETTING{'default filename'} );

 # Temporary directory settings
 $hbox = Gtk2::HBox->new;
 $vbox2->pack_start( $hbox, TRUE, TRUE, 0 );
 $label = Gtk2::Label->new( $d->get('Temporary directory') );
 $hbox->pack_start( $label, FALSE, FALSE, 0 );
 my $tmpentry = Gtk2::Entry->new;
 $hbox->add($tmpentry);
 $tmpentry->set_text( dirname($session) );
 my $button = Gtk2::Button->new( $d->get('Browse') );
 $button->signal_connect(
  clicked => sub {
   my $file_chooser = Gtk2::FileChooserDialog->new(
    $d->get('Select temporary directory'),
    $windowr, 'select-folder',
    'gtk-cancel' => 'cancel',
    'gtk-ok'     => 'ok'
   );
   $file_chooser->set_current_folder( $tmpentry->get_text );
   if ( 'ok' eq $file_chooser->run ) {
    $tmpentry->set_text( $file_chooser->get_filename );
   }
   $file_chooser->destroy;
  }
 );
 $hbox->pack_end( $button, TRUE, TRUE, 0 );

 # Blank page standard deviation threshold
 $hbox = Gtk2::HBox->new;
 $vbox2->pack_start( $hbox, TRUE, TRUE, 0 );
 $label = Gtk2::Label->new( $d->get('Blank threshold') );
 $hbox->pack_start( $label, FALSE, FALSE, 0 );
 my $spinbuttonb = Gtk2::SpinButton->new_with_range( 0, 1, 0.001 );
 $spinbuttonb->set_value( $SETTING{'Blank threshold'} );
 $tooltips->set_tip( $spinbuttonb,
  $d->get('Threshold used for selecting blank pages') );
 $hbox->add($spinbuttonb);

 # Dark page mean threshold
 $hbox = Gtk2::HBox->new;
 $vbox2->pack_start( $hbox, TRUE, TRUE, 0 );
 $label = Gtk2::Label->new( $d->get('Dark threshold') );
 $hbox->pack_start( $label, FALSE, FALSE, 0 );
 my $spinbuttond = Gtk2::SpinButton->new_with_range( 0, 1, 0.01 );
 $spinbuttond->set_value( $SETTING{'Dark threshold'} );
 $tooltips->set_tip( $spinbuttond,
  $d->get('Threshold used for selecting dark pages') );
 $hbox->add($spinbuttond);

 # OCR output
 $hbox = Gtk2::HBox->new;
 $vbox2->pack_start( $hbox, TRUE, TRUE, 0 );
 $label = Gtk2::Label->new( $d->get('OCR output') );
 $hbox->pack_start( $label, FALSE, FALSE, 0 );
 my @array = (
  [
   'replace',
   $d->get('Replace'),
   $d->get(
    'Replace the contents of the text buffer with that from the OCR output.')
  ],
  [
   'prepend', $d->get('Prepend'),
   $d->get('Prepend the OCR output to the text buffer.')
  ],
  [
   'append', $d->get('Append'),
   $d->get('Append the OCR output to the text buffer.')
  ],
 );
 my $comboo = combobox_from_array(@array);
 combobox_set_active( $comboo, $SETTING{'OCR output'}, @array );
 $hbox->pack_end( $comboo, TRUE, TRUE, 0 );

 # User-defined tools
 my $frame = Gtk2::Frame->new( $d->get('User-defined tools') );
 $vbox2->pack_start( $frame, TRUE, TRUE, 0 );
 my $vboxt = Gtk2::VBox->new;
 $vboxt->set_border_width($border_width);
 $frame->add($vboxt);
 for my $tool (@user_defined_tools) {
  add_user_defined_tool_entry( $vboxt, $tool );
 }
 my $abutton = Gtk2::Button->new_from_stock('gtk-add');
 $vboxt->pack_start( $abutton, TRUE, TRUE, 0 );
 $abutton->signal_connect(
  clicked => sub {
   my %tool = ( cmd => 'my-tool %i %o' );
   add_user_defined_tool_menu( \%tool );
   add_user_defined_tool_entry( $vboxt, \%tool );
   $vboxt->reorder_child( $abutton, -1 );
   $SETTING{user_defined_tools} = list_user_defined_tools($vboxt);
  }
 );

 # Apply button
 $hbox = _hbox_with_apply_cancel_buttons(
  sub {
   $windowr->hide;
   if ( $SETTING{frontend} ne $frontends[ $combob->get_active ][0] ) {
    $SETTING{frontend} = $frontends[ $combob->get_active ][0];
    $windows->hide;
    undef $windows;
   }
   else {
    $SETTING{'visible-scan-options'} = ();
    $SETTING{'scan-reload-triggers'} = ();
    for ( @{ $option_visibility_list->{data} } ) {
     $SETTING{'visible-scan-options'}{ $_->[0] } = $_->[2];
     push @{ $SETTING{'scan-reload-triggers'} }, $_->[0] if ( $_->[3] );
    }
    if ( $SETTING{frontend} ne 'libsane-perl' ) {
     $windows->set( 'visible-scan-options', $SETTING{'visible-scan-options'} );
     $windows->set( 'reload-triggers',      $SETTING{'scan-reload-triggers'} );
     $SETTING{'scan prefix'}   = $preentry->get_text;
     $SETTING{'cache options'} = $cbc->get_active;
     if ( defined $windows ) {
      $windows->set( 'prefix',        $SETTING{'scan prefix'} );
      $windows->set( 'cache-options', $SETTING{'cache options'} );
     }
     delete $SETTING{cache}
       if ( defined $SETTING{cache} and not $SETTING{'cache options'} );
    }
   }
   $SETTING{'auto-open-scan-dialog'} = $cbo->get_active;
   $SETTING{'cycle sane handle'}     = $cbcsh->get_active;
   $SETTING{'default filename'}      = $fileentry->get_text;
   $SETTING{'restore window'}        = $cbw->get_active;

   my @tmpdirs = File::Spec->splitdir($session);
   pop @tmpdirs;    # Remove the top level
   my $tmp = File::Spec->catdir(@tmpdirs);

   # Expand tildes in the filename
   my $newdir = expand_tildes( $tmpentry->get_text );

   if ( $newdir ne $tmp ) {
    $SETTING{TMPDIR} = $newdir;
    show_message_dialog(
     $window,
     'warning',
     'close',
     $d->get(
'You will have to restart gscanp2df for changes to the temporary directory to take effect.'
     )
    );
   }
   $SETTING{'Blank threshold'} = $spinbuttonb->get_value;
   $SETTING{'Dark threshold'}  = $spinbuttond->get_value;
   $SETTING{'OCR output'}      = $array[ $comboo->get_active ][0];

   # Store viewer preferences
   $SETTING{'view files toggle'} = $cbv->get_active;
  },
  sub {
   $windowr->hide;
  }
 );
 $vbox->pack_start( $hbox, FALSE, FALSE, 0 );
 $windowr->show_all;
 return;
}

sub _hbox_with_apply_cancel_buttons {
 my ( $apply_callback, $cancel_callback ) = @_;

 my $hbox = Gtk2::HBox->new;

 # Apply button
 my $button = Gtk2::Button->new_from_stock('gtk-apply');
 $hbox->pack_start( $button, TRUE, TRUE, 0 );
 $button->signal_connect( clicked => $apply_callback );

 # Cancel button
 $button = Gtk2::Button->new_from_stock('gtk-cancel');
 $hbox->pack_end( $button, TRUE, TRUE, 0 );
 $button->signal_connect( clicked => $cancel_callback );
 return $hbox;
}

# Return list of user-defined tools
sub list_user_defined_tools {
 my ($vbox) = @_;
 my @list;
 for my $hbox ( $vbox->get_children ) {
  if ( $hbox->isa('Gtk2::HBox') ) {
   for my $widget ( $hbox->get_children ) {
    push @list, $widget->get_text if ( $widget->isa('Gtk2::Entry') );
   }
  }
 }
 return \@list;
}

# Add user-defined tool menu item
sub add_user_defined_tool_menu {
 my ($tool) = @_;
 my $action_group = Gtk2::ActionGroup->new("actions");
 $tool->{name} = "$action_group";
 my $tool_ui = <<"EOS";
<ui>
 <menubar name='MenuBar'>
  <menu action='Tools'>
   <menu action='User-defined'>
    <menuitem action='$tool->{name}'/>
   </menu>
  </menu>
 </menubar>
</ui>
EOS

 # add the basic XML description of the GUI
 $tool->{merge_id} = $uimanager->add_ui_from_string($tool_ui);

 my @action_item = (

  # Fields for each action item:
  # [name, stock_id, value, label, accelerator, tooltip, callback]
  [ $tool->{name}, undef, $tool->{cmd}, undef, undef, \&user_defined_tool ],
 );
 $action_group->add_actions( \@action_item, undef );

 # Add the actiongroup to the uimanager
 $uimanager->insert_action_group( $action_group, 0 );
 $tool->{action_group} = $action_group;
 return;
}

# Add user-defined tool entry
sub add_user_defined_tool_entry {
 my ( $vbox, $tool ) = @_;
 my $hbox = Gtk2::HBox->new;
 $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
 my $entry = Gtk2::Entry->new;
 $entry->set_text( $tool->{cmd} );
 $entry->signal_connect(
  changed => sub {
   my $action = $tool->{action_group}->get_action( $tool->{name} );
   $action->set( 'label', $entry->get_text );
   $SETTING{user_defined_tools} = list_user_defined_tools($vbox);
   ()    # this callback must return either 2 or 0 items.
  }
 );

 $tooltips->set_tip(
  $entry,
  $d->get(
"Use \%i and \%o for the input and output filenames respectively, or a single \%i if the image is to be modified in-place.\n\nThe other variable available is:\n\n\%r resolution"
  )
 );
 $hbox->pack_start( $entry, TRUE, TRUE, 0 );
 my $button = Gtk2::Button->new;
 $button->set_image( Gtk2::Image->new_from_stock( 'gtk-delete', 'button' ) );
 $button->signal_connect(
  clicked => sub {
   $uimanager->remove_ui( $tool->{merge_id} );
   $hbox->destroy;
   $SETTING{user_defined_tools} = list_user_defined_tools($vbox);
  }
 );
 $hbox->pack_end( $button, FALSE, FALSE, 0 );
 $hbox->show_all;
 return;
}

sub properties {

 if ( defined $windowp ) {
  $windowp->present;
  return;
 }

 $windowp = Gscan2pdf::Dialog->new(
  'transient-for'  => $window,
  title            => $d->get('Properties'),
  'hide-on-delete' => TRUE,
  border_width     => $border_width
 );
 my $vbox = $windowp->get('vbox');

 my $hbox = Gtk2::HBox->new;
 $vbox->pack_start( $hbox, TRUE, TRUE, 0 );
 my $label = Gtk2::Label->new( $d->get('Resolution') );
 $hbox->pack_start( $label, FALSE, FALSE, 0 );
 my $entry = Gtk2::Entry->new;
 $entry->set_width_chars(4);
 $entry->set_activates_default(TRUE);
 $entry->signal_connect(
  'insert-text' => sub {
   my ( $widget, $string, $len, $position ) = @_;

   # only allow a valid positive real number
   for ( split '', $string ) {
    if (( $_ ne '.' and ( $_ lt '0' or $_ gt '9' ) )
     or ( $_ eq '.' and $entry->get_text =~ /\./ ) )
    {
     $entry->signal_stop_emission_by_name('insert-text');
     last;
    }
   }
   ()    # this callback must return either 2 or 0 items.
  }
 );

 $entry->set_text( get_selected_properties() );
 $slist->get_selection->signal_connect(
  changed => sub {
   $entry->set_text( get_selected_properties() );
  }
 );
 $hbox->pack_end( $entry, TRUE, TRUE, 0 );

 $hbox = Gtk2::HBox->new;
 $vbox->pack_start( $hbox, TRUE, TRUE, 0 );

 # Apply button
 my $button = Gtk2::Button->new_from_stock('gtk-apply');
 $button->can_default(TRUE);
 $windowp->set_default($button);
 $hbox->pack_start( $button, TRUE, TRUE, 0 );
 $button->signal_connect(
  clicked => sub {
   $windowp->hide;
   my $resolution = $entry->get_text;
   $slist->get_model->signal_handler_block( $slist->{row_changed_signal} );
   for ( $slist->get_selected_indices ) {
    $slist->{data}[$_][2]{resolution} = $resolution;
   }
   $slist->get_model->signal_handler_unblock( $slist->{row_changed_signal} );
  }
 );

 # Cancel button
 $button = Gtk2::Button->new_from_stock('gtk-cancel');
 $hbox->pack_end( $button, TRUE, TRUE, 0 );
 $button->signal_connect(
  clicked => sub {
   $windowp->hide;
  }
 );
 $windowp->show_all;
 return;
}

# Helper function for properties()
sub get_selected_properties {
 my @page       = $slist->get_selected_indices;
 my $resolution = '';
 $resolution = $slist->{data}[ shift @page ][2]{resolution} if ( @page > 0 );
 for (@page) {
  if ( $slist->{data}[$_][2]{resolution} != $resolution ) {
   $resolution = '';
   last;
  }
 }

 # round the value to a sensible number of significant figures
 return $resolution eq '' ? '' : sprintf( "%g", $resolution );
}

# Helper function to display a message dialog, wait for a response, and return it

sub show_message_dialog {
 my ( $parent, $type, $buttons, $text ) = @_;
 my $dialog =
   Gtk2::MessageDialog->new( $parent, 'destroy-with-parent', $type, $buttons,
  $text );
 my $response = $dialog->run;
 $dialog->destroy;
 return $response;
}

Gtk2->main;

__END__

=encoding utf8

=head1 Name

gscan2pdf - A GUI to produce PDFs or DjVus from scanned documents

=for html <p align="center">
 <img src="http://sourceforge.net/dbimage.php?id=249848" border="1" width="632"
 height="480" alt="Screenshot" /><br/>Screenshot: Main page v0.9.30</p>

=head1 Synopsis

=over

=item 1. Scan one or several pages in with File/Scan

=item 2. Create PDF of selected pages with File/Save

=back

=head1 Description

gscan2pdf has the following command-line options:

=over

=item --device=<device>
Specifies the device to use, instead of getting the list of devices from via the SANE API.
This can be useful if the scanner is on a remote computer which is not broadcasting its existence.

=item --help
Displays this help page and exits.

=item --log=<log file>
Specifies a file to store logging messages.

=item --(debug|info|warn|error|fatal)
Defines the log level. If a log file is specified, this defaults to 'debug', otherwise 'warn'.

=item --version
Displays the program version and exits.

=back

Scanning is handled with SANE via scanimage.
PDF conversion is done by PDF::API2.
TIFF export is handled by libtiff (faster and smaller memory footprint for
multipage files).

=head1 Download

gscan2pdf is available on Sourceforge
(L<http://sourceforge.net/projects/gscan2pdf/files/gscan2pdf/>).

=head2 Debian-based

If you are using Debian, you should find that sid has the latest version already
packaged.

If you are using a Ubuntu-based system, you can automatically keep up to date
with the latest version via the ppa:

C<sudo apt-add-repository ppa:jeffreyratcliffe/ppa>

If you are you are using Synaptic, then use menu
I<Edit/Reload Package Information>, search for gscan2pdf in the package list,
and lo and behold, you can install the nice shiny new version.

From the command line:

C<sudo apt-get update>

C<sudo apt-get install gscan2pdf>

=head2 RPMs

Download the rpm from Sourceforge, and then install it with
C<rpm -i gscan2pdf-version.rpm>

=head2 From source

The source is hosted in the files section of the gscan2pdf project on
Sourceforge (L<http://sourceforge.net/projects/gscan2pdf/files/>).

=head2 From the repository

gscan2pdf uses Git for its Revision Control System. You can browse the
tree at L<http://sourceforge.net/p/gscan2pdf/code/>.

Git users can clone the complete tree with
C<git clone git://git.code.sf.net/p/gscan2pdf/code>

=head1 Building gscan2pdf from source

Having downloaded the source either from a Sourceforge file release, or from the
Git repository, unpack it if necessary with
C<tar xvfz gscan2pdf-x.x.x.tar.gz
cd gscan2pdf-x.x.x>

C<perl Makefile.PL>, will create the Makefile.
There is a C<make test>, but this is not machine-dependent, and therefore really
just for my benefit to make sure I haven't broken the device-dependent options
parsing routine.

You can install directly from the source with C<make install>, but building the
appropriate package for your distribution should be as straightforward as
C<make debdist> or C<make rpmdist>. However, you will
additionally need the rpm, devscripts, fakeroot, debhelper and gettext packages.

=head1 Dependencies

The list below looks daunting, but all packages are available from any
reasonable up-to-date distribution. If you are using Synaptic, having installed
gscan2pdf, locate the gscan2pdf entry in Synaptic, right-click it and you can
install them under I<Recommends>. Note also that the library names given below
are the Debian/Ubuntu ones. Those distributions using RPM typically use
perl(module) where Debian has libmodule-perl.

=over

=item Required

=over

=item libgtk2.0-0 (>= 2.4)

The GTK+ graphical user interface library.

=item libglib-perl (>= 1.100-1)

Perl interface to the GLib and GObject libraries

=item libgtk2-perl (>= 1:1.043-1)

Perl interface to the 2.x series of the Gimp Toolkit library

=item libgtk2-imageview-perl

Perl bindings to the gtkimageview widget.
See L<http://trac.bjourne.webfactional.com/>

=item libgtk2-ex-simple-list-perl

A simple interface to Gtk2's complex MVC list widget

=item liblocale-gettext-perl (>= 1.05)

Using libc functions for internationalization in Perl

=item libpdf-api2-perl

provides the functions for creating PDF documents in Perl

=item libsane

API library for scanners

=item libsane-perl

Perl bindings for libsane.

=item libset-intspan-perl

manages sets of integers

=item libtiff-tools

TIFF manipulation and conversion tools

=item Imagemagick

Image manipulation programs

=item perlmagick

A perl interface to the libMagick graphics routines

=item sane-utils

API library for scanners -- utilities.

=back

=item Optional

=over

=item sane

scanner graphical frontends. Only required for the scanadf frontend.

=item libgtk2-ex-podviewer-perl

Perl Gtk2 widget for displaying Plain Old Documentation (POD). Not required if
you don't need the gscan2pdf documentation (which is anyway repeated on the
website).

=item unpaper

post-processing tool for scanned pages. See L<http://unpaper.berlios.de/>.

=item xdg-utils

Desktop integration utilities from freedesktop.org. Required for Email as PDF.
See L<http://portland.freedesktop.org/wiki/>

=item djvulibre-bin

Utilities for the DjVu image format. See L<http://djvu.sourceforge.net/>

=item gocr

A command line OCR. See L<http://jocr.sourceforge.net/>.

=item tesseract

A command line OCR. See L<http://code.google.com/p/tesseract-ocr/>

=item ocropus

A command line OCR. See L<http://code.google.com/p/ocropus/>

=item cuneiform

A command line OCR. See L<http://launchpad.net/cuneiform-linux>

=back

=back

=head1 Support

There are two mailing lists for gscan2pdf:

=over

=item gscan2pdf-announce

A low-traffic list for announcements, mostly of new releases. You can subscribe
at L<http://lists.sourceforge.net/lists/listinfo/gscan2pdf-announce>

=item gscan2pdf-help

General support, questions, etc.. You can subscribe at
L<http://lists.sourceforge.net/lists/listinfo/gscan2pdf-help>

=back

=head1 Reporting bugs

Before reporting bugs, please read the L<"FAQs"> section.

Please report any bugs found, preferably against the Debian package[1][2].
You do not need to be a Debian user, or set up an account to do this.

=over

=item 1. http://packages.debian.org/sid/gscan2pdf

=item 2. http://www.debian.org/Bugs/

=back

Alternatively, there is a bug tracker for the gscan2pdf project on
Sourceforge (L<http://sourceforge.net/p/gscan2pdf/_list/tickets?source=navbar>).

Please include the log file created by C<gscan2pdf --log=log> with any new bug report.

=head1 Translations

gscan2pdf has already been partly translated several languages.
If you would like to contribute to an existing or new translation, please check
out Rosetta: L<https://translations.launchpad.net/gscan2pdf>

Note that the translations for the scanner options are taken
directly from sane-backends. If you would like to contribute to these, you can
do so either at contact the sane-devel mailing list
(sane-devel@lists.alioth.debian.org) and have a look at the po/ directory in
the source code L<http://www.sane-project.org/cvs.html>.

Alternatively, Ubuntu has its own translation project. For the 9.04 release, the
translations are available at
L<https://translations.launchpad.net/ubuntu/jaunty/+source/sane-backends/+pots/sane-backends>

=head1 Menus

=head2 File

=head3 New

Clears the page list.

=head3 Open

Opens any format that imagemagick supports. PDFs will have their embedded
images extracted and imported one per page.

=head3 Scan

Sets options before scanning via SANE.

=head4 Device

Chooses between available scanners.

=head4 # Pages

Selects the number of pages, or all pages to scan.

=head4 Source document

Selects between single sided or double sides pages.

This affects the page numbering.
Single sided scans are numbered consecutively.
Double sided scans are incremented (or decremented, see below) by 2, i.e. 1, 3,
5, etc..

=head4 Side to scan

If double sided is selected above, assuming a non-duplex scanner, i.e. a
scanner that cannot automatically scan both sides of a page, this determines
whether the page number is incremented or decremented by 2.

To scan both sides of three pages, i.e. 6 sides:

=over

=item 1. Select:

# Pages = 3 (or "all" if your scanner can detect when it is out of paper)

Double sided

Facing side

=item 2. Scans sides 1, 3 & 5.

=item 3. Put pile back with scanner ready to scan back of last page.

=item 4. Select:

# Pages = 3 (or "all" if your scanner can detect when it is out of paper)

Double sided

Reverse side

=item 5. Scans sides 6, 4 & 2.

=item 6. gscan2pdf automatically sorts the pages so that they appear in the
correct order.

=back

=head4 Device-dependent options

These, naturally, depend on your scanner.
They can include

=over

=item Page size.

=item Mode (colour/black & white/greyscale)

=item Resolution (in PPI)

=item Batch-scan

Guarantees that a "no documents" condition will be returned after the last
scanned page, to prevent endless flatbed scans after a batch scan.

=item Wait-for-button/Button-wait

After sending the scan command, wait until the button on the scanner is pressed
before actually starting the scan process.

=item Source

Selects the document source.
Possible options can include Flatbed or ADF.
On some scanners, this is the only way of generating an out-of-documents signal.

=back

=head3 Save

Saves the selected or all pages as a PDF, DjVu, TIFF, PNG, JPEG, PNM or
GIF.

=head4 PDF Metadata

Metadata are information that are not visible when viewing the PDF, but are
embedded in the file and so searchable and can be examined, typically with the
"Properties" option of the PDF viewer.

The metadata are completely optional, but can also be used to generate the
filename see preferences for details.

=head4 DjVu

Both black and white, and colour images produce better
compression than PDF. See L<http://www.djvuzone.org/> for more details.

=head3 Email as PDF

Attaches the selected or all pages as a PDF to a blank email.
This requires xdg-email, which is in the xdg-utils package.
If this is not present, the option is ghosted out.

=head3 Print

Prints the selected or all pages.

=head3 Compress temporary files

If your temporary ($TMPDIR) directory is getting full, this function can be useful -
compressing all images at LZW-compressed TIFFs. These require much less space than
the PNM files that are typically produced by SANE or by importing a PDF.

=head2 Edit

=head3 Delete

Deletes the selected page.

=head3 Renumber

Renumbers the pages from 1..n.

Note that the page order can also be changed by drag and drop in the thumbnail
view.

=head3 Select

The select menus can be used to select, all, even, odd, blank, dark or modified
pages. Selecting blank or dark pages runs imagemagick to make the decision.
Selecting modified pages selects those which have modified by threshold,
unsharp, etc., since the last OCR run was made.

=head3 Preferences

The preferences menu item allows the control of the default behviour of various
functions. Most of these are self-explanatory.

=head4 Frontend

gscan2pdf supports two frontends, scanimage and scanadf.
scanadf support was added when it was realised that scanadf works better than
scanimage with some scanners. On Debian-based systems, scanadf is in the sane package,
not, like scanimage, in sane-utils. If scanadf is not present, the option is
obviously ghosted out.

In 0.9.27, Perl bindings for SANE were introduced and two further frontends,
scanimage-perl and scanadf-perl (scanimage and scanadf transliterated from C into
Perl) were added.

Before 1.2.0, options available through CLI frontends like scanimage were made
visible as users asked for them. In 1.2.0, all options can be shown or hidden
via Edit/Preferences, along with the ability to specify which options trigger a
reload.

=head4 Default filename for PDF files

The following variables are available, which are replaced by the corresponding
metadata:

 %a	author
 %t	title
 %y	document's year
 %Y	today's year
 %m	document's month
 %M	today's month
 %d	document's day
 %D	today's day

=head2 View

=head3 Zoom 100%

Zooms to 1:1. How this appears depends on the desktop resolution.

=head3 Zoom to fit

Scales the view such that all the page is visible.

=head3 Zoom in

=head3 Zoom out

=head3 Rotate 90 clockwise

The rotate options require the package imagemagick and, if this is not present,
are ghosted out.

=head3 Rotate 180

=head3 Rotate 90 anticlockwise

=head2 Tools

=head3 Threshold

Changes all pixels darker than the given value to black; all others become
white.

=head3 Unsharp mask

The unsharp option sharpens an image. The image is convolved with a Gaussian
operator of the given radius and standard deviation (sigma). For reasonable
results, radius should be larger than sigma. Use a radius of 0 to have the
method select a suitable radius.

=head3 Crop

=head3 unpaper

unpaper (see L<http://unpaper.berlios.de/>) is a utility for cleaning up a scan.

=head3 OCR (Optical Character Recognition)

The gocr, tesseract, ocropus or cuneiform utilities are used to produce text from
an image.

There is an OCR output buffer for each page and is embedded as
plain text behind the scanned image in the PDF
produced. This way, Beagle can index (i.e. search) the plain text.

In DjVu files, the OCR output buffer is embedded in the hidden text layer.
Thus these can also be indexed by Beagle.

There is an interesting review of OCR software at
L<http://web.archive.org/web/20080529012847/http://groundstate.ca/ocr>.
An important conclusion was that 400ppi is necessary for decent results.

Up to v2.04, the only way to tell which languages were available to tesseract
was to look for the language files. Therefore, gscan2pdf checks the path
returned by:

 tesseract '' '' -l ''

If there are no language files in the above location, then gscan2pdf
assumes that tesseract v1.0 is installed, which had no language files.

=head3 Variables for user-defined tools

The following variables are available:

 %i	input filename
 %o	output filename
 %r	resolution

An image can be modified in-place by just specifying %i.


=head1 FAQs

=head2 Why isn't option xyz available in the scan window?

Possibly because SANE or your scanner doesn't support it.

If an option listed in the output of C<scanimage --help> that you would like to
use isn't available, send me the output and I will look at implementing it.

=head2 I've only got an old flatbed scanner with no automatic sheetfeeder.
How do I scan a multipage document?

If you are lucky, you have an option like Wait-for-button or Button-wait, where
the scanner will wait for you to press the scan button on the device before it
starts the scan, allowing you to scan multiple pages without touching the
computer.

Otherwise, you have to set the number of pages to scan to 1 and hit the scan
button on the scan window for each page.

=head2 Why is option xyz ghosted out?

Probably because the package required for that option is not installed.
Email as PDF requires xdg-email (xdg-utils), unpaper and the rotate options
require imagemagick.

=head2 Why can I not scan from the flatbed of my HP scanner?

Generally for HP scanners with an ADF, to scan from the flatbed, you should
set "# Pages" to "1", and possibly "Batch scan" to "No".

=head2 When I update gscan2pdf using the Update Manager in Ubuntu, why is the list of changes never displayed?

As far as I can tell, this is pulled from changelogs.ubuntu.com, and therefore
only the changelogs from official Ubuntu builds are displayed.

=head2 Why can gscan2pdf not find my scanner?

If your scanner is not connected directly to the machine on which you are
running gscan2pdf and you have not installed the SANE daemon, saned,
gscan2pdf cannot automatically find it. In this case, you can specify the
scanner device on the command line:

C<gscan2pdf --device <device>>

=head2 How can I search for text in the OCR layer of the finished PDF or DJVU file?

pdftotext or djvutxt can extract the text layer from PDF or DJVU files. See the
respective man pages for details.

Having opened a PDF or DJVU file in evince or Acrobat Reader, the search
function will typically find the page with the requested text and highlight it.

There are various tools for searching or indexing files, including PDF and DJVU:

=over

=item *
(meta) Tracker (L<https://projects.gnome.org/tracker/>)

=item *
plone (L<http://plone.org/>)

=item *
pdfgrep (L<http://pdfgrep.sourceforge.net/>

=item *
swish-e (L<http://www.swish-e.org/>)

=item *
recoll (L<http://www.lesbonscomptes.com/recoll/>)

=item *
terrier (L<http://www.lesbonscomptes.com/recoll/>)

=back

=head1 See Also

 Xsane
 http://scantailor.sourceforge.net/

=head1 Author

Jeffrey Ratcliffe (ra28145 at users dot sf dot net)

=head1 Thanks to

=over

=item *
all the people who have sent patches, translations, bugs and feedback.

=item *
the GTK2 project for a most excellent graphics toolkit.

=item *
the Gtk2-Perl project for their superb Perl bindings for GTK2.

=item *
The SANE project for scanner access

=item *
Björn Lindqvist for the gtkimageview widget

=item *
Sourceforge for hosting the project.

=back

=for html <hr />
 <a href="http://sourceforge.net/projects/gscan2pdf">
 <img src="http://sflogo.sourceforge.net/sflogo.php?group_id=174140&amp;type=14" width="150" height="40" alt="Get gscan2pdf at SourceForge.net. Fast, secure and Free Open Source software downloads" /></a>
 <a href="http://sourceforge.net/p/gscan2pdf/donate/">
 <img src="http://sourceforge.net/images/project-support.jpg" width="88" height="32" border="0" alt="Support This Project"></a>

=cut
