diff options
Diffstat (limited to 'src/app-window.vala')
-rw-r--r-- | src/app-window.vala | 1840 |
1 files changed, 1840 insertions, 0 deletions
diff --git a/src/app-window.vala b/src/app-window.vala new file mode 100644 index 0000000..2cd75ee --- /dev/null +++ b/src/app-window.vala @@ -0,0 +1,1840 @@ +/* + * Copyright (C) 2009-2015 Canonical Ltd. + * Author: Robert Ancell <robert.ancell@canonical.com>, + * Eduard Gotwig <g@ox.io> + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. See http://www.gnu.org/copyleft/gpl.html the full text of the + * license. + */ + +private const int DEFAULT_TEXT_DPI = 150; +private const int DEFAULT_PHOTO_DPI = 300; + +[GtkTemplate (ui = "/org/gnome/SimpleScan/app-window.ui")] +public class AppWindow : Gtk.ApplicationWindow +{ + private const GLib.ActionEntry[] action_entries = + { + { "new_document", new_document_activate_cb }, + { "reorder", reorder_document_activate_cb }, + { "save", save_document_activate_cb }, + { "email", email_document_activate_cb }, + { "print", print_document_activate_cb }, + { "preferences", preferences_activate_cb }, + { "help", help_contents_activate_cb }, + { "about", about_activate_cb }, + { "quit", quit_activate_cb } + }; + + private Settings settings; + + private PreferencesDialog preferences_dialog; + + [GtkChild] + private Gtk.HeaderBar header_bar; + [GtkChild] + private Gtk.MenuBar menubar; + [GtkChild] + private Gtk.Toolbar toolbar; + [GtkChild] + private Gtk.Menu page_menu; + [GtkChild] + private Gtk.Stack stack; + [GtkChild] + private Gtk.Label status_primary_label; + [GtkChild] + private Gtk.Label status_secondary_label; + [GtkChild] + private Gtk.Box main_vbox; + [GtkChild] + private Gtk.RadioMenuItem custom_crop_menuitem; + [GtkChild] + private Gtk.RadioMenuItem a4_menuitem; + [GtkChild] + private Gtk.RadioMenuItem a5_menuitem; + [GtkChild] + private Gtk.RadioMenuItem a6_menuitem; + [GtkChild] + private Gtk.RadioMenuItem letter_menuitem; + [GtkChild] + private Gtk.RadioMenuItem legal_menuitem; + [GtkChild] + private Gtk.RadioMenuItem four_by_six_menuitem; + [GtkChild] + private Gtk.RadioMenuItem no_crop_menuitem; + [GtkChild] + private Gtk.MenuItem page_move_left_menuitem; + [GtkChild] + private Gtk.MenuItem page_move_right_menuitem; + [GtkChild] + private Gtk.MenuItem page_delete_menuitem; + [GtkChild] + private Gtk.MenuItem crop_rotate_menuitem; + [GtkChild] + private Gtk.MenuItem save_menuitem; + [GtkChild] + private Gtk.MenuItem email_menuitem; + [GtkChild] + private Gtk.MenuItem print_menuitem; + [GtkChild] + private Gtk.MenuItem copy_to_clipboard_menuitem; + [GtkChild] + private Gtk.Button save_button; + [GtkChild] + private Gtk.ToolButton save_toolbutton; + [GtkChild] + private Gtk.MenuItem stop_scan_menuitem; + [GtkChild] + private Gtk.ToolButton stop_toolbutton; + [GtkChild] + private Gtk.Button stop_button; + [GtkChild] + private Gtk.Button scan_button; + [GtkChild] + private Gtk.ActionBar action_bar; + private Gtk.ToggleButton crop_button; + private Gtk.Button delete_button; + + [GtkChild] + private Gtk.RadioMenuItem text_button_menuitem; + [GtkChild] + private Gtk.RadioMenuItem text_button_hb_menuitem; + [GtkChild] + private Gtk.RadioMenuItem text_menuitem; + [GtkChild] + private Gtk.RadioMenuItem photo_button_menuitem; + [GtkChild] + private Gtk.RadioMenuItem photo_button_hb_menuitem; + [GtkChild] + private Gtk.RadioMenuItem photo_menuitem; + + [GtkChild] + private Gtk.MenuButton menu_button; + + private string? missing_driver = null; + + private Gtk.FileChooserDialog? save_dialog; + + public Book book { get; private set; } + private bool book_needs_saving; + private string? book_uri = null; + + public Page selected_page + { + get + { + return book_view.selected_page; + } + set + { + book_view.selected_page = value; + } + } + + private AutosaveManager autosave_manager; + + private BookView book_view; + private bool updating_page_menu; + + private string document_hint = "photo"; + + private bool scanning_ = false; + public bool scanning + { + get { return scanning_; } + set + { + scanning_ = value; + stack.set_visible_child_name ("document"); + page_delete_menuitem.sensitive = !value; + delete_button.sensitive = !value; + stop_scan_menuitem.sensitive = value; + stop_toolbutton.sensitive = value; + scan_button.visible = !value; + stop_button.visible = value; + } + } + + private int window_width; + private int window_height; + private bool window_is_maximized; + private bool window_is_fullscreen; + + private uint save_state_timeout; + + public int brightness + { + get { return preferences_dialog.get_brightness (); } + set { preferences_dialog.set_brightness (value); } + } + + public int contrast + { + get { return preferences_dialog.get_contrast (); } + set { preferences_dialog.set_contrast (value); } + } + + public int page_delay + { + get { return preferences_dialog.get_page_delay (); } + set { preferences_dialog.set_page_delay (value); } + } + + public string? selected_device + { + owned get { return preferences_dialog.get_selected_device (); } + set { preferences_dialog.set_selected_device (value); } + } + + public signal void start_scan (string? device, ScanOptions options); + public signal void stop_scan (); + + public AppWindow () + { + settings = new Settings ("org.gnome.SimpleScan"); + + book = new Book (); + book.page_added.connect (page_added_cb); + book.reordered.connect (reordered_cb); + book.page_removed.connect (page_removed_cb); + book.changed.connect (book_changed_cb); + + load (); + + clear_document (); + autosave_manager = new AutosaveManager (); + autosave_manager.book = book; + autosave_manager.load (); + + if (book.n_pages == 0) + book_needs_saving = false; + else + { + stack.set_visible_child_name ("document"); + book_view.selected_page = book.get_page (0); + book_needs_saving = true; + book_changed_cb (book); + } + } + + ~AppWindow () + { + book.page_added.disconnect (page_added_cb); + book.reordered.disconnect (reordered_cb); + book.page_removed.disconnect (page_removed_cb); + } + + public void show_error_dialog (string error_title, string error_text) + { + var dialog = new Gtk.MessageDialog (this, + Gtk.DialogFlags.MODAL, + Gtk.MessageType.WARNING, + Gtk.ButtonsType.NONE, + "%s", error_title); + dialog.add_button (_("_Close"), 0); + dialog.format_secondary_text ("%s", error_text); + dialog.run (); + dialog.destroy (); + } + + public void authorize (string resource, out string username, out string password) + { + /* Label in authorization dialog. “%s” is replaced with the name of the resource requesting authorization */ + var description = _("Username and password required to access “%s”").printf (resource); + var authorize_dialog = new AuthorizeDialog (description); + authorize_dialog.visible = true; + authorize_dialog.transient_for = this; + authorize_dialog.run (); + authorize_dialog.destroy (); + + username = authorize_dialog.get_username (); + password = authorize_dialog.get_password (); + } + + public void set_scan_devices (List<ScanDevice> devices, string? missing_driver = null) + { + this.missing_driver = missing_driver; + + preferences_dialog.set_scan_devices (devices); + + if (devices != null) + { + status_primary_label.set_text (/* Label shown when detected a scanner */ + _("Ready to Scan")); + status_secondary_label.set_text (preferences_dialog.get_selected_device_label ()); + status_secondary_label.visible = true; + } + else if (missing_driver != null) + { + status_primary_label.set_text (/* Warning displayed when no drivers are installed but a compatible scanner is detected */ + _("Additional software needed")); + /* Instructions to install driver software */ + status_secondary_label.set_markup (_("You need to <a href=\"install-firmware\">install driver software</a> for your scanner.")); + status_secondary_label.visible = true; + } + else + { + /* Warning displayed when no scanners are detected */ + status_primary_label.set_text (_("No scanners detected")); + /* Hint to user on why there are no scanners detected */ + status_secondary_label.set_text (_("Please check your scanner is connected and powered on")); + status_secondary_label.visible = true; + } + } + + private string choose_file_location () + { + /* Get directory to save to */ + string? directory = null; + directory = settings.get_string ("save-directory"); + + if (directory == null || directory == "") + directory = Environment.get_user_special_dir (UserDirectory.DOCUMENTS); + + save_dialog = new Gtk.FileChooserDialog (/* Save dialog: Dialog title */ + _("Save As…"), + this, + Gtk.FileChooserAction.SAVE, + _("_Cancel"), Gtk.ResponseType.CANCEL, + _("_Save"), Gtk.ResponseType.ACCEPT, + null); + save_dialog.local_only = false; + if (book_uri != null) + save_dialog.set_uri (book_uri); + else { + save_dialog.set_current_folder (directory); + /* Default filename to use when saving document */ + save_dialog.set_current_name (_("Scanned Document.pdf")); + } + + /* Filter to only show images by default */ + var filter = new Gtk.FileFilter (); + filter.set_filter_name (/* Save dialog: Filter name to show only supported image files */ + _("Image Files")); + filter.add_mime_type ("image/jpeg"); + filter.add_mime_type ("image/png"); +#if HAVE_WEBP + filter.add_mime_type ("image/webp"); +#endif + filter.add_mime_type ("application/pdf"); + save_dialog.add_filter (filter); + filter = new Gtk.FileFilter (); + filter.set_filter_name (/* Save dialog: Filter name to show all files */ + _("All Files")); + filter.add_pattern ("*"); + save_dialog.add_filter (filter); + + var file_type_store = new Gtk.ListStore (2, typeof (string), typeof (string)); + Gtk.TreeIter iter; + file_type_store.append (out iter); + file_type_store.set (iter, + /* Save dialog: Label for saving in PDF format */ + 0, _("PDF (multi-page document)"), + 1, ".pdf", + -1); + file_type_store.append (out iter); + file_type_store.set (iter, + /* Save dialog: Label for saving in JPEG format */ + 0, _("JPEG (compressed)"), + 1, ".jpg", + -1); + file_type_store.append (out iter); + file_type_store.set (iter, + /* Save dialog: Label for saving in PNG format */ + 0, _("PNG (lossless)"), + 1, ".png", + -1); +#if HAVE_WEBP + file_type_store.append (out iter); + file_type_store.set (iter, + /* Save dialog: Label for sabing in WEBP format */ + 0, _("WebP (compressed)"), + 1, ".webp", + -1); +#endif + + var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6); + box.visible = true; + save_dialog.set_extra_widget (box); + + /* Label in save dialog beside combo box to choose file format (PDF, JPEG, PNG, WEBP) */ + var label = new Gtk.Label (_("File format:")); + label.visible = true; + box.pack_start (label, false, false, 0); + + var file_type_combo = new Gtk.ComboBox.with_model (file_type_store); + file_type_combo.visible = true; + var renderer = new Gtk.CellRendererText (); + file_type_combo.pack_start (renderer, true); + file_type_combo.add_attribute (renderer, "text", 0); + box.pack_start (file_type_combo, false, true, 0); + + /* Label in save dialog beside compression slider */ + var quality_label = new Gtk.Label (_("Compression:")); + box.pack_start (quality_label, false, false, 0); + + var quality_adjustment = new Gtk.Adjustment (75, 0, 100, 1, 10, 0); + var quality_scale = new Gtk.Scale (Gtk.Orientation.HORIZONTAL, quality_adjustment); + quality_scale.width_request = 200; + quality_scale.draw_value = false; + quality_scale.add_mark (0, Gtk.PositionType.BOTTOM, null); + quality_scale.add_mark (75, Gtk.PositionType.BOTTOM, null); + quality_scale.add_mark (90, Gtk.PositionType.BOTTOM, null); + quality_scale.add_mark (100, Gtk.PositionType.BOTTOM, null); + quality_adjustment.value = settings.get_int ("jpeg-quality"); + quality_adjustment.value_changed.connect (() => { settings.set_int ("jpeg-quality", (int) quality_adjustment.value); }); + box.pack_start (quality_scale, false, false, 0); + + file_type_combo.set_active (0); + file_type_combo.changed.connect (() => + { + var extension = ""; + Gtk.TreeIter i; + if (file_type_combo.get_active_iter (out i)) + file_type_store.get (i, 1, out extension, -1); + + var path = save_dialog.get_filename (); + var filename = Path.get_basename (path); + + /* Replace extension */ + var extension_index = filename.last_index_of_char ('.'); + if (extension_index >= 0) + filename = filename.slice (0, extension_index); + filename = filename + extension; + save_dialog.set_current_name (filename); + + /* Quality not applicable to PNG */ + quality_scale.visible = quality_label.visible = (extension != ".png"); + }); + + string? uri = null; + while (true) + { + var response = save_dialog.run (); + if (response != Gtk.ResponseType.ACCEPT) + break; + + var extension = ""; + Gtk.TreeIter i; + if (file_type_combo.get_active_iter (out i)) + file_type_store.get (i, 1, out extension, -1); + + var path = save_dialog.get_filename (); + var filename = Path.get_basename (path); + + var extension_index = filename.last_index_of_char ('.'); + if (extension_index < 0) + path += extension; + + uri = File.new_for_path (path).get_uri (); + + /* Check the file(s) don't already exist */ + var files = new List<File> (); + var format = uri_to_format (uri); +#if HAVE_WEBP + if (format == "jpeg" || format == "png" || format == "webp") +#else + if (format == "jpeg" || format == "png") +#endif + { + for (var j = 0; j < book.n_pages; j++) + files.append (make_indexed_file (uri, j, book.n_pages)); + } + else + files.append (File.new_for_uri (uri)); + + if (check_overwrite (save_dialog, files)) + break; + } + + settings.set_string ("save-directory", save_dialog.get_current_folder ()); + + save_dialog.destroy (); + save_dialog = null; + + return uri; + } + + private bool check_overwrite (Gtk.Window parent, List<File> files) + { + foreach (var file in files) + { + if (!file.query_exists ()) + continue; + + var dialog = new Gtk.MessageDialog (parent, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE, + /* Contents of dialog that shows if saving would overwrite and existing file. %s is replaced with the name of the file. */ + _("A file named “%s” already exists. Do you want to replace it?"), file.get_basename ()); + dialog.add_button (_("_Cancel"), Gtk.ResponseType.CANCEL); + dialog.add_button (/* Button in dialog that shows if saving would overwrite and existing file. Clicking the button allows simple-scan to overwrite the file. */ + _("_Replace"), Gtk.ResponseType.ACCEPT); + var response = dialog.run (); + dialog.destroy (); + + if (response != Gtk.ResponseType.ACCEPT) + return false; + } + + return true; + } + + private string uri_to_format (string uri) + { + var uri_lower = uri.down (); + if (uri_lower.has_suffix (".pdf")) + return "pdf"; + else if (uri_lower.has_suffix (".png")) + return "png"; +#if HAVE_WEBP + else if (uri_lower.has_suffix (".webp")) + return "webp"; +#endif + else + return "jpeg"; + } + + private async bool save_document_async () + { + var uri = choose_file_location (); + if (uri == null) + return false; + + var file = File.new_for_uri (uri); + + debug ("Saving to '%s'", uri); + + var format = uri_to_format (uri); + + var cancellable = new Cancellable (); + var progress_bar = new CancellableProgressBar (_("Saving"), cancellable); + action_bar.pack_end (progress_bar); + progress_bar.visible = true; + try + { + yield book.save_async (format, settings.get_int ("jpeg-quality"), file, (fraction) => + { + progress_bar.set_fraction (fraction); + }, cancellable); + } + catch (Error e) + { + progress_bar.destroy (); + warning ("Error saving file: %s", e.message); + show_error_dialog (/* Title of error dialog when save failed */ + _("Failed to save file"), + e.message); + return false; + } + progress_bar.destroy_with_delay (500); + + book_needs_saving = false; + book_uri = uri; + return true; + } + + private async bool prompt_to_save_async (string title, string discard_label) + { + if (!book_needs_saving) + return true; + + var dialog = new Gtk.MessageDialog (this, + Gtk.DialogFlags.MODAL, + Gtk.MessageType.WARNING, + Gtk.ButtonsType.NONE, + "%s", title); + dialog.format_secondary_text ("%s", + /* Text in dialog warning when a document is about to be lost*/ + _("If you don’t save, changes will be permanently lost.")); + dialog.add_button (discard_label, Gtk.ResponseType.NO); + dialog.add_button (_("_Cancel"), Gtk.ResponseType.CANCEL); + dialog.add_button (_("_Save"), Gtk.ResponseType.YES); + + var response = dialog.run (); + dialog.destroy (); + + switch (response) + { + case Gtk.ResponseType.YES: + if (yield save_document_async ()) + return true; + else + return false; + case Gtk.ResponseType.NO: + return true; + default: + return false; + } + } + + private void clear_document () + { + book.clear (); + book_needs_saving = false; + book_uri = null; + save_menuitem.sensitive = false; + email_menuitem.sensitive = false; + print_menuitem.sensitive = false; + save_button.sensitive = false; + save_toolbutton.sensitive = false; + copy_to_clipboard_menuitem.sensitive = false; + status_primary_label.set_text (/* Label shown when detected a scanner */ + _("Ready to Scan")); + stack.set_visible_child_name ("startup"); + } + + private void new_document () + { + prompt_to_save_async.begin (/* Text in dialog warning when a document is about to be lost */ + _("Save current document?"), + /* Button in dialog to create new document and discard unsaved document */ + _("Discard Changes"), (obj, res) => + { + if (!prompt_to_save_async.end(res)) + return; + + if (scanning) + stop_scan (); + + clear_document (); + }); + } + + [GtkCallback] + private bool status_label_activate_link_cb (Gtk.Label label, string uri) + { + if (uri == "install-firmware") + { + install_drivers (); + return true; + } + + return false; + } + + [GtkCallback] + private void new_button_clicked_cb (Gtk.Widget widget) + { + new_document(); + } + + public void new_document_activate_cb () + { + new_document(); + } + + private void set_document_hint (string document_hint, bool save = false) + { + this.document_hint = document_hint; + + if (document_hint == "text") + { + text_button_menuitem.active = true; + text_button_hb_menuitem.active = true; + text_menuitem.active = true; + } + else if (document_hint == "photo") + { + photo_button_menuitem.active = true; + photo_button_hb_menuitem.active = true; + photo_menuitem.active = true; + } + + if (save) + settings.set_string ("document-type", document_hint); + } + + [GtkCallback] + private void text_menuitem_toggled_cb (Gtk.CheckMenuItem widget) + { + if (widget.active) + set_document_hint ("text", true); + } + + [GtkCallback] + private void photo_menuitem_toggled_cb (Gtk.CheckMenuItem widget) + { + if (widget.active) + set_document_hint ("photo", true); + } + + private ScanOptions make_scan_options () + { + var options = new ScanOptions (); + if (document_hint == "text") + { + options.scan_mode = ScanMode.GRAY; + options.dpi = preferences_dialog.get_text_dpi (); + options.depth = 2; + } + else + { + options.scan_mode = ScanMode.COLOR; + options.dpi = preferences_dialog.get_photo_dpi (); + options.depth = 8; + } + preferences_dialog.get_paper_size (out options.paper_width, out options.paper_height); + options.brightness = brightness; + options.contrast = contrast; + options.page_delay = page_delay; + + return options; + } + + [GtkCallback] + private void scan_button_clicked_cb (Gtk.Widget widget) + { + var options = make_scan_options (); + options.type = ScanType.SINGLE; + status_primary_label.set_text (/* Label shown when scan started */ + _("Contacting scanner…")); + start_scan (selected_device, options); + } + + [GtkCallback] + private void stop_scan_button_clicked_cb (Gtk.Widget widget) + { + stop_scan (); + } + + [GtkCallback] + private void continuous_scan_button_clicked_cb (Gtk.Widget widget) + { + if (scanning) + stop_scan (); + else + { + var options = make_scan_options (); + options.type = preferences_dialog.get_page_side (); + start_scan (selected_device, options); + } + } + + [GtkCallback] + private void batch_button_clicked_cb (Gtk.Widget widget) + { + var options = make_scan_options (); + options.type = ScanType.BATCH; + start_scan (selected_device, options); + } + + [GtkCallback] + private void preferences_button_clicked_cb (Gtk.Widget widget) + { + preferences_dialog.present (); + } + + public void preferences_activate_cb () + { + preferences_dialog.present (); + } + + private void update_page_menu () + { + var page = book_view.selected_page; + if (page == null) + { + page_move_left_menuitem.sensitive = false; + page_move_right_menuitem.sensitive = false; + } + else + { + var index = book.get_page_index (page); + page_move_left_menuitem.sensitive = index > 0; + page_move_right_menuitem.sensitive = index < book.n_pages - 1; + } + } + + private void page_selected_cb (BookView view, Page? page) + { + if (page == null) + return; + + updating_page_menu = true; + + update_page_menu (); + + var menuitem = no_crop_menuitem; + if (page.has_crop) + { + var crop_name = page.crop_name; + if (crop_name != null) + { + if (crop_name == "A4") + menuitem = a4_menuitem; + else if (crop_name == "A5") + menuitem = a5_menuitem; + else if (crop_name == "A6") + menuitem = a6_menuitem; + else if (crop_name == "letter") + menuitem = letter_menuitem; + else if (crop_name == "legal") + menuitem = legal_menuitem; + else if (crop_name == "4x6") + menuitem = four_by_six_menuitem; + } + else + menuitem = custom_crop_menuitem; + } + + menuitem.active = true; + crop_button.active = page.has_crop; + + updating_page_menu = false; + } + + private void show_page_cb (BookView view, Page page) + { + File file; + try + { + var dir = DirUtils.make_tmp ("simple-scan-XXXXXX"); + file = File.new_for_path (Path.build_filename (dir, "scan.png")); + page.save_png (file); + } + catch (Error e) + { + show_error_dialog (/* Error message display when unable to save image for preview */ + _("Unable to save image for preview"), + e.message); + return; + } + + try + { + Gtk.show_uri (screen, file.get_uri (), Gtk.get_current_event_time ()); + } + catch (Error e) + { + show_error_dialog (/* Error message display when unable to preview image */ + _("Unable to open image preview application"), + e.message); + } + } + + private void show_page_menu_cb (BookView view) + { + page_menu.popup (null, null, null, 3, Gtk.get_current_event_time ()); + } + + [GtkCallback] + private void rotate_left_button_clicked_cb (Gtk.Widget widget) + { + if (updating_page_menu) + return; + var page = book_view.selected_page; + if (page != null) + page.rotate_left (); + } + + [GtkCallback] + private void rotate_right_button_clicked_cb (Gtk.Widget widget) + { + if (updating_page_menu) + return; + var page = book_view.selected_page; + if (page != null) + page.rotate_right (); + } + + private void set_crop (string? crop_name) + { + crop_rotate_menuitem.sensitive = crop_name != null; + + if (updating_page_menu) + return; + + var page = book_view.selected_page; + if (page == null) + { + warning ("Trying to set crop but no selected page"); + return; + } + + if (crop_name == null) + page.set_no_crop (); + else if (crop_name == "custom") + { + var width = page.width; + var height = page.height; + var crop_width = (int) (width * 0.8 + 0.5); + var crop_height = (int) (height * 0.8 + 0.5); + page.set_custom_crop (crop_width, crop_height); + page.move_crop ((width - crop_width) / 2, (height - crop_height) / 2); + } + else + page.set_named_crop (crop_name); + } + + [GtkCallback] + private void no_crop_menuitem_toggled_cb (Gtk.CheckMenuItem widget) + { + if (widget.active) + set_crop (null); + } + + [GtkCallback] + private void custom_crop_menuitem_toggled_cb (Gtk.CheckMenuItem widget) + { + if (widget.active) + set_crop ("custom"); + } + + [GtkCallback] + private void four_by_six_menuitem_toggled_cb (Gtk.CheckMenuItem widget) + { + if (widget.active) + set_crop ("4x6"); + } + + [GtkCallback] + private void legal_menuitem_toggled_cb (Gtk.CheckMenuItem widget) + { + if (widget.active) + set_crop ("legal"); + } + + [GtkCallback] + private void letter_menuitem_toggled_cb (Gtk.CheckMenuItem widget) + { + if (widget.active) + set_crop ("letter"); + } + + [GtkCallback] + private void a6_menuitem_toggled_cb (Gtk.CheckMenuItem widget) + { + if (widget.active) + set_crop ("A6"); + } + + [GtkCallback] + private void a5_menuitem_toggled_cb (Gtk.CheckMenuItem widget) + { + if (widget.active) + set_crop ("A5"); + } + + [GtkCallback] + private void a4_menuitem_toggled_cb (Gtk.CheckMenuItem widget) + { + if (widget.active) + set_crop ("A4"); + } + + [GtkCallback] + private void crop_rotate_menuitem_activate_cb (Gtk.Widget widget) + { + var page = book_view.selected_page; + if (page == null) + return; + page.rotate_crop (); + } + + [GtkCallback] + private void page_move_left_menuitem_activate_cb (Gtk.Widget widget) + { + var page = book_view.selected_page; + var index = book.get_page_index (page); + if (index > 0) + book.move_page (page, index - 1); + } + + [GtkCallback] + private void page_move_right_menuitem_activate_cb (Gtk.Widget widget) + { + var page = book_view.selected_page; + var index = book.get_page_index (page); + if (index < book.n_pages - 1) + book.move_page (page, book.get_page_index (page) + 1); + } + + [GtkCallback] + private void page_delete_menuitem_activate_cb (Gtk.Widget widget) + { + book_view.book.delete_page (book_view.selected_page); + } + + private void reorder_document () + { + var dialog = new Gtk.Window (); + dialog.type_hint = Gdk.WindowTypeHint.DIALOG; + dialog.modal = true; + dialog.border_width = 12; + /* Title of dialog to reorder pages */ + dialog.title = _("Reorder Pages"); + dialog.set_transient_for (this); + dialog.key_press_event.connect ((e) => + { + if (e.state == 0 && e.keyval == Gdk.Key.Escape) + { + dialog.destroy (); + return true; + } + + return false; + }); + dialog.visible = true; + + var g = new Gtk.Grid (); + g.row_homogeneous = true; + g.row_spacing = 6; + g.column_homogeneous = true; + g.column_spacing = 6; + g.visible = true; + dialog.add (g); + + /* Label on button for combining sides in reordering dialog */ + var b = make_reorder_button (_("Combine sides"), "F1F2F3B1B2B3-F1B1F2B2F3B3"); + b.clicked.connect (() => + { + book.combine_sides (); + dialog.destroy (); + }); + b.visible = true; + g.attach (b, 0, 0, 1, 1); + + /* Label on button for combining sides in reverse order in reordering dialog */ + b = make_reorder_button (_("Combine sides (reverse)"), "F1F2F3B3B2B1-F1B1F2B2F3B3"); + b.clicked.connect (() => + { + book.combine_sides_reverse (); + dialog.destroy (); + }); + b.visible = true; + g.attach (b, 1, 0, 1, 1); + + /* Label on button for reversing in reordering dialog */ + b = make_reorder_button (_("Reverse"), "C1C2C3C4C5C6-C6C5C4C3C2C1"); + b.clicked.connect (() => + { + book.reverse (); + dialog.destroy (); + }); + b.visible = true; + g.attach (b, 0, 2, 1, 1); + + /* Label on button for cancelling page reordering dialog */ + b = make_reorder_button (_("Keep unchanged"), "C1C2C3C4C5C6-C1C2C3C4C5C6"); + b.clicked.connect (() => + { + dialog.destroy (); + }); + b.visible = true; + g.attach (b, 1, 2, 1, 1); + + dialog.present (); + } + + public void reorder_document_activate_cb () + { + reorder_document (); + } + + [GtkCallback] + private void reorder_menuitem_activate_cb (Gtk.Widget widget) + { + reorder_document (); + } + + private Gtk.Button make_reorder_button (string text, string items) + { + var b = new Gtk.Button (); + + var vbox = new Gtk.Box (Gtk.Orientation.VERTICAL, 6); + vbox.visible = true; + b.add (vbox); + + var label = new Gtk.Label (text); + label.visible = true; + vbox.pack_start (label, true, true, 0); + + var rb = make_reorder_box (items); + rb.visible = true; + vbox.pack_start (rb, true, true, 0); + + return b; + } + + private Gtk.Box make_reorder_box (string items) + { + var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6); + box.visible = true; + + Gtk.Box? page_box = null; + for (var i = 0; items[i] != '\0'; i++) + { + if (items[i] == '-') + { + var a = new Gtk.Arrow (Gtk.ArrowType.RIGHT, Gtk.ShadowType.NONE); + a.visible = true; + box.pack_start (a, false, false, 0); + page_box = null; + continue; + } + + /* First character describes side */ + var side = items[i]; + i++; + if (items[i] == '\0') + break; + + if (page_box == null) + { + page_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 3); + page_box.visible = true; + box.pack_start (page_box, false, false, 0); + } + + /* Get colours for each page (from Tango palette) */ + var r = 1.0; + var g = 1.0; + var b = 1.0; + switch (side) + { + case 'F': + /* Plum */ + r = 0x75 / 255.0; + g = 0x50 / 255.0; + b = 0x7B / 255.0; + break; + case 'B': + /* Orange */ + r = 0xF5 / 255.0; + g = 0x79 / 255.0; + b = 0.0; + break; + case 'C': + /* Butter to Scarlet Red */ + var p = (items[i] - '1') / 5.0; + r = (0xED / 255.0) * (1 - p) + 0xCC * p; + g = (0xD4 / 255.0) * (1 - p); + b = 0; + break; + } + + /* Mix with white to look more paper like */ + r = r + (1.0 - r) * 0.7; + g = g + (1.0 - g) * 0.7; + b = b + (1.0 - b) * 0.7; + + var icon = new PageIcon ("%c".printf (items[i]), r, g, b); + icon.visible = true; + page_box.pack_start (icon, false, false, 0); + } + + return box; + } + + [GtkCallback] + private void save_file_button_clicked_cb (Gtk.Widget widget) + { + save_document_async.begin (); + } + + public void save_document_activate_cb () + { + save_document_async.begin (); + } + + [GtkCallback] + private void copy_to_clipboard_button_clicked_cb (Gtk.Widget widget) + { + var page = book_view.selected_page; + if (page != null) + page.copy_to_clipboard (this); + } + + private void draw_page (Gtk.PrintOperation operation, + Gtk.PrintContext print_context, + int page_number) + { + var context = print_context.get_cairo_context (); + var page = book.get_page (page_number); + + /* Rotate to same aspect */ + bool is_landscape = false; + if (print_context.get_width () > print_context.get_height ()) + is_landscape = true; + if (page.is_landscape != is_landscape) + { + context.translate (print_context.get_width (), 0); + context.rotate (Math.PI_2); + } + + context.scale (print_context.get_dpi_x () / page.dpi, + print_context.get_dpi_y () / page.dpi); + + var image = page.get_image (true); + Gdk.cairo_set_source_pixbuf (context, image, 0, 0); + context.paint (); + } + + [GtkCallback] + private void email_button_clicked_cb (Gtk.Widget widget) + { + email_document_async.begin (); + } + + public void email_document_activate_cb () + { + email_document_async.begin (); + } + + private async void email_document_async () + { + try + { + var dir = DirUtils.make_tmp ("simple-scan-XXXXXX"); + var type = document_hint == "text" ? "pdf" : "jpeg"; + var file = File.new_for_path (Path.build_filename (dir, "scan." + type)); + yield book.save_async (type, settings.get_int ("jpeg-quality"), file, null, null); + var command_line = "xdg-email"; + if (type == "pdf") + command_line += "--attach %s".printf (file.get_path ()); + else + { + for (var i = 0; i < book.n_pages; i++) { + var indexed_file = make_indexed_file (file.get_uri (), i, book.n_pages); + command_line += " --attach %s".printf (indexed_file.get_path ()); + } + } + Process.spawn_command_line_async (command_line); + } + catch (Error e) + { + warning ("Unable to email document: %s", e.message); + } + } + + private void print_document () + { + var print = new Gtk.PrintOperation (); + print.n_pages = (int) book.n_pages; + print.draw_page.connect (draw_page); + + try + { + print.run (Gtk.PrintOperationAction.PRINT_DIALOG, this); + } + catch (Error e) + { + warning ("Error printing: %s", e.message); + } + + print.draw_page.disconnect (draw_page); + } + + [GtkCallback] + private void print_button_clicked_cb (Gtk.Widget widget) + { + print_document (); + } + + public void print_document_activate_cb () + { + print_document (); + } + + private void launch_help () + { + try + { + Gtk.show_uri (screen, "help:simple-scan", Gtk.get_current_event_time ()); + } + catch (Error e) + { + show_error_dialog (/* Error message displayed when unable to launch help browser */ + _("Unable to open help file"), + e.message); + } + } + + [GtkCallback] + private void help_contents_menuitem_activate_cb (Gtk.Widget widget) + { + launch_help (); + } + + public void help_contents_activate_cb () + { + launch_help (); + } + + private void show_about () + { + string[] authors = { "Robert Ancell <robert.ancell@canonical.com>" }; + + /* The license this software is under (GPL3+) */ + string license = _("This program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program. If not, see <http://www.gnu.org/licenses/>."); + + /* Title of about dialog */ + string title = _("About Simple Scan"); + + /* Description of program */ + string description = _("Simple document scanning tool"); + + Gtk.show_about_dialog (this, + "title", title, + "program-name", "Simple Scan", + "version", VERSION, + "comments", description, + "logo-icon-name", "scanner", + "authors", authors, + "translator-credits", _("translator-credits"), + "website", "https://launchpad.net/simple-scan", + "copyright", "Copyright © 2009-2015 Canonical Ltd.", + "license", license, + "wrap-license", true, + null); + } + + [GtkCallback] + private void about_menuitem_activate_cb (Gtk.Widget widget) + { + show_about (); + } + + public void about_activate_cb () + { + show_about (); + } + + private void on_quit () + { + prompt_to_save_async.begin (/* Text in dialog warning when a document is about to be lost */ + _("Save document before quitting?"), + /* Text in dialog warning when a document is about to be lost */ + _("Quit without Saving"), (obj, res) => + { + if (!prompt_to_save_async.end(res)) + return; + + destroy (); + + if (save_state_timeout != 0) + save_state (true); + + autosave_manager.cleanup (); + }); + } + + [GtkCallback] + private void quit_menuitem_activate_cb (Gtk.Widget widget) + { + on_quit (); + } + + public void quit_activate_cb () + { + on_quit (); + } + + public override void size_allocate (Gtk.Allocation allocation) + { + base.size_allocate (allocation); + + if (!window_is_maximized && !window_is_fullscreen) + { + get_size (out window_width, out window_height); + save_state (); + } + } + + private void install_drivers () + { + var message = "", instructions = ""; + string[] packages_to_install = {}; + switch (missing_driver) + { + case "brscan": + case "brscan2": + case "brscan3": + case "brscan4": + /* Message to indicate a Brother scanner has been detected */ + message = _("You appear to have a Brother scanner."); + /* Instructions on how to install Brother scanner drivers */ + instructions = _("Drivers for this are available on the <a href=\"http://support.brother.com\">Brother website</a>."); + break; + case "samsung": + /* Message to indicate a Samsung scanner has been detected */ + message = _("You appear to have a Samsung scanner."); + /* Instructions on how to install Samsung scanner drivers */ + instructions = _("Drivers for this are available on the <a href=\"http://samsung.com/support\">Samsung website</a>."); + break; + case "hpaio": + /* Message to indicate a HP scanner has been detected */ + message = _("You appear to have an HP scanner."); + packages_to_install = { "libsane-hpaio" }; + break; + case "epkowa": + /* Message to indicate an Epson scanner has been detected */ + message = _("You appear to have an Epson scanner."); + /* Instructions on how to install Epson scanner drivers */ + instructions = _("Drivers for this are available on the <a href=\"http://support.epson.com\">Epson website</a>."); + break; + } + var dialog = new Gtk.Dialog.with_buttons (/* Title of dialog giving instructions on how to install drivers */ + _("Install drivers"), this, Gtk.DialogFlags.MODAL, _("_Close"), Gtk.ResponseType.CLOSE); + dialog.get_content_area ().border_width = 12; + dialog.get_content_area ().spacing = 6; + + var label = new Gtk.Label (message); + label.visible = true; + label.xalign = 0f; + dialog.get_content_area ().pack_start (label, true, true, 0); + + var instructions_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6); + instructions_box.visible = true; + dialog.get_content_area ().pack_start (instructions_box, true, true, 0); + + var stack = new Gtk.Stack (); + instructions_box.pack_start (stack, false, false, 0); + + var spinner = new Gtk.Spinner (); + spinner.visible = true; + stack.add (spinner); + + var status_label = new Gtk.Label (""); + status_label.visible = true; + stack.add (status_label); + + var instructions_label = new Gtk.Label (instructions); + instructions_label.visible = true; + instructions_label.xalign = 0f; + instructions_label.use_markup = true; + instructions_box.pack_start (instructions_label, false, false, 0); + + label = new Gtk.Label (/* Message in driver install dialog */ + _("Once installed you will need to restart Simple Scan.")); + label.visible = true; + label.xalign = 0f; + dialog.get_content_area ().border_width = 12; + dialog.get_content_area ().pack_start (label, true, true, 0); + + if (packages_to_install.length > 0) + { +#if HAVE_PACKAGEKIT + stack.visible = true; + spinner.active = true; + instructions_label.set_text (/* Label shown while installing drivers */ + _("Installing drivers…")); + install_packages.begin (packages_to_install, () => {}, (object, result) => + { + status_label.visible = true; + spinner.active = false; + status_label.set_text ("☒"); + stack.visible_child = status_label; + /* Label shown once drivers successfully installed */ + var result_text = _("Drivers installed successfully!"); + try + { + var results = install_packages.end (result); + if (results.get_error_code () == null) + status_label.set_text ("☑"); + else + { + var e = results.get_error_code (); + /* Label shown if failed to install drivers */ + result_text = _("Failed to install drivers (error code %d).").printf (e.code); + } + } + catch (Error e) + { + /* Label shown if failed to install drivers */ + result_text = _("Failed to install drivers."); + warning ("Failed to install drivers: %s", e.message); + } + instructions_label.set_text (result_text); + }); +#else + instructions_label.set_text (/* Label shown to prompt user to install packages (when PackageKit not available) */ + ngettext ("You need to install the %s package.", "You need to install the %s packages.", packages_to_install.length).printf (string.joinv (", ", packages_to_install))); +#endif + } + + dialog.run (); + dialog.destroy (); + } + +#if HAVE_PACKAGEKIT + private async Pk.Results? install_packages (string[] packages, Pk.ProgressCallback progress_callback) throws GLib.Error + { + var task = new Pk.Task (); + Pk.Results results; + results = yield task.resolve_async (Pk.Filter.NOT_INSTALLED, packages, null, progress_callback); + if (results == null || results.get_error_code () != null) + return results; + + var package_array = results.get_package_array (); + var package_ids = new string[package_array.length + 1]; + package_ids[package_array.length] = null; + for (var i = 0; i < package_array.length; i++) + package_ids[i] = package_array.data[i].get_id (); + + return yield task.install_packages_async (package_ids, null, progress_callback); + } +#endif + + public override bool window_state_event (Gdk.EventWindowState event) + { + var result = Gdk.EVENT_PROPAGATE; + + if (base.window_state_event != null) + result = base.window_state_event (event); + + if ((event.changed_mask & Gdk.WindowState.MAXIMIZED) != 0) + { + window_is_maximized = (event.new_window_state & Gdk.WindowState.MAXIMIZED) != 0; + save_state (); + } + if ((event.changed_mask & Gdk.WindowState.FULLSCREEN) != 0) + { + window_is_fullscreen = (event.new_window_state & Gdk.WindowState.FULLSCREEN) != 0; + save_state (); + } + + return result; + } + + [GtkCallback] + private bool window_delete_event_cb (Gtk.Widget widget, Gdk.EventAny event) + { + on_quit (); + return true; /* Let us quit on our own terms */ + } + + private void page_added_cb (Book book, Page page) + { + update_page_menu (); + } + + private void reordered_cb (Book book) + { + update_page_menu (); + } + + private void page_removed_cb (Book book, Page page) + { + update_page_menu (); + } + + private void book_changed_cb (Book book) + { + save_menuitem.sensitive = true; + email_menuitem.sensitive = true; + print_menuitem.sensitive = true; + save_button.sensitive = true; + save_toolbutton.sensitive = true; + book_needs_saving = true; + copy_to_clipboard_menuitem.sensitive = true; + } + + private void load () + { + var use_header_bar = !is_traditional_desktop (); + + preferences_dialog = new PreferencesDialog (settings, use_header_bar); + preferences_dialog.delete_event.connect (() => { return true; }); + preferences_dialog.response.connect (() => { preferences_dialog.visible = false; }); + + Gtk.IconTheme.get_default ().append_search_path (ICON_DIR); + + Gtk.Window.set_default_icon_name ("scanner"); + + var app = Application.get_default () as Gtk.Application; + + if (!use_header_bar) + { + set_titlebar (null); + menubar.visible = true; + toolbar.visible = true; + } + else + { + /* Set HeaderBar title here because Glade doesn't keep it translated */ + /* https://bugzilla.gnome.org/show_bug.cgi?id=782753 */ + /* Title of scan window */ + header_bar.title = _("Simple Scan"); + + app.add_action_entries (action_entries, this); + + var appmenu = new Menu (); + + var section = new Menu (); + appmenu.append_section (null, section); + section.append (_("Preferences"), "app.preferences"); + + section = new Menu (); + appmenu.append_section (null, section); + section.append (_("Keyboard Shortcuts"), "win.show-help-overlay"); + section.append (_("Help"), "app.help"); + section.append (_("About"), "app.about"); + section.append (_("Quit"), "app.quit"); + + app.app_menu = appmenu; + + app.add_accelerator ("<Ctrl>N", "app.new_document", null); + app.add_accelerator ("<Ctrl>S", "app.save", null); + app.add_accelerator ("<Ctrl>E", "app.email", null); + app.add_accelerator ("<Ctrl>P", "app.print", null); + app.add_accelerator ("F1", "app.help", null); + app.add_accelerator ("<Ctrl>Q", "app.quit", null); + + var gear_menu = new Menu (); + section = new Menu (); + gear_menu.append_section (null, section); + section.append (_("Email"), "app.email"); + section.append (_("Reorder Pages"), "app.reorder"); + section.append (_("Preferences"), "app.preferences"); + menu_button.set_menu_model (gear_menu); + } + app.add_window (this); + + /* Populate ActionBar (not supported in Glade) */ + /* https://bugzilla.gnome.org/show_bug.cgi?id=769966 */ + var button = new Gtk.Button.with_label (/* Label on new document button */ + _("Start Again…")); + button.visible = true; + button.clicked.connect (new_button_clicked_cb); + action_bar.pack_start (button); + + var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 10); + box.visible = true; + action_bar.set_center_widget (box); + + var rotate_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); + rotate_box.get_style_context ().add_class (Gtk.STYLE_CLASS_LINKED); + rotate_box.visible = true; + box.pack_start (rotate_box, false, true, 0); + + button = new Gtk.Button.from_icon_name ("object-rotate-left-symbolic"); + button.visible = true; + /* Tooltip for rotate left (counter-clockwise) button */ + button.tooltip_text = _("Rotate the page to the left (counter-clockwise)"); + button.clicked.connect (rotate_left_button_clicked_cb); + rotate_box.pack_start (button, false, true, 0); + + button = new Gtk.Button.from_icon_name ("object-rotate-right-symbolic"); + button.visible = true; + /* Tooltip for rotate right (clockwise) button */ + button.tooltip_text = _("Rotate the page to the right (clockwise)"); + button.clicked.connect (rotate_right_button_clicked_cb); + rotate_box.pack_start (button, false, true, 0); + + crop_button = new Gtk.ToggleButton (); + crop_button.visible = true; + var image = new Gtk.Image.from_icon_name ("edit-cut-symbolic", Gtk.IconSize.BUTTON); + image.visible = true; + crop_button.add (image); + /* Tooltip for crop button */ + crop_button.tooltip_text = _("Crop the selected page"); + crop_button.toggled.connect ((widget) => + { + if (updating_page_menu) + return; + + if (widget.active) + custom_crop_menuitem.active = true; + else + no_crop_menuitem.active = true; + }); + box.pack_start (crop_button, false, true, 0); + + delete_button = new Gtk.Button.from_icon_name ("user-trash-symbolic"); + delete_button.visible = true; + /* Tooltip for delete button */ + delete_button.tooltip_text = _("Delete the selected page"); + delete_button.clicked.connect (() => { book_view.book.delete_page (book_view.selected_page); }); + box.pack_start (delete_button, false, true, 0); + + var document_type = settings.get_string ("document-type"); + if (document_type != null) + set_document_hint (document_type); + + book_view = new BookView (book); + book_view.border_width = 18; + main_vbox.pack_start (book_view, true, true, 0); + book_view.page_selected.connect (page_selected_cb); + book_view.show_page.connect (show_page_cb); + book_view.show_menu.connect (show_page_menu_cb); + book_view.visible = true; + + preferences_dialog.transient_for = this; + + /* Load previous state */ + load_state (); + + /* Restore window size */ + debug ("Restoring window to %dx%d pixels", window_width, window_height); + set_default_size (window_width, window_height); + if (window_is_maximized) + { + debug ("Restoring window to maximized"); + maximize (); + } + if (window_is_fullscreen) + { + debug ("Restoring window to fullscreen"); + fullscreen (); + } + } + + private bool is_desktop (string name) + { + var desktop_name_list = Environment.get_variable ("XDG_CURRENT_DESKTOP"); + if (desktop_name_list == null) + return false; + + foreach (var n in desktop_name_list.split (":")) + if (n == name) + return true; + + return false; + } + + private bool is_traditional_desktop () + { + const string[] traditional_desktops = { "Unity", "XFCE", "MATE", "LXDE", "Cinnamon", "X-Cinnamon", "i3" }; + foreach (var name in traditional_desktops) + if (is_desktop (name)) + return true; + return false; + } + + private string state_filename + { + owned get { return Path.build_filename (Environment.get_user_cache_dir (), "simple-scan", "state"); } + } + + private void load_state () + { + debug ("Loading state from %s", state_filename); + + var f = new KeyFile (); + try + { + f.load_from_file (state_filename, KeyFileFlags.NONE); + } + catch (Error e) + { + if (!(e is FileError.NOENT)) + warning ("Failed to load state: %s", e.message); + } + window_width = state_get_integer (f, "window", "width", 600); + if (window_width <= 0) + window_width = 600; + window_height = state_get_integer (f, "window", "height", 400); + if (window_height <= 0) + window_height = 400; + window_is_maximized = state_get_boolean (f, "window", "is-maximized"); + window_is_fullscreen = state_get_boolean (f, "window", "is-fullscreen"); + } + + private int state_get_integer (KeyFile f, string group_name, string key, int default = 0) + { + try + { + return f.get_integer (group_name, key); + } + catch + { + return default; + } + } + + private bool state_get_boolean (KeyFile f, string group_name, string key, bool default = false) + { + try + { + return f.get_boolean (group_name, key); + } + catch + { + return default; + } + } + + private void save_state (bool force = false) + { + if (!force) + { + if (save_state_timeout != 0) + Source.remove (save_state_timeout); + save_state_timeout = Timeout.add (100, () => + { + save_state (true); + save_state_timeout = 0; + return false; + }); + return; + } + + debug ("Saving state to %s", state_filename); + + var f = new KeyFile (); + f.set_integer ("window", "width", window_width); + f.set_integer ("window", "height", window_height); + f.set_boolean ("window", "is-maximized", window_is_maximized); + f.set_boolean ("window", "is-fullscreen", window_is_fullscreen); + try + { + FileUtils.set_contents (state_filename, f.to_data ()); + } + catch (Error e) + { + warning ("Failed to write state: %s", e.message); + } + } + + public void start () + { + visible = true; + } +} + +private class CancellableProgressBar : Gtk.HBox +{ + private Gtk.ProgressBar bar; + private Gtk.Button? button; + + public CancellableProgressBar (string? text, Cancellable? cancellable) + { + bar = new Gtk.ProgressBar (); + bar.visible = true; + bar.set_text (text); + bar.set_show_text (true); + pack_start (bar); + + if (cancellable != null) + { + button = new Gtk.Button.with_label (/* Text of button for cancelling save */ + _("Cancel")); + button.visible = true; + button.clicked.connect (() => + { + set_visible (false); + cancellable.cancel (); + }); + pack_start (button); + } + } + + public void set_fraction (double fraction) + { + bar.set_fraction (fraction); + } + + public void destroy_with_delay (uint delay) + { + button.set_sensitive (false); + + Timeout.add (delay, () => + { + this.destroy (); + return false; + }); + } +} |