Update year
[clawsker.git] / clawsker
index 91e4d73a109c438ab9bf6f2e55b84ac3eba3954f..7e4e9e9903439aef1f8ca8d09e46fe4152f8d762 100755 (executable)
--- a/clawsker
+++ b/clawsker
@@ -1,7 +1,7 @@
 #!/usr/bin/perl -w
 #
 # Clawsker :: A Claws Mail Tweaker
-# Copyright 2007-2017 Ricardo Mones <ricardo@mones.org>
+# Copyright 2007-2018 Ricardo Mones <ricardo@mones.org>
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -65,6 +65,10 @@ sub _ {
     about_license => _('License:'),
     about_version => _('Version:'),
 
+    exit_title => _('Clawsker warning'),
+    exit_fact => _('There are unapplied modifications.'),
+    exit_question => _('Do you really want to quit?'),
+
     tab_colours => _('Colours'),
     tab_behaviour => _('Behaviour'),
     tab_gui => _('GUI'),
@@ -72,6 +76,7 @@ sub _ {
     tab_winpos => _('Windows'),
     tab_accounts => _('Accounts'),
     tab_plugins => _('Plugins'),
+    tab_hotkeys => _('Hotkeys'),
     tab_info => _('Info'),
 
     ab_frame => _('Addressbook'),
@@ -279,8 +284,14 @@ my $ACCOUNTRC = 'accountrc';
 # supported and available plugins lists
 my @PLUGINS = qw(AttRemover GPG ManageSieve Libravatar PerlPlugin);
 my @AVPLUGINS = ();
+# loaded hotkeys from load_menurc
+my $HOTKEYS;
+# current tree selection
+my $SELHOTKEY;
 # loaded icons
 my @APPICONS = ();
+# modification flag
+my $MODIFIED = 0;
 
 # index constants for preference arrays
 use constant NAME  => 0; # the name on the rc file
@@ -296,6 +307,10 @@ use constant HBOX_PAD => 5;
 use constant FRAME_SPC => 2;
 use constant PAGE_SPC => 5;
 
+# for data references indexing
+use constant VALUE => 0;
+use constant IVALUE => 1;
+
 # version functions
 
 sub version_greater_or_equal {
@@ -331,7 +346,9 @@ sub get_claws_version {
 
 sub handle_bool_value {
     my ($widget, $event, $dataref) = @_;
-    $$dataref = ($widget->get_active ())? '1': '0';
+    $$dataref->[VALUE] = ($widget->get_active ())? '1': '0';
+    $MODIFIED += $$dataref->[VALUE] != $$dataref->[IVALUE]? 1: -1
+        if $$dataref->[IVALUE];
 }
 
 sub handle_int_value {
@@ -340,24 +357,30 @@ sub handle_int_value {
     s/^\s+//;
     s/\s+$//;
     if (/^[0-9]+$/) {
-        $$dataref = $_;
+        $$dataref->[VALUE] = $_;
         $widget->set_text ($_);
     }
     else {
-        $widget->set_text ($$dataref);
+        $widget->set_text ($$dataref->[VALUE]);
     }
+    $MODIFIED += $$dataref->[VALUE] != $$dataref->[IVALUE]? 1: -1
+        if $$dataref->[IVALUE];
 }
 
 sub handle_string_value {
     my ($widget, $event, $dataref) = @_;
-    $$dataref = $widget->get_text ();
+    $$dataref->[VALUE] = $widget->get_text ();
+    $MODIFIED += $$dataref->[VALUE] ne $$dataref->[IVALUE]? 1: -1
+        if $$dataref->[IVALUE];
 }
 
 sub handle_nchar_value {
     my ($widget, $event, $dataref, $minlen, $maxlen) = @_;
     $_ = substr ($widget->get_text (), 0, $maxlen);
     $widget->set_text ($_);
-    $$dataref = $_;
+    $$dataref->[VALUE] = $_;
+    $MODIFIED += $$dataref->[VALUE] ne $$dataref->[IVALUE]? 1: -1
+        if $$dataref->[IVALUE];
 }
 
 sub gdk_color_from_str {
@@ -385,12 +408,16 @@ sub str_from_gdk_color {
 sub handle_color_value {
     my ($widget, $event, $dataref) = @_;
     my $newcol = $widget->get_color;
-    $$dataref = str_from_gdk_color ($newcol);
+    $$dataref->[VALUE] = str_from_gdk_color ($newcol);
+    $MODIFIED += $$dataref->[VALUE] ne $$dataref->[IVALUE]? 1: -1
+        if $$dataref->[IVALUE];
 }
 
 sub handle_selection_value {
     my ($widget, $event, $dataref) = @_;
-    $$dataref = $widget->get_active;
+    $$dataref->[VALUE] = $widget->get_active;
+    $MODIFIED += $$dataref->[VALUE] ne $$dataref->[IVALUE]? 1: -1
+        if $$dataref->[IVALUE];
 }
 
 sub get_rc_filename {
@@ -501,7 +528,7 @@ sub new_check_button_for($$$) {
     my $label = $$hash{$key}[LABEL];
     #
     my $cb = Gtk2::CheckButton->new ($label);
-    my $value = $$vhash{$name};
+    my $value = $$vhash{$name}[VALUE];
     $value //= $$hash{$key}[CMDEF];
     $cb->set_active ($value eq '1');
     $cb->signal_connect (clicked => sub {
@@ -529,7 +556,7 @@ sub new_text_box_for_int($$$) {
     my $glabel = Gtk2::Label->new ($label);
     my $pagei = int (($type[2] - $type[1]) / 10);
     my $gentry = Gtk2::SpinButton->new_with_range ($type[1], $type[2], $pagei);
-    my $value = $$vhash{$name};
+    my $value = $$vhash{$name}[VALUE];
     $value //= $$hash{$key}[CMDEF];
     $gentry->set_numeric (TRUE);
     $gentry->set_value ($value);
@@ -557,7 +584,7 @@ sub new_text_box_for_nchar($$$) {
     my $width = $type[3];
     $width //= $type[2];
     $gentry->set_width_chars(int ($width) + 2) if defined ($width);
-    my $value = $$vhash{$name};
+    my $value = $$vhash{$name}[VALUE];
     $value //= $$hash{$key}[CMDEF];
     $gentry->set_text ($value);
     $gentry->signal_connect('key-release-event' => sub {
@@ -576,7 +603,7 @@ sub new_color_button_for($$$) {
     my $name = $$hash{$key}[NAME];
     my $label = $$hash{$key}[LABEL];
     #
-    my $value = $$vhash{$name};
+    my $value = $$vhash{$name}[VALUE];
     $value //= $$hash{$key}[CMDEF];
     my $col = gdk_color_from_str ($value);
     my $glabel = Gtk2::Label->new ($label);
@@ -610,7 +637,7 @@ sub new_selection_box_for($$$) {
             my ($w, $e) = @_;
             handle_selection_value ($w, $e, \$$vhash{$name});
         });
-    my $value = $$vhash{$name};
+    my $value = $$vhash{$name}[VALUE];
     $value //= $$hash{$key}[CMDEF];
     $combo->set_active ($value);
     set_widget_hint ($combo, $$hash{$key}[DESC]);
@@ -681,7 +708,7 @@ sub new_subpage_frame {
         'cache_max_mem_usage',
         [ $xl::s{l_oth_max_use}, $xl::s{l_oth_max_use_units} ],
         $xl::s{h_oth_max_use},
-        'int,0,262144', # 0 Kb - 256 Mb
+        'int,0,524288', # 0 Kb - 512 Mb
         '0.7.8.36',
         '4096',
     ],
@@ -876,7 +903,7 @@ sub new_other_page() {
         $xl::s{h_gui_warn_send_multi},
         'int,0,1000',
         '3.14.1.125',
-        '0',
+        '3.15.0.28',
     ],
     next_del => [
         'next_on_delete',
@@ -1235,18 +1262,18 @@ sub new_behaviour_page() {
         '#000000',
     ],
     qs_error_bg => [
-        '',
+        'qs_error_bgcolor',
         $xl::s{l_col_qs_error_bg},
         $xl::s{h_col_qs_error_bg},
-        'qs_error_bgcolor',
+        'color',
         '3.14.1.31',
         '#ff7070',
     ],
     qs_error_text => [
-        '',
+        'qs_error_color',
         $xl::s{l_col_qs_error_text},
         $xl::s{h_col_qs_error_text},
-        'qs_error_color',
+        'color',
         '3.14.1.31',
         '#000000',
     ],
@@ -1695,6 +1722,22 @@ sub new_colours_page() {
         '0.0.0',
         '-1',
     ],
+    acio_w => [
+        'actionsiodialog_width',
+        $xl::s{l_win_w},
+        $xl::s{h_win_w},
+        'int,0,3000', # 0 pixels - 3000 pixels
+        '0.0.0',
+        '582',
+    ],
+    acio_h => [
+        'actionsiodialog_height',
+        $xl::s{l_win_h},
+        $xl::s{h_win_h},
+        'int,0,3000', # 0 pixels - 3000 pixels
+        '0.0.0',
+        '310',
+    ],
     tags_w => [
         'tagswin_width',
         $xl::s{l_win_w},
@@ -1894,6 +1937,21 @@ sub new_winpos_subpage_filtering() {
            );
 }
 
+sub new_winpos_subpage_useractions() {
+    return new_vbox_pack (
+                new_subpage_frame (
+                     new_hbox_pack (
+                          new_text_box_for_int (\%pr::win, 'acti_w', \%HPVALUE),
+                          new_text_box_for_int (\%pr::win, 'acti_h', \%HPVALUE)),
+                     _('User Actions prefs window'), 'not-packed'),
+                new_subpage_frame (
+                     new_hbox_pack (
+                          new_text_box_for_int (\%pr::win, 'acio_w', \%HPVALUE),
+                          new_text_box_for_int (\%pr::win, 'acio_h', \%HPVALUE)),
+                     _('User Actions I/O window'), 'not-packed')
+           );
+}
+
 sub new_winpos_subpage_prefs() {
     return new_vbox_pack (
                 new_subpage_frame (
@@ -1906,11 +1964,6 @@ sub new_winpos_subpage_prefs() {
                           new_text_box_for_int (\%pr::win, 'temp_w', \%HPVALUE),
                           new_text_box_for_int (\%pr::win, 'temp_h', \%HPVALUE)),
                      _('Templates window'), 'not-packed'),
-                new_subpage_frame (
-                     new_hbox_pack (
-                          new_text_box_for_int (\%pr::win, 'acti_w', \%HPVALUE),
-                          new_text_box_for_int (\%pr::win, 'acti_h', \%HPVALUE)),
-                     _('Actions window'), 'not-packed'),
                 new_subpage_frame (
                      new_hbox_pack (
                           new_text_box_for_int (\%pr::win, 'tags_w', \%HPVALUE),
@@ -1954,6 +2007,7 @@ sub new_winpos_page() {
     $winbook->append_page (new_winpos_subpage_addrbook, _('Addressbook'));
     $winbook->append_page (new_winpos_subpage_accounts, _('Accounts'));
     $winbook->append_page (new_winpos_subpage_filtering, _('Filtering'));
+    $winbook->append_page (new_winpos_subpage_useractions, _('User Actions'));
     $winbook->append_page (new_winpos_subpage_prefs, _('Preferences'));
     $winbook->append_page (new_winpos_subpage_misc, _('Other'));
     return $winbook;
@@ -2120,6 +2174,123 @@ sub new_plugins_page() {
                 $frame{'PerlPlugin'});
 }
 
+use constant {
+    C_LABEL => 0,
+    C_HOTKEY => 1,
+    C_GROUP => 2,
+    C_ACCEL => 3,
+    C_BCOLOR => 4,
+    # cell backgrounds
+    BG_LIGHTER => '#ffffff',
+    BG_DARKER => '#eeeeee'
+};
+
+sub new_hotkeys_list_label {
+    my $renderer = Gtk2::CellRendererText->new ();
+    $renderer->set_property('alignment' => 'left');
+    $renderer->set_property('editable' => FALSE);
+    return $renderer;
+}
+
+sub new_hotkeys_list_hotkey {
+    my $renderer = Gtk2::CellRendererAccel->new ();
+    $renderer->set_property ('accel-mode' => 'gtk');
+    $renderer->set_property ('editable' => TRUE);
+    $renderer->signal_connect ('accel-edited' => sub {
+        my ($w, $path, $key, $mods, $keycode) = @_;
+        my $accel = Gtk2::Accelerator->name ($key, $mods);
+        my ($model, $iter) = $SELHOTKEY->get_selected ();
+        $model->set($iter, C_HOTKEY, "\"$accel\"");
+        my $gkey = $model->get_value ($iter, C_GROUP);
+        my $akey = $model->get_value ($iter, C_ACCEL);
+        my $data = $HOTKEYS->{$gkey}->{$akey};
+        $data->{'key'} = "\"$accel\"";
+        $data->{'enabled'} = TRUE;
+    });
+    $renderer->signal_connect ('accel-cleared' => sub {
+        my ($w, $path) = @_;
+        my ($model, $iter) = $SELHOTKEY->get_selected ();
+        $model->set($iter, C_HOTKEY, "\"\"");
+        my $gkey = $model->get_value ($iter, C_GROUP);
+        my $akey = $model->get_value ($iter, C_ACCEL);
+        my $data = $HOTKEYS->{$gkey}->{$akey};
+        $data->{'key'} = "\"\"";
+        $data->{'enabled'} = FALSE;
+    });
+    return $renderer;
+}
+
+sub new_hotkeys_list {
+    my ($gkey, $group) = @_;
+    my $store = Gtk2::ListStore->new(
+        qw/Glib::String Glib::String Glib::String Glib::String Glib::String/);
+    my $even = FALSE;
+    foreach my $akey (sort keys %$group) {
+        my $iter = $store->append ();
+        my $hotkey = $group->{$akey}->{'key'};
+        my $label = $akey;
+        $label =~ s/[<>]//g; # <rrsyl> and <IMAPFolder> !?
+        my $bgcol = $even ? BG_DARKER: BG_LIGHTER;
+        $store->set ($iter, C_LABEL, $label, C_HOTKEY, $hotkey,
+            C_GROUP, $gkey, C_ACCEL, $akey, C_BCOLOR, $bgcol);
+        $even = not $even;
+    }
+    my $treeview = Gtk2::TreeView->new_with_model ($store);
+    # labels column
+    $treeview->insert_column_with_data_func (
+        0, _("Menu path"), new_hotkeys_list_label (),
+        sub {
+            my ($col, $renderer, $model, $iter, $data) = @_;
+            $renderer->set_property (
+                'markup' => '<span size="smaller">'
+                            . $model->get_value ($iter, C_LABEL)
+                            . '</span>');
+            $renderer->set_property (
+                'background' => $model->get_value ($iter, C_BCOLOR));
+        }
+    );
+    # hotkeys column
+    $treeview->insert_column_with_data_func (
+        1, _('Hotkey'), new_hotkeys_list_hotkey (),
+        sub {
+            my ($col, $renderer, $model, $iter, $data) = @_;
+            my $hkey = $model->get_value ($iter, C_HOTKEY);
+            $hkey =~ s/\"//g;
+            my ($acckey, $accmod) = Gtk2::Accelerator->parse ($hkey);
+            $renderer->set_property ('accel-key' => $acckey);
+            $renderer->set_property ('accel-mods' => $accmod);
+            $renderer->set_property (
+                'background' => $model->get_value ($iter, C_BCOLOR));
+        }
+    );
+    # callback for saving current selection
+    my $selection = $treeview->get_selection ();
+    $selection->signal_connect ('changed' => sub { $SELHOTKEY = shift });
+    return $treeview;
+}
+
+sub new_hotkeys_page() {
+    my $swin = Gtk2::ScrolledWindow->new ();
+    my $vbox = Gtk2::VBox->new (FALSE, 5);
+    foreach my $gkey (sort keys %$HOTKEYS) {
+        my $group = $HOTKEYS->{$gkey};
+        # group title
+        my $glabel = Gtk2::Label->new ('<b>' . $gkey . '</b>');
+        $glabel->set_use_markup (TRUE);
+        $glabel->set_alignment (0, 0.5);
+        $glabel->set_padding (5, 1);
+        $vbox->pack_start ($glabel, FALSE, FALSE, 0);
+        # group key list
+        my $keylist = new_hotkeys_list ($gkey, $group);
+        $vbox->pack_start ($keylist, FALSE, FALSE, 0);
+    }
+    $swin->set_border_width (5);
+    $swin->set_shadow_type ('none');
+    $swin->set_policy ('automatic', 'always');
+    $swin->add_with_viewport ($vbox);
+    return $swin;
+}
+
 sub new_info_page() {
     my $t0 = Gtk2::Table->new (7, 2, FALSE);
     my $v = get_toolkit_versions ();
@@ -2282,20 +2453,23 @@ sub opt_clawsrc {
 sub init_hidden_preferences {
     foreach my $hash (\%pr::beh, \%pr::col, \%pr::gui, \%pr::oth, \%pr::win) {
         foreach my $key (keys %$hash) {
-            $HPVALUE{${$hash}{$key}[NAME]} = $PREFS{${$hash}{$key}[NAME]};
+            $HPVALUE{${$hash}{$key}[NAME]}[VALUE] = $PREFS{${$hash}{$key}[NAME]};
+            $HPVALUE{${$hash}{$key}[NAME]}[IVALUE] = $PREFS{${$hash}{$key}[NAME]};
         }
     }
     foreach my $akey (keys %ACPREFS) {
         foreach my $key (keys %pr::acc) {
             my $pname = $pr::acc{$key}[NAME];
-            $ACHPVALUE{$akey}{$pname} = $ACPREFS{$akey}{$pname};
+            $ACHPVALUE{$akey}{$pname}[VALUE] = $ACPREFS{$akey}{$pname};
+            $ACHPVALUE{$akey}{$pname}[IVALUE] = $ACPREFS{$akey}{$pname};
         }
     }
     foreach my $key (keys %pr::plu) {
         my $plugin = $pr::plu{$key}[PLUGIN];
         my $pname = $pr::plu{$key}[NAME];
         if (defined $PLPREFS{$plugin}) {
-            $PLHPVALUE{$plugin}{$pname} = $PLPREFS{$plugin}{$pname};
+            $PLHPVALUE{$plugin}{$pname}[VALUE] = $PLPREFS{$plugin}{$pname};
+            $PLHPVALUE{$plugin}{$pname}[IVALUE] = $PLPREFS{$plugin}{$pname};
         }
     }
     return TRUE;
@@ -2366,6 +2540,18 @@ sub save_resource {
     close (RCF);
 }
 
+sub backup_resource {
+    my $rc = shift;
+    my $rcbak = "$rc.backup";
+    do {
+        my $emsg = _("Unable to create backup file '{name}'\n", name => $rcbak);
+        log_message ($emsg);
+        error_dialog ($emsg);
+        return FALSE;
+    } unless rename ($rc, $rcbak);
+    return TRUE;
+}
+
 # specific loaders
 sub load_menurc {
     my $rc = shift;
@@ -2448,28 +2634,37 @@ sub load_ac_preferences {
     return TRUE;
 }
 
+sub load_hk_preferences {
+    my $rc = get_menurc_filename ();
+    return FALSE unless check_rc_file ($rc);
+    $HOTKEYS = load_menurc ($rc);
+    return TRUE;
+}
+
 sub load_preferences {
     return FALSE unless check_claws_not_running ();
-    return (load_rc_preferences () and load_ac_preferences ());
+    return (load_rc_preferences ()
+        and load_ac_preferences ()
+        and load_hk_preferences ()
+    );
 }
 
 # save current preferences to disc
-sub save_preferences {
+sub save_rc_preferences {
     my $rc = get_rc_filename ();
     log_message ("Saving preferences to $rc\n");
     return FALSE unless check_rc_file ($rc);
     return FALSE unless check_claws_not_running ();
-    my $rcbak = "$rc.backup";
-    rename ($rc, $rcbak);
+    return FALSE unless backup_resource ($rc);
     foreach (keys %PREFS) {
         if (defined $HPVALUE{$_}) {
-            $CONFIGDATA->{'Common'}{$_} = $HPVALUE{$_};
+            $CONFIGDATA->{'Common'}{$_} = $HPVALUE{$_}[VALUE];
         }
     }
     foreach my $plugin (@AVPLUGINS) {
         foreach (keys %{$CONFIGDATA->{$plugin}}) {
             if (defined $PLHPVALUE{$plugin}{$_}) {
-                $CONFIGDATA->{$plugin}{$_} = $PLHPVALUE{$plugin}{$_};
+                $CONFIGDATA->{$plugin}{$_} = $PLHPVALUE{$plugin}{$_}[VALUE];
             }
         }
     }
@@ -2482,13 +2677,12 @@ sub save_ac_preferences {
     log_message ("Saving account preferences to $rc\n");
     return FALSE unless check_rc_file ($rc);
     return FALSE unless check_claws_not_running ();
-    my $rcbak = "$rc.backup";
-    rename ($rc, $rcbak);
+    return FALSE unless backup_resource ($rc);
     foreach my $asect (keys %$ACCOUNTDATA) {
         if ($asect =~ /^Account: (\d+)$/) {
             foreach (keys %{$ACCOUNTDATA->{$asect}}) {
                 if (defined $ACHPVALUE{$1}{$_}) {
-                    $ACCOUNTDATA->{$asect}{$_} = $ACHPVALUE{$1}{$_};
+                    $ACCOUNTDATA->{$asect}{$_} = $ACHPVALUE{$1}{$_}[VALUE];
                 }
             }
         }
@@ -2497,6 +2691,24 @@ sub save_ac_preferences {
     return TRUE;
 }
 
+sub save_hk_preferences {
+    my $rc = get_menurc_filename ();
+    log_message ("Saving hotkey preferences to $rc\n");
+    return FALSE unless check_rc_file ($rc);
+    return FALSE unless check_claws_not_running ();
+    return FALSE unless backup_resource ($rc);
+    save_menurc ($rc, $HOTKEYS);
+    return TRUE;
+}
+
+sub save_preferences {
+    my $result = save_rc_preferences ()
+        and save_ac_preferences ()
+        and save_hk_preferences ();
+    $MODIFIED = 0 if $result;
+    return $result;
+}
+
 # create notebook
 sub new_notebook {
     my $nb = Gtk2::Notebook->new;
@@ -2508,6 +2720,7 @@ sub new_notebook {
     $nb->append_page (new_winpos_page (), $xl::s{tab_winpos});
     $nb->append_page (new_accounts_page (), $xl::s{tab_accounts});
     $nb->append_page (new_plugins_page (), $xl::s{tab_plugins});
+    $nb->append_page (new_hotkeys_page (), $xl::s{tab_hotkeys});
     $nb->append_page (new_info_page (), $xl::s{tab_info});
 
     return $nb;
@@ -2532,7 +2745,7 @@ 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 &lt;http://www.gnu.org/licenses/&gt;.";
-    my $year = "2007-2017";
+    my $year = "2007-2018";
     my $holder = "Ricardo Mones &lt;ricardo\@mones.org&gt;";
     my $url = "http://www.claws-mail.org/clawsker.php";
 
@@ -2557,6 +2770,24 @@ along with this program.  If not, see &lt;http://www.gnu.org/licenses/&gt;.";
     return $dialog;
 }
 
+sub exit_handler {
+  my ($parent) = @_;
+  if ($MODIFIED != 0 and not $READONLY) {
+    my $fact = $xl::s{exit_fact};
+    my $question = $xl::s{exit_question};
+    my $dialog = Gtk2::MessageDialog->new_with_markup ($parent,
+                    [qw/modal destroy-with-parent/],
+                    'warning', 'yes-no',
+                    "<span>$fact</span>\n\n"
+                    . "<span weight=\"bold\">$question</span>");
+    $dialog->set_title ($xl::s{exit_title});
+    my $resp = $dialog->run;
+    $dialog->hide;
+    return TRUE if ($resp eq 'no');
+  }
+  Gtk2->main_quit;
+}
+
 # create buttons box
 sub new_button_box {
     my ($parent, $adlg) = @_;
@@ -2567,12 +2798,9 @@ sub new_button_box {
     # my $b_undo = Gtk2::Button->new_from_stock ('gtk-undo');
     my $hbox = Gtk2::HBox->new (FALSE, 5);
     # signal handlers
-    $b_exit->signal_connect (clicked => sub { Gtk2->main_quit });
+    $b_exit->signal_connect (clicked => sub { exit_handler($parent) });
     $b_apply->set_sensitive (not $READONLY);
-    $b_apply->signal_connect (clicked => sub {
-        save_preferences ($parent);
-        save_ac_preferences ($parent);
-    });
+    $b_apply->signal_connect (clicked => sub { save_preferences($parent) });
     # $b_undo->signal_connect (clicked => sub { undo_current_changes });
     $b_about->signal_connect (clicked => sub { $adlg->run; $adlg->hide });
     # package them
@@ -2605,6 +2833,13 @@ sub get_app_icons {
     return @APPICONS;
 }
 
+sub escape_key_handler {
+    my ($widget, $event) = @_;
+    if ($event->keyval == Gtk2::Gdk->keyval_from_name('Escape')) {
+        exit_handler($widget);
+    }
+}
+
 # initialise
 exit unless parse_command_line ();
 Gtk2->init;
@@ -2615,9 +2850,10 @@ exit unless init_hidden_preferences ();
 my $box = Gtk2::VBox->new (FALSE, 5);
 $box->set_border_width(3);
 my $about = new_about_dialog ();
-$box->pack_start (new_notebook (), FALSE, FALSE, 0);
+$box->pack_start (new_notebook (), TRUE, TRUE, 0);
 $box->pack_end (new_button_box ($main_window, $about), FALSE, FALSE, 0);
-$main_window->signal_connect (delete_event => sub { Gtk2->main_quit });
+$main_window->signal_connect (delete_event => sub { exit_handler($main_window) });
+$main_window->signal_connect (key_press_event => \&escape_key_handler);
 $main_window->set_title ($xl::s{win_title});
 $main_window->set_icon_list (get_app_icons ());
 $main_window->add ($box);