From ab6556e393162fff0e0e0c80a9fff689b4e2ca05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Frings-F=C3=BCrst?= Date: Sat, 11 Nov 2017 18:47:31 +0100 Subject: New upstream version 3.26.2 --- src/app-window.ui | 872 +++++++++++++++ src/app-window.vala | 1840 +++++++++++++++++++++++++++++++ src/authorize-dialog.ui | 141 +++ src/authorize-dialog.vala | 37 + src/book-view.vala | 38 +- src/book.vala | 781 +++++++++----- src/help-overlay.ui | 128 +++ src/libwebp.vapi | 55 + src/libwebpmux.vapi | 128 +++ src/meson.build | 9 +- src/page.vala | 63 +- src/preferences-dialog.ui | 608 +++++++++++ src/preferences-dialog.vala | 534 +++++++++ src/screensaver.vala | 25 + src/simple-scan.gresource.xml | 7 +- src/simple-scan.ui | 1454 ------------------------- src/simple-scan.vala | 175 ++- src/ui.vala | 2398 ----------------------------------------- 18 files changed, 5013 insertions(+), 4280 deletions(-) create mode 100644 src/app-window.ui create mode 100644 src/app-window.vala create mode 100644 src/authorize-dialog.ui create mode 100644 src/authorize-dialog.vala create mode 100644 src/help-overlay.ui create mode 100644 src/libwebp.vapi create mode 100644 src/libwebpmux.vapi create mode 100644 src/preferences-dialog.ui create mode 100644 src/preferences-dialog.vala create mode 100644 src/screensaver.vala delete mode 100644 src/simple-scan.ui delete mode 100644 src/ui.vala (limited to 'src') diff --git a/src/app-window.ui b/src/app-window.ui new file mode 100644 index 0000000..bbdf1c7 --- /dev/null +++ b/src/app-window.ui @@ -0,0 +1,872 @@ + + + + + + True + False + mail-send + + + True + False + gtk-help + + + True + False + + + True + False + Single _Page + True + + + + + + True + False + All Pages From _Feeder + True + + + + + + True + False + _Multiple Pages From Flatbed + True + + + + + + True + False + + + + + True + False + Text + True + True + + + + + + True + False + Photo + True + True + True + text_button_hb_menuitem + + + + + + + True + False + + + True + False + Single _Page + True + + + + + + True + False + All Pages From _Feeder + True + + + + + + True + False + _Multiple Pages From Flatbed + True + + + + + + True + False + + + + + True + False + Text + True + True + + + + + + True + False + Photo + True + True + True + text_button_menuitem + + + + + 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 , + * Eduard Gotwig + * + * 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 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 install driver software 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 (); + 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 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 " }; + + /* 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 ."); + + /* 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 Brother website."); + 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 Samsung website."); + 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 Epson website."); + 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 ("N", "app.new_document", null); + app.add_accelerator ("S", "app.save", null); + app.add_accelerator ("E", "app.email", null); + app.add_accelerator ("P", "app.print", null); + app.add_accelerator ("F1", "app.help", null); + app.add_accelerator ("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; + }); + } +} diff --git a/src/authorize-dialog.ui b/src/authorize-dialog.ui new file mode 100644 index 0000000..c099563 --- /dev/null +++ b/src/authorize-dialog.ui @@ -0,0 +1,141 @@ + + + + + + diff --git a/src/authorize-dialog.vala b/src/authorize-dialog.vala new file mode 100644 index 0000000..a6e5ab0 --- /dev/null +++ b/src/authorize-dialog.vala @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2009-2017 Canonical Ltd. + * Author: Robert Ancell , + * Eduard Gotwig + * + * 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. + */ + +[GtkTemplate (ui = "/org/gnome/SimpleScan/authorize-dialog.ui")] +private class AuthorizeDialog : Gtk.Dialog +{ + [GtkChild] + private Gtk.Label authorize_label; + [GtkChild] + private Gtk.Entry username_entry; + [GtkChild] + private Gtk.Entry password_entry; + + public AuthorizeDialog (string title) + { + authorize_label.set_text (title); + } + + public string get_username () + { + return username_entry.text; + } + + public string get_password () + { + return password_entry.text; + } +} diff --git a/src/book-view.vala b/src/book-view.vala index 182edc7..9a9c9bb 100644 --- a/src/book-view.vala +++ b/src/book-view.vala @@ -23,20 +23,6 @@ public class BookView : Gtk.Box private bool laying_out; private bool show_selected_page; - /* Page to show when book empty */ - private PageView? default_page_view = null; - public Page default_page - { - set - { - if (value == null) - default_page_view = null; - else - default_page_view = new PageView (value); - need_layout = true; - } - } - /* Currently selected page */ private PageView? selected_page_view = null; public Page? selected_page @@ -310,16 +296,8 @@ public class BookView : Gtk.Box private void layout_into (int width, int height, out int book_width, out int book_height) { var pages = new List (); - if (book.n_pages == 0) - { - if (default_page_view != null) - pages.append (default_page_view); - } - else - { - for (var i = 0; i < book.n_pages; i++) - pages.append (get_nth_page (i)); - } + for (var i = 0; i < book.n_pages; i++) + pages.append (get_nth_page (i)); /* Get maximum page resolution */ int max_dpi = 0; @@ -461,16 +439,8 @@ public class BookView : Gtk.Box context.clip_extents (out left, out top, out right, out bottom); var pages = new List (); - if (book.n_pages == 0) - { - if (default_page_view != null) - pages.append (default_page_view); - } - else - { - for (var i = 0; i < book.n_pages; i++) - pages.append (get_nth_page (i)); - } + for (var i = 0; i < book.n_pages; i++) + pages.append (get_nth_page (i)); /* Render each page */ foreach (var page in pages) diff --git a/src/book.vala b/src/book.vala index a843981..d53b31a 100644 --- a/src/book.vala +++ b/src/book.vala @@ -9,6 +9,8 @@ * license. */ +public delegate void ProgressionCallback (double fraction); + public class Book { private List pages; @@ -20,7 +22,6 @@ public class Book public signal void reordered (); public signal void cleared (); public signal void changed (); - public signal void saving (int i); public Book () { @@ -135,92 +136,440 @@ public class Book return pages.index (page); } - public File make_indexed_file (string uri, int i) + public async void save_async (string t, int q, File f, ProgressionCallback? p, Cancellable? c) throws Error { - if (n_pages == 1) - return File.new_for_uri (uri); - - /* Insert index before extension */ - var basename = Path.get_basename (uri); - string prefix = uri, suffix = ""; - var extension_index = basename.last_index_of_char ('.'); - if (extension_index >= 0) - { - suffix = basename.slice (extension_index, basename.length); - prefix = uri.slice (0, uri.length - suffix.length); - } - var width = n_pages.to_string().length; - var number_format = "%%0%dd".printf (width); - var filename = prefix + "-" + number_format.printf (i + 1) + suffix; - return File.new_for_uri (filename); + var book_saver = new BookSaver (); + yield book_saver.save_async (this, t, q, f, p, c); } +} - private void save_multi_file (string type, int quality, File file) throws Error +private class BookSaver +{ + private uint n_pages; + private int quality; + private File file; + private unowned ProgressionCallback progression_callback; + private double progression; + private Mutex progression_mutex; + private Cancellable? cancellable; + private AsyncQueue write_queue; + private ThreadPool encoder; + private SourceFunc save_async_callback; + + /* save_async get called in the main thread to start saving. It + * distributes all encode tasks to other threads then yield so + * the ui can continue operating. The method then return once saving + * is completed, cancelled, or failed */ + public async void save_async (Book book, string type, int quality, File file, ProgressionCallback? progression_callback, Cancellable? cancellable) throws Error { + var timer = new Timer (); + + this.n_pages = book.n_pages; + this.quality = quality; + this.file = file; + this.cancellable = cancellable; + this.save_async_callback = save_async.callback; + this.write_queue = new AsyncQueue (); + this.progression = 0; + this.progression_mutex = Mutex (); + + /* Configure a callback that monitor saving progression */ + if (progression_callback == null) + this.progression_callback = (fraction) => + { + debug ("Save progression: %f%%", fraction*100.0); + }; + else + this.progression_callback = progression_callback; + + /* Configure an encoder */ + ThreadPoolFunc? encode_delegate = null; + switch (type) + { + case "jpeg": + encode_delegate = encode_jpeg; + break; + case "png": + encode_delegate = encode_png; + break; +#if HAVE_WEBP + case "webp": + encode_delegate = encode_webp; + break; +#endif + case "pdf": + encode_delegate = encode_pdf; + break; + } + encoder = new ThreadPool.with_owned_data (encode_delegate, (int) get_num_processors (), false); + + /* Configure a writer */ + ThreadFunc? write_delegate = null; + switch (type) + { + case "jpeg": + case "png": +#if HAVE_WEBP + case "webp": +#endif + write_delegate = write_multifile; + break; + case "pdf": + write_delegate = write_pdf; + break; + } + var writer = new Thread (null, write_delegate); + + /* Issue encode tasks */ for (var i = 0; i < n_pages; i++) { - var page = get_page (i); - page.save (type, quality, make_indexed_file (file.get_uri (), i)); - saving (i); + var encode_task = new EncodeTask (); + encode_task.number = i; + encode_task.page = book.get_page(i); + encoder.add ((owned) encode_task); } + + /* Waiting for saving to finish */ + yield; + + /* Any error from any thread ends up here */ + var error = writer.join (); + if (error != null) + throw error; + + timer.stop (); + debug ("Save time: %f seconds", timer.elapsed (null)); } - private uint8[]? compress_zlib (uint8[] data) + /* Those methods are run in the encoder threads pool. It process + * one encode_task issued by save_async and reissue the result with + * a write_task */ + + private void encode_png (owned EncodeTask encode_task) { - var stream = ZLib.DeflateStream (ZLib.Level.BEST_COMPRESSION); - var out_data = new uint8[data.length]; + var page = encode_task.page; + var icc_data = page.get_icc_data_encoded (); + var write_task = new WriteTask (); + var image = page.get_image (true); - stream.next_in = data; - stream.next_out = out_data; - while (stream.avail_in > 0) + string[] keys = { "x-dpi", "y-dpi", "icc-profile", null }; + string[] values = { "%d".printf (page.dpi), "%d".printf (page.dpi), icc_data, null }; + if (icc_data == null) + keys[2] = null; + + try { - if (stream.deflate (ZLib.Flush.FINISH) == ZLib.Status.STREAM_ERROR) - break; + image.save_to_bufferv (out write_task.data, "png", keys, values); } + catch (Error error) + { + write_task.error = error; + } + write_task.number = encode_task.number; + write_queue.push ((owned) write_task); - if (stream.avail_in > 0) - return null; + update_progression (); + } - var n_written = data.length - stream.avail_out; - out_data.resize ((int) n_written); + private void encode_jpeg (owned EncodeTask encode_task) + { + var page = encode_task.page; + var icc_data = page.get_icc_data_encoded (); + var write_task = new WriteTask (); + var image = page.get_image (true); - return out_data; + string[] keys = { "x-dpi", "y-dpi", "quality", "icc-profile", null }; + string[] values = { "%d".printf (page.dpi), "%d".printf (page.dpi), "%d".printf (quality), icc_data, null }; + if (icc_data == null) + keys[3] = null; + + try + { + image.save_to_bufferv (out write_task.data, "jpeg", keys, values); + } + catch (Error error) + { + write_task.error = error; + } + write_task.number = encode_task.number; + write_queue.push ((owned) write_task); + + update_progression (); } - private ByteArray jpeg_data; +#if HAVE_WEBP + private void encode_webp (owned EncodeTask encode_task) + { + var page = encode_task.page; + var icc_data = page.get_icc_data_encoded (); + var write_task = new WriteTask (); + var image = page.get_image (true); + var webp_data = WebP.encode_rgb (image.get_pixels (), + image.get_width (), + image.get_height (), + image.get_rowstride (), + (float) quality); +#if HAVE_COLORD + WebP.MuxError mux_error; + var mux = WebP.Mux.new_mux (); + uint8[] output; + + mux_error = mux.set_image (webp_data, false); + debug ("mux.set_image: %s", mux_error.to_string ()); + + if (icc_data != null) + { + mux_error = mux.set_chunk ("ICCP", icc_data.data, false); + debug ("mux.set_chunk: %s", mux_error.to_string ()); + if (mux_error != WebP.MuxError.OK) + warning ("icc profile data not saved with page %i", encode_task.number); + } + + mux_error = mux.assemble (out output); + debug ("mux.assemble: %s", mux_error.to_string ()); + if (mux_error != WebP.MuxError.OK) + write_task.error = new FileError.FAILED (_("Unable to encode page %i").printf (encode_task.number)); - private uint8[] compress_jpeg (Gdk.Pixbuf image, int quality, int dpi) + write_task.data = (owned) output; +#else + + if (webp_data.length == 0) + write_task.error = new FileError.FAILED (_("Unable to encode page %i").printf (encode_task.number)); + + write_task.data = (owned) webp_data; +#endif + write_task.number = encode_task.number; + write_queue.push ((owned) write_task); + + update_progression (); + } +#endif + + private void encode_pdf (owned EncodeTask encode_task) { - jpeg_data = new ByteArray (); - string[] keys = { "quality", "density-unit", "x-density", "y-density", null }; - string[] values = { "%d".printf (quality), "dots-per-inch", "%d".printf (dpi), "%d".printf (dpi), null }; + var page = encode_task.page; + var image = page.get_image (true); + var width = image.width; + var height = image.height; + unowned uint8[] pixels = image.get_pixels (); + int depth = 8; + string color_space = "DeviceRGB"; + string? filter = null; + uint8[] data; + + if (page.is_color) + { + depth = 8; + color_space = "DeviceRGB"; + var data_length = height * width * 3; + data = new uint8[data_length]; + for (var row = 0; row < height; row++) + { + var in_offset = row * image.rowstride; + var out_offset = row * width * 3; + for (var x = 0; x < width; x++) + { + var in_o = in_offset + x*3; + var out_o = out_offset + x*3; + + data[out_o] = pixels[in_o]; + data[out_o+1] = pixels[in_o+1]; + data[out_o+2] = pixels[in_o+2]; + } + } + } + else if (page.depth == 2) + { + int shift_count = 6; + depth = 2; + color_space = "DeviceGray"; + var data_length = height * ((width * 2 + 7) / 8); + data = new uint8[data_length]; + var offset = 0; + for (var row = 0; row < height; row++) + { + /* Pad to the next line */ + if (shift_count != 6) + { + offset++; + shift_count = 6; + } + + var in_offset = row * image.rowstride; + for (var x = 0; x < width; x++) + { + /* Clear byte */ + if (shift_count == 6) + data[offset] = 0; + + /* Set bits */ + var p = pixels[in_offset + x*3]; + if (p >= 192) + data[offset] |= 3 << shift_count; + else if (p >= 128) + data[offset] |= 2 << shift_count; + else if (p >= 64) + data[offset] |= 1 << shift_count; + + /* Move to the next position */ + if (shift_count == 0) + { + offset++; + shift_count = 6; + } + else + shift_count -= 2; + } + } + } + else if (page.depth == 1) + { + int mask = 0x80; + + depth = 1; + color_space = "DeviceGray"; + var data_length = height * ((width + 7) / 8); + data = new uint8[data_length]; + var offset = 0; + for (var row = 0; row < height; row++) + { + /* Pad to the next line */ + if (mask != 0x80) + { + offset++; + mask = 0x80; + } + + var in_offset = row * image.rowstride; + for (var x = 0; x < width; x++) + { + /* Clear byte */ + if (mask == 0x80) + data[offset] = 0; + + /* Set bit */ + if (pixels[in_offset+x*3] != 0) + data[offset] |= (uint8) mask; + + /* Move to the next bit */ + mask >>= 1; + if (mask == 0) + { + offset++; + mask = 0x80; + } + } + } + } + else + { + depth = 8; + color_space = "DeviceGray"; + var data_length = height * width; + data = new uint8 [data_length]; + for (var row = 0; row < height; row++) + { + var in_offset = row * image.rowstride; + var out_offset = row * width; + for (var x = 0; x < width; x++) + data[out_offset+x] = pixels[in_offset+x*3]; + } + } + + /* Compress data and use zlib compression if it is smaller than JPEG. + * zlib compression is slower in the worst case, so do JPEG first + * and stop zlib if it exceeds the JPEG size */ + var write_task = new WriteTaskPDF (); + uint8[]? jpeg_data = null; try { - image.save_to_callbackv (write_pixbuf_data, "jpeg", keys, values); + jpeg_data = compress_jpeg (image, quality, page.dpi); } - catch (Error e) + catch (Error error) + { + write_task.error = error; + } + var zlib_data = compress_zlib (data, jpeg_data.length); + if (zlib_data != null) { + filter = "FlateDecode"; + data = zlib_data; + } + else + { + filter = "DCTDecode"; + data = jpeg_data; } - var data = (owned) jpeg_data.data; - jpeg_data = null; - return data; + write_task.number = encode_task.number; + write_task.data = data; + write_task.width = width; + write_task.height = height; + write_task.color_space = color_space; + write_task.depth = depth; + write_task.filter = filter; + write_task.dpi = page.dpi; + write_queue.push (write_task); + + update_progression (); } - private bool write_pixbuf_data (uint8[] buf) throws Error + private Error? write_multifile () { - jpeg_data.append (buf); - return true; + for (var i=0; i < n_pages; i++) + { + if (cancellable.is_cancelled ()) + { + finished_saving (); + return null; + } + + var write_task = write_queue.pop (); + if (write_task.error != null) + { + finished_saving (); + return write_task.error; + } + + var indexed_file = make_indexed_file (file.get_uri (), write_task.number, n_pages); + try + { + var stream = indexed_file.replace (null, false, FileCreateFlags.NONE); + stream.write_all (write_task.data, null); + } + catch (Error error) + { + finished_saving (); + return error; + } + } + + update_progression (); + finished_saving (); + return null; } - private void save_pdf (File file, int quality) throws Error + /* Those methods are run in the writer thread. It receive all + * write_tasks sent to it by the encoder threads and write those to + * disk. */ + + private Error? write_pdf () { /* Generate a random ID for this file */ var id = ""; for (var i = 0; i < 4; i++) id += "%08x".printf (Random.next_int ()); - var stream = file.replace (null, false, FileCreateFlags.NONE, null); + FileOutputStream? stream = null; + try + { + stream = file.replace (null, false, FileCreateFlags.NONE, null); + } + catch (Error error) + { + finished_saving (); + return error; + } var writer = new PDFWriter (stream); /* Choose object numbers */ @@ -304,163 +653,41 @@ public class Book writer.write_string (">>\n"); writer.write_string ("endobj\n"); - for (var i = 0; i < n_pages; i++) + /* Process each page in order */ + var tasks_in_standby = new Queue (); + for (int i = 0; i < n_pages; i++) { - var page = get_page (i); - var image = page.get_image (true); - var width = image.width; - var height = image.height; - unowned uint8[] pixels = image.get_pixels (); - var page_width = width * 72.0 / page.dpi; - var page_height = height * 72.0 / page.dpi; - - int depth = 8; - string color_space = "DeviceRGB"; - string? filter = null; - char[] width_buffer = new char[double.DTOSTR_BUF_SIZE]; - char[] height_buffer = new char[double.DTOSTR_BUF_SIZE]; - uint8[] data; - if (page.is_color) + if (cancellable.is_cancelled ()) { - depth = 8; - color_space = "DeviceRGB"; - var data_length = height * width * 3; - data = new uint8[data_length]; - for (var row = 0; row < height; row++) - { - var in_offset = row * image.rowstride; - var out_offset = row * width * 3; - for (var x = 0; x < width; x++) - { - var in_o = in_offset + x*3; - var out_o = out_offset + x*3; - - data[out_o] = pixels[in_o]; - data[out_o+1] = pixels[in_o+1]; - data[out_o+2] = pixels[in_o+2]; - } - } - } - else if (page.depth == 2) - { - int shift_count = 6; - depth = 2; - color_space = "DeviceGray"; - var data_length = height * ((width * 2 + 7) / 8); - data = new uint8[data_length]; - var offset = 0; - for (var row = 0; row < height; row++) - { - /* Pad to the next line */ - if (shift_count != 6) - { - offset++; - shift_count = 6; - } - - var in_offset = row * image.rowstride; - for (var x = 0; x < width; x++) - { - /* Clear byte */ - if (shift_count == 6) - data[offset] = 0; - - /* Set bits */ - var p = pixels[in_offset + x*3]; - if (p >= 192) - data[offset] |= 3 << shift_count; - else if (p >= 128) - data[offset] |= 2 << shift_count; - else if (p >= 64) - data[offset] |= 1 << shift_count; - - /* Move to the next position */ - if (shift_count == 0) - { - offset++; - shift_count = 6; - } - else - shift_count -= 2; - } - } + finished_saving (); + return null; } - else if (page.depth == 1) - { - int mask = 0x80; - - depth = 1; - color_space = "DeviceGray"; - var data_length = height * ((width + 7) / 8); - data = new uint8[data_length]; - var offset = 0; - for (var row = 0; row < height; row++) - { - /* Pad to the next line */ - if (mask != 0x80) - { - offset++; - mask = 0x80; - } - var in_offset = row * image.rowstride; - for (var x = 0; x < width; x++) - { - /* Clear byte */ - if (mask == 0x80) - data[offset] = 0; - - /* Set bit */ - if (pixels[in_offset+x*3] != 0) - data[offset] |= (uint8) mask; - - /* Move to the next bit */ - mask >>= 1; - if (mask == 0) - { - offset++; - mask = 0x80; - } - } - } - } + var write_task = tasks_in_standby.peek_head (); + if (write_task != null && write_task.number == i) + tasks_in_standby.pop_head (); else { - depth = 8; - color_space = "DeviceGray"; - var data_length = height * width; - data = new uint8 [data_length]; - for (var row = 0; row < height; row++) + while (true) { - var in_offset = row * image.rowstride; - var out_offset = row * width; - for (var x = 0; x < width; x++) - data[out_offset+x] = pixels[in_offset+x*3]; - } - } - - /* Compress data */ - var compressed_data = compress_zlib (data); - if (compressed_data != null) - { - /* Try if JPEG compression is better */ - if (depth > 1) - { - var jpeg_data = compress_jpeg (image, quality, page.dpi); - if (jpeg_data.length < compressed_data.length) + write_task = (WriteTaskPDF) write_queue.pop (); + if (write_task.error != null) { - filter = "DCTDecode"; - data = jpeg_data; + finished_saving (); + return write_task.error; } - } + if (write_task.number == i) + break; - if (filter == null) - { - filter = "FlateDecode"; - data = compressed_data; + tasks_in_standby.insert_sorted (write_task, (a, b) => {return a.number - b.number;}); } } + var page_width = write_task.width * 72.0 / write_task.dpi; + var page_height = write_task.height * 72.0 / write_task.dpi; + var width_buffer = new char[double.DTOSTR_BUF_SIZE]; + var height_buffer = new char[double.DTOSTR_BUF_SIZE]; + /* Page */ writer.write_string ("\n"); writer.start_object (page_numbers[i]); @@ -481,16 +708,16 @@ public class Book writer.write_string ("<<\n"); writer.write_string ("/Type /XObject\n"); writer.write_string ("/Subtype /Image\n"); - writer.write_string ("/Width %d\n".printf (width)); - writer.write_string ("/Height %d\n".printf (height)); - writer.write_string ("/ColorSpace /%s\n".printf (color_space)); - writer.write_string ("/BitsPerComponent %d\n".printf (depth)); - writer.write_string ("/Length %d\n".printf (data.length)); - if (filter != null) - writer.write_string ("/Filter /%s\n".printf (filter)); + writer.write_string ("/Width %d\n".printf (write_task.width)); + writer.write_string ("/Height %d\n".printf (write_task.height)); + writer.write_string ("/ColorSpace /%s\n".printf (write_task.color_space)); + writer.write_string ("/BitsPerComponent %d\n".printf (write_task.depth)); + writer.write_string ("/Length %d\n".printf (write_task.data.length)); + if (write_task.filter != null) + writer.write_string ("/Filter /%s\n".printf (write_task.filter)); writer.write_string (">>\n"); writer.write_string ("stream\n"); - writer.write (data); + writer.write (write_task.data); writer.write_string ("\n"); writer.write_string ("endstream\n"); writer.write_string ("endobj\n"); @@ -500,7 +727,7 @@ public class Book writer.start_object (struct_tree_root_number); writer.write_string ("%u 0 obj\n".printf (struct_tree_root_number)); writer.write_string ("<<\n"); - writer.write_string ("/Type /StructTreeRoot\n"); + writer.write_string ("/Type /StructTreeRoot\n"); writer.write_string (">>\n"); writer.write_string ("endobj\n"); @@ -517,8 +744,6 @@ public class Book writer.write_string ("\n"); writer.write_string ("endstream\n"); writer.write_string ("endobj\n"); - - saving (i); } /* Info */ @@ -535,10 +760,10 @@ public class Book var xref_offset = writer.offset; writer.write_string ("xref\n"); writer.write_string ("0 %zu\n".printf (writer.object_offsets.length + 1)); - writer.write_string ("%010zu 65535 f \n".printf (next_empty_object (writer, 0))); + writer.write_string ("%010zu 65535 f \n".printf (writer.next_empty_object (0))); for (var i = 0; i < writer.object_offsets.length; i++) if (writer.object_offsets[i] == 0) - writer.write_string ("%010zu 65535 f \n".printf (next_empty_object (writer, i + 1))); + writer.write_string ("%010zu 65535 f \n".printf (writer.next_empty_object (i + 1))); else writer.write_string ("%010zu 00000 n \n".printf (writer.object_offsets[i])); @@ -554,31 +779,102 @@ public class Book writer.write_string ("startxref\n"); writer.write_string ("%zu\n".printf (xref_offset)); writer.write_string ("%%EOF\n"); + + update_progression (); + finished_saving (); + return null; } - static int next_empty_object (PDFWriter writer, int start) + /* update_progression is called once by page by encoder threads and + * once at the end by writer thread. */ + private void update_progression () { - for (var i = start; i < writer.object_offsets.length; i++) - if (writer.object_offsets[i] == 0) - return i + 1; - return 0; + double step = 1.0 / (double)(n_pages+1); + progression_mutex.lock (); + progression += step; + progression_mutex.unlock (); + Idle.add (() => + { + progression_callback (progression); + return false; + }); } - public void save (string type, int quality, File file) throws Error + /* finished_saving is called by the writer thread when it's done, + * meaning there is nothing left to do or saving has been + * cancelled */ + private void finished_saving () { - switch (type) + /* At this point, any remaining encode_task ought to remain unprocessed */ + ThreadPool.free ((owned) encoder, true, true); + + /* Wake-up save_async method in main thread */ + Idle.add ((owned)save_async_callback); + } + + /* Utility methods */ + + private static uint8[]? compress_zlib (uint8[] data, uint max_size) + { + var stream = ZLib.DeflateStream (ZLib.Level.BEST_COMPRESSION); + var out_data = new uint8[max_size]; + + stream.next_in = data; + stream.next_out = out_data; + while (true) { - case "jpeg": - case "png": - save_multi_file (type, quality, file); - break; - case "pdf": - save_pdf (file, quality); - break; + /* Compression complete */ + if (stream.avail_in == 0) + break; + + /* Out of space */ + if (stream.avail_out == 0) + return null; + + if (stream.deflate (ZLib.Flush.FINISH) == ZLib.Status.STREAM_ERROR) + return null; } + + var n_written = out_data.length - stream.avail_out; + out_data.resize ((int) n_written); + + return out_data; + } + + private static uint8[] compress_jpeg (Gdk.Pixbuf image, int quality, int dpi) throws Error + { + uint8[] jpeg_data; + string[] keys = { "quality", "x-dpi", "y-dpi", null }; + string[] values = { "%d".printf (quality), "%d".printf (dpi), "%d".printf (dpi), null }; + + image.save_to_bufferv (out jpeg_data, "jpeg", keys, values); + return jpeg_data; } } +private class EncodeTask +{ + public int number; + public Page page; +} + +private class WriteTask +{ + public int number; + public uint8[] data; + public Error error; +} + +private class WriteTaskPDF : WriteTask +{ + public int width; + public int height; + public string color_space; + public int depth; + public string? filter; + public int dpi; +} + private class PDFWriter { public size_t offset = 0; @@ -621,31 +917,32 @@ private class PDFWriter { object_offsets[index - 1] = (uint)offset; } -} - -public class PsWriter -{ - public Cairo.PsSurface surface; - public FileOutputStream stream; - public PsWriter (FileOutputStream stream) + public int next_empty_object (int start) { - this.stream = stream; - surface = new Cairo.PsSurface.for_stream (write_cairo_data, 0, 0); + for (var i = start; i < object_offsets.length; i++) + if (object_offsets[i] == 0) + return i + 1; + return 0; } +} - private Cairo.Status write_cairo_data (uint8[] data) +public File make_indexed_file (string uri, uint i, uint n_pages) +{ + if (n_pages == 1) + return File.new_for_uri (uri); + + /* Insert index before extension */ + var basename = Path.get_basename (uri); + string prefix = uri, suffix = ""; + var extension_index = basename.last_index_of_char ('.'); + if (extension_index >= 0) { - try - { - stream.write_all (data, null, null); - } - catch (Error e) - { - warning ("Error writing data: %s", e.message); - return Cairo.Status.WRITE_ERROR; - } - - return Cairo.Status.SUCCESS; + suffix = basename.slice (extension_index, basename.length); + prefix = uri.slice (0, uri.length - suffix.length); } + var width = n_pages.to_string().length; + var number_format = "%%0%dd".printf (width); + var filename = prefix + "-" + number_format.printf (i + 1) + suffix; + return File.new_for_uri (filename); } diff --git a/src/help-overlay.ui b/src/help-overlay.ui new file mode 100644 index 0000000..dabec9f --- /dev/null +++ b/src/help-overlay.ui @@ -0,0 +1,128 @@ + + + + + 1 + + + 1 + + + 1 + Scanning + + + 1 + <ctrl>1 + Scan a single page + + + + + 1 + <ctrl>f + Scan all pages from document feeder + + + + + 1 + <ctrl>m + Scan continuously from a flatbed scanner + + + + + 1 + Escape + Stop scan in progress + + + + + + + 1 + Document Modification + + + 1 + less + Move page left + + + + + 1 + greater + Move page right + + + + + 1 + bracketleft + Rotate page to the left (anti-clockwise) + + + + + 1 + bracketright + Rotate page to the right (clockwise) + + + + + 1 + Delete + Delete page + + + + + + + 1 + Document Management + + + 1 + <ctrl>n + Start new document + + + + + 1 + <ctrl>s + Save scanned document + + + + + 1 + <ctrl>e + Email scanned document + + + + + 1 + <ctrl>p + Print scanned document + + + + + 1 + <ctrl>c + Copy current page to clipboard + + + + + + + + diff --git a/src/libwebp.vapi b/src/libwebp.vapi new file mode 100644 index 0000000..74bfdd5 --- /dev/null +++ b/src/libwebp.vapi @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2017 Stéphane Fillion + * Authors: Stéphane Fillion + * + * 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. + */ + +namespace WebP +{ + // Returns the size of the compressed data (pointed to by *output), or 0 if + // an error occurred. The compressed data must be released by the caller + // using the call 'free(*output)'. + // These functions compress using the lossy format, and the quality_factor + // can go from 0 (smaller output, lower quality) to 100 (best quality, + // larger output). + [CCode (cheader_filename = "webp/encode.h", cname = "WebPEncodeRGB")] + private size_t _encode_rgb ([CCode (array_length = false)] uint8[] rgb, + int width, + int height, + int stride, + float quality_factor, + [CCode (array_length = false)] out uint8[] output); + [CCode (cname = "vala_encode_rgb")] + public uint8[] encode_rgb (uint8[] rgb, int width, int height, int stride, float quality_factor) + { + uint8[] output; + size_t length; + length = _encode_rgb (rgb, width, height, stride, quality_factor, out output); + output.length = (int) length; + return output; + } + + // These functions are the equivalent of the above, but compressing in a + // lossless manner. Files are usually larger than lossy format, but will + // not suffer any compression loss. + [CCode (cheader_filename = "webp/encode.h", cname = "WebPEncodeLosslessRGB")] + private size_t _encode_lossless_rgb ([CCode (array_length = false)] uint8[] rgb, + int width, + int height, + int stride, + [CCode (array_length = false)] out uint8[] output); + [CCode (cname = "vala_encode_lossless_rgb")] + public uint8[] encode_lossless_rgb (uint8[] rgb, int width, int height, int stride) + { + uint8[] output; + size_t length; + length = _encode_lossless_rgb (rgb, width, height, stride, out output); + output.length = (int) length; + return output; + } +} diff --git a/src/libwebpmux.vapi b/src/libwebpmux.vapi new file mode 100644 index 0000000..f2461a2 --- /dev/null +++ b/src/libwebpmux.vapi @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2017 Stéphane Fillion + * Authors: Stéphane Fillion + * + * 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. + */ + +namespace WebP +{ + // Error codes + [CCode (cheader_filename = "webp/mux.h", cname = "WebPMuxError", cprefix = "WEBP_MUX_", has_type_id = false)] + public enum MuxError + { + OK = 1, + NOT_FOUND = 0, + INVALID_ARGUMENT = -1, + BAD_DATA = -2, + MEMORY_ERROR = -3, + NOT_ENOUGH_DATA = -4 + } + + // Data type used to describe 'raw' data, e.g., chunk data + // (ICC profile, metadata) and WebP compressed image data. + [CCode (cheader_filename = "webp/mux.h", cname = "WebPData", destroy_function = "", has_type_id = false)] + private struct Data + { + [CCode (array_length = false)] unowned uint8[] bytes; + size_t size; + } + + // main opaque object. + [CCode (cheader_filename = "webp/mux.h", cname = "WebPMux", free_function = "WebPMuxDelete")] + [Compact] + public class Mux + { + // Creates an empty mux object. + // Returns: + // A pointer to the newly created empty mux object. + // Or NULL in case of memory error. + [CCode (cname = "WebPMuxNew")] + public static Mux? new_mux (); + + // Sets the (non-animated and non-fragmented) image in the mux object. + // Note: Any existing images (including frames/fragments) will be removed. + // Parameters: + // mux - (in/out) object in which the image is to be set + // bitstream - (in) can be a raw VP8/VP8L bitstream or a single-image + // WebP file (non-animated and non-fragmented) + // copy_data - (in) value 1 indicates given data WILL be copied to the mux + // object and value 0 indicates data will NOT be copied. + // Returns: + // WEBP_MUX_INVALID_ARGUMENT - if mux is NULL or bitstream is NULL. + // WEBP_MUX_MEMORY_ERROR - on memory allocation error. + // WEBP_MUX_OK - on success. + [CCode (cname = "WebPMuxSetImage")] + private MuxError _set_image (Data bitstream, bool copy_data); + [CCode (cname = "vala_set_image")] + public MuxError set_image (uint8[] bitstream, bool copy_data) + { + Data data; + data.bytes = bitstream; + data.size = bitstream.length; + return _set_image (data, copy_data); + } + + // Adds a chunk with id 'fourcc' and data 'chunk_data' in the mux object. + // Any existing chunk(s) with the same id will be removed. + // Parameters: + // mux - (in/out) object to which the chunk is to be added + // fourcc - (in) a character array containing the fourcc of the given chunk; + // e.g., "ICCP", "XMP ", "EXIF" etc. + // chunk_data - (in) the chunk data to be added + // copy_data - (in) value 1 indicates given data WILL be copied to the mux + // object and value 0 indicates data will NOT be copied. + // Returns: + // WEBP_MUX_INVALID_ARGUMENT - if mux, fourcc or chunk_data is NULL + // or if fourcc corresponds to an image chunk. + // WEBP_MUX_MEMORY_ERROR - on memory allocation error. + // WEBP_MUX_OK - on success. + [CCode (cname = "WebPMuxSetChunk")] + private MuxError _set_chunk ([CCode (array_length = false)] uchar[] fourcc, + Data chunk_data, + bool copy_data); + [CCode (cname = "vala_set_chunk")] + public MuxError set_chunk (string fourcc, uint8[] chunk_data, bool copy_data) + requires (fourcc.length == 4) + { + Data data; + data.bytes = chunk_data; + data.size = chunk_data.length; + return _set_chunk ((uchar[]) fourcc, data, copy_data); + } + + // Assembles all chunks in WebP RIFF format and returns in 'assembled_data'. + // This function also validates the mux object. + // Note: The content of 'assembled_data' will be ignored and overwritten. + // Also, the content of 'assembled_data' is allocated using malloc(), and NOT + // owned by the 'mux' object. It MUST be deallocated by the caller by calling + // WebPDataClear(). It's always safe to call WebPDataClear() upon return, + // even in case of error. + // Parameters: + // mux - (in/out) object whose chunks are to be assembled + // assembled_data - (out) assembled WebP data + // Returns: + // WEBP_MUX_BAD_DATA - if mux object is invalid. + // WEBP_MUX_INVALID_ARGUMENT - if mux or assembled_data is NULL. + // WEBP_MUX_MEMORY_ERROR - on memory allocation error. + // WEBP_MUX_OK - on success. + [CCode (cname = "WebPMuxAssemble")] + private MuxError _assemble (out Data assembled_data); + [CCode (cname = "vala_assemble")] + public MuxError assemble (out uint8[] assembled_data) + { + Data data; + MuxError mux_error; + unowned uint8[] out_array; + mux_error = _assemble (out data); + out_array = data.bytes; + out_array.length = (int) data.size; + assembled_data = out_array; + return mux_error; + } + } +} diff --git a/src/meson.build b/src/meson.build index cfda86d..9e40e42 100644 --- a/src/meson.build +++ b/src/meson.build @@ -12,16 +12,23 @@ if packagekit_dep.found () vala_args += [ '-D', 'HAVE_PACKAGEKIT' ] dependencies += packagekit_dep endif +if webp_dep.found () and (not colord_dep.found () or webpmux_dep.found ()) # Webpmux only required if colord + vala_args += [ '-D', 'HAVE_WEBP' ] + dependencies += [ webp_dep, webpmux_dep ] +endif simple_scan = executable ('simple-scan', [ 'config.vapi', + 'app-window.vala', + 'authorize-dialog.vala', 'book.vala', 'book-view.vala', 'page.vala', 'page-view.vala', + 'preferences-dialog.vala', 'simple-scan.vala', 'scanner.vala', - 'ui.vala', + 'screensaver.vala', 'autosave-manager.vala' ] + resources, dependencies: dependencies, vala_args: vala_args, diff --git a/src/page.vala b/src/page.vala index 8936187..582aef8 100644 --- a/src/page.vala +++ b/src/page.vala @@ -624,13 +624,16 @@ public class Page return image; } - private string? get_icc_data_encoded (string icc_profile_filename) + public string? get_icc_data_encoded () { + if (color_profile == null) + return null; + /* Get binary data */ string contents; try { - FileUtils.get_contents (icc_profile_filename, out contents); + FileUtils.get_contents (color_profile, out contents); } catch (Error e) { @@ -641,63 +644,33 @@ public class Page /* Encode into base64 */ return Base64.encode ((uchar[]) contents.to_utf8 ()); } - + public void copy_to_clipboard (Gtk.Window window) - { + { var display = window.get_display (); var clipboard = Gtk.Clipboard.get_for_display (display, Gdk.SELECTION_CLIPBOARD); var image = get_image (true); clipboard.set_image (image); } - public void save (string type, int quality, File file) throws Error + public void save_png (File file) throws Error { var stream = file.replace (null, false, FileCreateFlags.NONE, null); - var writer = new PixbufWriter (stream); var image = get_image (true); string? icc_profile_data = null; if (color_profile != null) - icc_profile_data = get_icc_data_encoded (color_profile); + icc_profile_data = get_icc_data_encoded (); - if (strcmp (type, "jpeg") == 0) - { - string[] keys = { "x-dpi", "y-dpi", "quality", "icc-profile", null }; - string[] values = { "%d".printf (dpi), "%d".printf (dpi), "%d".printf (quality), icc_profile_data, null }; - if (icc_profile_data == null) - keys[3] = null; - writer.save (image, "jpeg", keys, values); - } - else if (strcmp (type, "png") == 0) - { - string[] keys = { "x-dpi", "y-dpi", "icc-profile", null }; - string[] values = { "%d".printf (dpi), "%d".printf (dpi), icc_profile_data, null }; - if (icc_profile_data == null) - keys[2] = null; - writer.save (image, "png", keys, values); - } - else - throw new FileError.INVAL ("Unknown file type: %s".printf (type)); - } -} + string[] keys = { "x-dpi", "y-dpi", "icc-profile", null }; + string[] values = { "%d".printf (dpi), "%d".printf (dpi), icc_profile_data, null }; + if (icc_profile_data == null) + keys[2] = null; -public class PixbufWriter -{ - public FileOutputStream stream; - - public PixbufWriter (FileOutputStream stream) - { - this.stream = stream; - } - - public void save (Gdk.Pixbuf image, string type, string[] option_keys, string[] option_values) throws Error - { - image.save_to_callbackv (write_pixbuf_data, type, option_keys, option_values); - } - - private bool write_pixbuf_data (uint8[] buf) throws Error - { - stream.write_all (buf, null, null); - return true; + image.save_to_callbackv ((buf) => + { + stream.write_all (buf, null, null); + return true; + }, "png", keys, values); } } diff --git a/src/preferences-dialog.ui b/src/preferences-dialog.ui new file mode 100644 index 0000000..2272b77 --- /dev/null +++ b/src/preferences-dialog.ui @@ -0,0 +1,608 @@ + + + + + + -100 + 100 + 1 + 10 + + + -100 + 100 + 1 + 10 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/preferences-dialog.vala b/src/preferences-dialog.vala new file mode 100644 index 0000000..bf213fb --- /dev/null +++ b/src/preferences-dialog.vala @@ -0,0 +1,534 @@ +/* + * Copyright (C) 2009-2017 Canonical Ltd. + * Author: Robert Ancell , + * Eduard Gotwig + * + * 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. + */ + +[GtkTemplate (ui = "/org/gnome/SimpleScan/preferences-dialog.ui")] +private class PreferencesDialog : Gtk.Dialog +{ + private Settings settings; + + private bool setting_devices; + private bool user_selected_device; + + [GtkChild] + private Gtk.ComboBox device_combo; + [GtkChild] + private Gtk.ComboBox text_dpi_combo; + [GtkChild] + private Gtk.ComboBox photo_dpi_combo; + [GtkChild] + private Gtk.ComboBox paper_size_combo; + [GtkChild] + private Gtk.Scale brightness_scale; + [GtkChild] + private Gtk.Scale contrast_scale; + [GtkChild] + private Gtk.ListStore device_model; + [GtkChild] + private Gtk.RadioButton page_delay_3s_button; + [GtkChild] + private Gtk.RadioButton page_delay_5s_button; + [GtkChild] + private Gtk.RadioButton page_delay_7s_button; + [GtkChild] + private Gtk.RadioButton page_delay_10s_button; + [GtkChild] + private Gtk.RadioButton page_delay_15s_button; + [GtkChild] + private Gtk.ListStore text_dpi_model; + [GtkChild] + private Gtk.ListStore photo_dpi_model; + [GtkChild] + private Gtk.RadioButton front_side_button; + [GtkChild] + private Gtk.RadioButton back_side_button; + [GtkChild] + private Gtk.RadioButton both_side_button; + [GtkChild] + private Gtk.ListStore paper_size_model; + [GtkChild] + private Gtk.Adjustment brightness_adjustment; + [GtkChild] + private Gtk.Adjustment contrast_adjustment; + [GtkChild] + private Gtk.Button preferences_close_button; + + public PreferencesDialog (Settings settings, bool use_header_bar) + { + Object (use_header_bar: use_header_bar ? 1 : -1); + + if (use_header_bar) + preferences_close_button.visible = false; + + this.settings = settings; + + Gtk.TreeIter iter; + paper_size_model.append (out iter); + paper_size_model.set (iter, 0, 0, 1, 0, 2, + /* Combo box value for automatic paper size */ + _("Automatic"), -1); + paper_size_model.append (out iter); + paper_size_model.set (iter, 0, 1050, 1, 1480, 2, "A6", -1); + paper_size_model.append (out iter); + paper_size_model.set (iter, 0, 1480, 1, 2100, 2, "A5", -1); + paper_size_model.append (out iter); + paper_size_model.set (iter, 0, 2100, 1, 2970, 2, "A4", -1); + paper_size_model.append (out iter); + paper_size_model.set (iter, 0, 2159, 1, 2794, 2, "Letter", -1); + paper_size_model.append (out iter); + paper_size_model.set (iter, 0, 2159, 1, 3556, 2, "Legal", -1); + paper_size_model.append (out iter); + paper_size_model.set (iter, 0, 1016, 1, 1524, 2, "4×6", -1); + + var renderer = new Gtk.CellRendererText (); + device_combo.pack_start (renderer, true); + device_combo.add_attribute (renderer, "text", 1); + + var dpi = settings.get_int ("text-dpi"); + if (dpi <= 0) + dpi = DEFAULT_TEXT_DPI; + set_dpi_combo (text_dpi_combo, DEFAULT_TEXT_DPI, dpi); + text_dpi_combo.changed.connect (() => { settings.set_int ("text-dpi", get_text_dpi ()); }); + dpi = settings.get_int ("photo-dpi"); + if (dpi <= 0) + dpi = DEFAULT_PHOTO_DPI; + set_dpi_combo (photo_dpi_combo, DEFAULT_PHOTO_DPI, dpi); + photo_dpi_combo.changed.connect (() => { settings.set_int ("photo-dpi", get_photo_dpi ()); }); + + set_page_side ((ScanType) settings.get_enum ("page-side")); + front_side_button.toggled.connect ((button) => { if (button.active) settings.set_enum ("page-side", ScanType.ADF_FRONT); }); + back_side_button.toggled.connect ((button) => { if (button.active) settings.set_enum ("page-side", ScanType.ADF_BACK); }); + both_side_button.toggled.connect ((button) => { if (button.active) settings.set_enum ("page-side", ScanType.ADF_BOTH); }); + + renderer = new Gtk.CellRendererText (); + paper_size_combo.pack_start (renderer, true); + paper_size_combo.add_attribute (renderer, "text", 2); + + var lower = brightness_adjustment.lower; + var darker_label = "%s".printf (_("Darker")); + var upper = brightness_adjustment.upper; + var lighter_label = "%s".printf (_("Lighter")); + brightness_scale.add_mark (lower, Gtk.PositionType.BOTTOM, darker_label); + brightness_scale.add_mark (0, Gtk.PositionType.BOTTOM, null); + brightness_scale.add_mark (upper, Gtk.PositionType.BOTTOM, lighter_label); + brightness_adjustment.value = settings.get_int ("brightness"); + brightness_adjustment.value_changed.connect (() => { settings.set_int ("brightness", get_brightness ()); }); + + lower = contrast_adjustment.lower; + var less_label = "%s".printf (_("Less")); + upper = contrast_adjustment.upper; + var more_label = "%s".printf (_("More")); + contrast_scale.add_mark (lower, Gtk.PositionType.BOTTOM, less_label); + contrast_scale.add_mark (0, Gtk.PositionType.BOTTOM, null); + contrast_scale.add_mark (upper, Gtk.PositionType.BOTTOM, more_label); + contrast_adjustment.value = settings.get_int ("contrast"); + contrast_adjustment.value_changed.connect (() => { settings.set_int ("contrast", get_contrast ()); }); + + var paper_width = settings.get_int ("paper-width"); + var paper_height = settings.get_int ("paper-height"); + set_paper_size (paper_width, paper_height); + paper_size_combo.changed.connect (() => + { + int w, h; + get_paper_size (out w, out h); + settings.set_int ("paper-width", w); + settings.set_int ("paper-height", h); + }); + + set_page_delay (settings.get_int ("page-delay")); + page_delay_3s_button.toggled.connect ((button) => { if (button.active) settings.set_int ("page-delay", 3); }); + page_delay_5s_button.toggled.connect ((button) => { if (button.active) settings.set_int ("page-delay", 5); }); + page_delay_7s_button.toggled.connect ((button) => { if (button.active) settings.set_int ("page-delay", 7); }); + page_delay_10s_button.toggled.connect ((button) => { if (button.active) settings.set_int ("page-delay", 10); }); + page_delay_15s_button.toggled.connect ((button) => { if (button.active) settings.set_int ("page-delay", 15); }); + } + + public void set_scan_devices (List devices) + { + setting_devices = true; + + /* If the user hasn't chosen a scanner choose the best available one */ + var have_selection = false; + if (user_selected_device) + have_selection = device_combo.active >= 0; + + /* Add new devices */ + int index = 0; + Gtk.TreeIter iter; + foreach (var device in devices) + { + int n_delete = -1; + + /* Find if already exists */ + if (device_model.iter_nth_child (out iter, null, index)) + { + int i = 0; + do + { + string name; + bool matched; + + device_model.get (iter, 0, out name, -1); + matched = name == device.name; + + if (matched) + { + n_delete = i; + break; + } + i++; + } while (device_model.iter_next (ref iter)); + } + + /* If exists, remove elements up to this one */ + if (n_delete >= 0) + { + int i; + + /* Update label */ + device_model.set (iter, 1, device.label, -1); + + for (i = 0; i < n_delete; i++) + { + device_model.iter_nth_child (out iter, null, index); +#if VALA_0_36 + device_model.remove (ref iter); +#else + device_model.remove (iter); +#endif + } + } + else + { + device_model.insert (out iter, index); + device_model.set (iter, 0, device.name, 1, device.label, -1); + } + index++; + } + + /* Remove any remaining devices */ + while (device_model.iter_nth_child (out iter, null, index)) +#if VALA_0_36 + device_model.remove (ref iter); +#else + device_model.remove (iter); +#endif + + /* Select the previously selected device or the first available device */ + if (!have_selection) + { + var device = settings.get_string ("selected-device"); + if (device != null && find_scan_device (device, out iter)) + device_combo.set_active_iter (iter); + else + device_combo.set_active (0); + } + + setting_devices = false; + } + + public string? get_selected_device () + { + Gtk.TreeIter iter; + + if (device_combo.get_active_iter (out iter)) + { + string device; + device_model.get (iter, 0, out device, -1); + return device; + } + + return null; + } + + public string? get_selected_device_label () + { + Gtk.TreeIter iter; + + if (device_combo.get_active_iter (out iter)) + { + string label; + device_model.get (iter, 1, out label, -1); + return label; + } + + return null; + } + + public void set_selected_device (string device) + { + user_selected_device = true; + + Gtk.TreeIter iter; + if (!find_scan_device (device, out iter)) + return; + + device_combo.set_active_iter (iter); + } + + private bool find_scan_device (string device, out Gtk.TreeIter iter) + { + bool have_iter = false; + + if (device_model.get_iter_first (out iter)) + { + do + { + string d; + device_model.get (iter, 0, out d, -1); + if (d == device) + have_iter = true; + } while (!have_iter && device_model.iter_next (ref iter)); + } + + return have_iter; + } + + private void set_page_side (ScanType page_side) + { + switch (page_side) + { + case ScanType.ADF_FRONT: + front_side_button.active = true; + break; + case ScanType.ADF_BACK: + back_side_button.active = true; + break; + default: + case ScanType.ADF_BOTH: + both_side_button.active = true; + break; + } + } + + public ScanType get_page_side () + { + if (front_side_button.active) + return ScanType.ADF_FRONT; + else if (back_side_button.active) + return ScanType.ADF_BACK; + else + return ScanType.ADF_BOTH; + } + + public void set_paper_size (int width, int height) + { + Gtk.TreeIter iter; + bool have_iter; + + for (have_iter = paper_size_model.get_iter_first (out iter); + have_iter; + have_iter = paper_size_model.iter_next (ref iter)) + { + int w, h; + paper_size_model.get (iter, 0, out w, 1, out h, -1); + if (w == width && h == height) + break; + } + + if (!have_iter) + have_iter = paper_size_model.get_iter_first (out iter); + if (have_iter) + paper_size_combo.set_active_iter (iter); + } + + public int get_text_dpi () + { + Gtk.TreeIter iter; + int dpi = DEFAULT_TEXT_DPI; + + if (text_dpi_combo.get_active_iter (out iter)) + text_dpi_model.get (iter, 0, out dpi, -1); + + return dpi; + } + + public int get_photo_dpi () + { + Gtk.TreeIter iter; + int dpi = DEFAULT_PHOTO_DPI; + + if (photo_dpi_combo.get_active_iter (out iter)) + photo_dpi_model.get (iter, 0, out dpi, -1); + + return dpi; + } + + public bool get_paper_size (out int width, out int height) + { + Gtk.TreeIter iter; + + width = height = 0; + if (paper_size_combo.get_active_iter (out iter)) + { + paper_size_model.get (iter, 0, ref width, 1, ref height, -1); + return true; + } + + return false; + } + + public int get_brightness () + { + return (int) brightness_adjustment.value; + } + + public void set_brightness (int brightness) + { + brightness_adjustment.value = brightness; + } + + public int get_contrast () + { + return (int) contrast_adjustment.value; + } + + public void set_contrast (int contrast) + { + contrast_adjustment.value = contrast; + } + + public int get_page_delay () + { + if (page_delay_15s_button.active) + return 15; + else if (page_delay_10s_button.active) + return 10; + else if (page_delay_7s_button.active) + return 7; + else if (page_delay_5s_button.active) + return 5; + else + return 3; + } + + public void set_page_delay (int page_delay) + { + if (page_delay >= 15) + page_delay_15s_button.active = true; + else if (page_delay >= 10) + page_delay_10s_button.active = true; + else if (page_delay >= 7) + page_delay_7s_button.active = true; + else if (page_delay >= 5) + page_delay_5s_button.active = true; + else + page_delay_3s_button.active = true; + } + + private void set_dpi_combo (Gtk.ComboBox combo, int default_dpi, int current_dpi) + { + var renderer = new Gtk.CellRendererText (); + combo.pack_start (renderer, true); + combo.add_attribute (renderer, "text", 1); + + var model = combo.model as Gtk.ListStore; + int[] scan_resolutions = {75, 150, 300, 600, 1200, 2400}; + foreach (var dpi in scan_resolutions) + { + string label; + if (dpi == default_dpi) + /* Preferences dialog: Label for default resolution in resolution list */ + label = _("%d dpi (default)").printf (dpi); + else if (dpi == 75) + /* Preferences dialog: Label for minimum resolution in resolution list */ + label = _("%d dpi (draft)").printf (dpi); + else if (dpi == 1200) + /* Preferences dialog: Label for maximum resolution in resolution list */ + label = _("%d dpi (high resolution)").printf (dpi); + else + /* Preferences dialog: Label for resolution value in resolution list (dpi = dots per inch) */ + label = _("%d dpi").printf (dpi); + + Gtk.TreeIter iter; + model.append (out iter); + model.set (iter, 0, dpi, 1, label, -1); + + if (dpi == current_dpi) + combo.set_active_iter (iter); + } + } + + [GtkCallback] + private void device_combo_changed_cb (Gtk.Widget widget) + { + if (setting_devices) + return; + user_selected_device = true; + if (get_selected_device () != null) + settings.set_string ("selected-device", get_selected_device ()); + } +} + +private class PageIcon : Gtk.DrawingArea +{ + private string text; + private double r; + private double g; + private double b; + private const int MINIMUM_WIDTH = 20; + + public PageIcon (string text, double r = 1.0, double g = 1.0, double b = 1.0) + { + this.text = text; + this.r = r; + this.g = g; + this.b = b; + } + + public override void get_preferred_width (out int minimum_width, out int natural_width) + { + minimum_width = natural_width = MINIMUM_WIDTH; + } + + public override void get_preferred_height (out int minimum_height, out int natural_height) + { + minimum_height = natural_height = (int) Math.round (MINIMUM_WIDTH * Math.SQRT2); + } + + public override void get_preferred_height_for_width (int width, out int minimum_height, out int natural_height) + { + minimum_height = natural_height = (int) (width * Math.SQRT2); + } + + public override void get_preferred_width_for_height (int height, out int minimum_width, out int natural_width) + { + minimum_width = natural_width = (int) (height / Math.SQRT2); + } + + public override bool draw (Cairo.Context c) + { + var w = get_allocated_width (); + var h = get_allocated_height (); + if (w * Math.SQRT2 > h) + w = (int) Math.round (h / Math.SQRT2); + else + h = (int) Math.round (w * Math.SQRT2); + + c.translate ((get_allocated_width () - w) / 2, (get_allocated_height () - h) / 2); + + c.rectangle (0.5, 0.5, w - 1, h - 1); + + c.set_source_rgb (r, g, b); + c.fill_preserve (); + + c.set_line_width (1.0); + c.set_source_rgb (0.0, 0.0, 0.0); + c.stroke (); + + Cairo.TextExtents extents; + c.text_extents (text, out extents); + c.translate ((w - extents.width) * 0.5 - 0.5, (h + extents.height) * 0.5 - 0.5); + c.show_text (text); + + return true; + } +} diff --git a/src/screensaver.vala b/src/screensaver.vala new file mode 100644 index 0000000..ef2dfb8 --- /dev/null +++ b/src/screensaver.vala @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2017 Stéphane Fillion + * Authors: Stéphane Fillion + * + * 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. + */ + +[DBus (name = "org.freedesktop.ScreenSaver")] +public interface FreedesktopScreensaver : Object +{ + public static FreedesktopScreensaver get_proxy () throws IOError + { + return Bus.get_proxy_sync (BusType.SESSION, "org.freedesktop.ScreenSaver", "/org/freedesktop/ScreenSaver"); + } + + [DBus (name = "Inhibit")] + public abstract uint32 inhibit (string application_name, string reason_for_inhibit) throws IOError; + + [DBus (name = "UnInhibit")] + public abstract void uninhibit (uint32 cookie) throws IOError; +} diff --git a/src/simple-scan.gresource.xml b/src/simple-scan.gresource.xml index b6fe6a6..a62619e 100644 --- a/src/simple-scan.gresource.xml +++ b/src/simple-scan.gresource.xml @@ -1,6 +1,11 @@ - simple-scan.ui + app-window.ui + preferences-dialog.ui + authorize-dialog.ui + + + help-overlay.ui diff --git a/src/simple-scan.ui b/src/simple-scan.ui deleted file mode 100644 index 6e1d15a..0000000 --- a/src/simple-scan.ui +++ /dev/null @@ -1,1454 +0,0 @@ - - - - - False - 12 - False - True - normal - True - - - True - False - vertical - 12 - - - True - False - end - - - _Authorize - True - True - True - True - - - False - False - 0 - - - - - False - True - end - 0 - - - - - True - False - 5 - vertical - 12 - - - True - False - To connect to ? you need to authorize - - - True - True - 0 - - - - - True - False - 6 - 6 - - - True - True - True - - - - 1 - 0 - 1 - 1 - - - - - True - True - True - False - - - - 1 - 1 - 1 - 1 - - - - - True - False - _Username for resource: - True - username_entry - - - 0 - 0 - 1 - 1 - - - - - True - False - _Password: - True - password_entry - - - 0 - 1 - 1 - 1 - - - - - False - True - 1 - - - - - False - True - 1 - - - - - - authorize_button - - - - -100 - 100 - 1 - 10 - - - -100 - 100 - 1 - 10 - - - - - - - - - - - True - False - mail-send - - - True - False - gtk-help - - - - - - - - - - - 3 - Front and Back - - - 1 - Front - - - 2 - Back - - - - - - - - - - - - - - - - - - - - - - - 100 - 1 - 10 - - - 0 - 10000 - 100 - 1000 - - - - - - - - - - - - False - 7 - Preferences - False - scanner - normal - - - - - True - False - vertical - 2 - - - True - False - end - - - gtk-close - True - True - True - True - - - False - False - 0 - - - - - False - True - end - 0 - - - - - True - False - 5 - 6 - 6 - - - True - False - 0 - Scan S_ource: - True - device_combo - - - 0 - 0 - 1 - 1 - - - - - True - False - True - device_model - - - - 1 - 0 - 1 - 1 - - - - - True - False - 0 - _Text Resolution: - True - text_dpi_combo - - - 0 - 1 - 1 - 1 - - - - - True - False - 0 - _Photo Resolution: - True - photo_dpi_combo - - - 0 - 2 - 1 - 1 - - - - - True - False - True - text_dpi_model - - - 1 - 1 - 1 - 1 - - - - - True - False - True - photo_dpi_model - - - 1 - 2 - 1 - 1 - - - - - True - False - 0 - Scan Side: - True - photo_dpi_combo - - - 0 - 3 - 1 - 1 - - - - - True - False - True - page_side_model - - - 1 - 3 - 1 - 1 - - - - - True - False - 0 - Page Size: - True - photo_dpi_combo - - - 0 - 4 - 1 - 1 - - - - - True - False - True - paper_size_model - - - 1 - 4 - 1 - 1 - - - - - True - False - 0 - Brightness: - True - - - 0 - 5 - 1 - 1 - - - - - True - True - True - brightness_adjustment - False - - - 1 - 5 - 1 - 1 - - - - - True - False - 0 - Contrast: - True - contrast_scale - - - 0 - 6 - 1 - 1 - - - - - True - True - True - contrast_adjustment - False - - - 1 - 6 - 1 - 1 - - - - - True - False - 0 - Quality: - True - quality_scale - - - 0 - 7 - 1 - 1 - - - - - True - True - True - quality_adjustment - False - - - 1 - 7 - 1 - 1 - - - - - True - False - 0 - Delay between pages: - True - page_delay_scale - - - 0 - 8 - 1 - 1 - - - - - True - True - True - page_delay_adjustment - True - - - 1 - 8 - 1 - 1 - - - - - False - True - 1 - - - - - - preferences_close_button - - - - True - False - - - True - False - Single _Page - True - - - - - - True - False - All Pages From _Feeder - True - - - - - - True - False - _Multiple Pages From Flatbed - True - - - - - - True - False - - - - - True - False - Text - True - True - - - - - - True - False - Photo - True - True - True - text_button_menuitem - - - - - - True - False - - - True - False - Single _Page - True - - - - - - True - False - All Pages From _Feeder - True - - - - - - True - False - _Multiple Pages From Flatbed - True - - - - - - True - False - - - - - True - False - Text - True - True - - - - - - True - False - Photo - True - True - True - text_button_hb_menuitem - - - - - diff --git a/src/simple-scan.vala b/src/simple-scan.vala index 2ab83f0..841e702 100644 --- a/src/simple-scan.vala +++ b/src/simple-scan.vala @@ -23,7 +23,7 @@ public class SimpleScan : Gtk.Application /* Help string for command line --debug flag */ N_("Print debugging messages"), null}, { "fix-pdf", 0, 0, OptionArg.STRING, ref fix_pdf_filename, - N_("Fix PDF files generated with older versions of Simple Scan"), "FILENAME..."}, + N_("Fix PDF files generated with older versions of Simple Scan"), "FILENAME…"}, { null } }; private static Timer log_timer; @@ -32,12 +32,16 @@ public class SimpleScan : Gtk.Application private ScanDevice? default_device = null; private bool have_devices = false; private GUsb.Context usb_context; - private UserInterface ui; + private AppWindow app; private Scanner scanner; private Book book; public SimpleScan (ScanDevice? device = null) { + /* The inhibit () method use this */ + Object (application_id: "org.gnome.SimpleScan"); + register_session = true; + default_device = device; } @@ -45,11 +49,10 @@ public class SimpleScan : Gtk.Application { base.startup (); - ui = new UserInterface (); - book = ui.book; - ui.start_scan.connect (scan_cb); - ui.stop_scan.connect (cancel_cb); - ui.email.connect (email_cb); + app = new AppWindow (); + book = app.book; + app.start_scan.connect (scan_cb); + app.stop_scan.connect (cancel_cb); scanner = Scanner.get_instance (); scanner.update_devices.connect (update_scan_devices_cb); @@ -78,15 +81,15 @@ public class SimpleScan : Gtk.Application List device_list = null; device_list.append (default_device); - ui.set_scan_devices (device_list); - ui.selected_device = default_device.name; + app.set_scan_devices (device_list); + app.selected_device = default_device.name; } } public override void activate () { base.activate (); - ui.start (); + app.start (); scanner.start (); } @@ -94,7 +97,7 @@ public class SimpleScan : Gtk.Application { base.shutdown (); book = null; - ui = null; + app = null; usb_context = null; scanner.free (); } @@ -127,7 +130,7 @@ public class SimpleScan : Gtk.Application if (!have_devices) missing_driver = suggest_driver (); - ui.set_scan_devices (devices_copy, missing_driver); + app.set_scan_devices (devices_copy, missing_driver); } /* Taken from /usr/local/Brother/sane/Brsane.ini from brscan driver */ @@ -212,7 +215,7 @@ public class SimpleScan : Gtk.Application private void authorize_cb (Scanner scanner, string resource) { string username, password; - ui.authorize (resource, out username, out password); + app.authorize (resource, out username, out password); scanner.authorize (username, password); } @@ -222,7 +225,7 @@ public class SimpleScan : Gtk.Application var page = book.get_page (-1); if (page != null && !page.has_data) { - ui.selected_page = page; + app.selected_page = page; page.start (); return page; } @@ -262,7 +265,7 @@ public class SimpleScan : Gtk.Application page.set_custom_crop (cw, ch); page.move_crop (cx, cy); } - ui.selected_page = page; + app.selected_page = page; page.start (); return page; @@ -385,118 +388,80 @@ public class SimpleScan : Gtk.Application remove_empty_page (); if (error_code != Sane.Status.CANCELLED) { - ui.show_error (/* Title of error dialog when scan failed */ - _("Failed to scan"), - error_string, - have_devices); + app.show_error_dialog (/* Title of error dialog when scan failed */ + _("Failed to scan"), + error_string); } } - private void scanner_scanning_changed_cb (Scanner scanner) - { - ui.scanning = scanner.is_scanning (); - } - - private void scan_cb (UserInterface ui, string? device, ScanOptions options) - { - debug ("Requesting scan at %d dpi from device '%s'", options.dpi, device); + private uint inhibit_cookie; + private FreedesktopScreensaver? fdss; - if (!scanner.is_scanning ()) - append_page (); - - scanner.scan (device, options); - } - - private void cancel_cb (UserInterface ui) - { - scanner.cancel (); - } - - private string? get_temporary_filename (string prefix, string extension) + private void scanner_scanning_changed_cb (Scanner scanner) { - /* NOTE: I'm not sure if this is a 100% safe strategy to use g_file_open_tmp(), close and - * use the filename but it appears to work in practise */ + var is_scanning = scanner.is_scanning (); - var filename = "%sXXXXXX.%s".printf (prefix, extension); - string path; - try - { - var fd = FileUtils.open_tmp (filename, out path); - Posix.close (fd); - } - catch (Error e) + if (is_scanning) { - warning ("Error saving email attachment: %s", e.message); - return null; - } + /* Attempt to inhibit the screensaver when scanning */ + var reason = _("Scan in progress"); - return path; - } + /* This should work on Gnome, Budgie, Cinnamon, Mate, Unity, ... + * but will not work on KDE, LXDE, XFCE, ... */ + inhibit_cookie = inhibit (app, Gtk.ApplicationInhibitFlags.IDLE, reason); - private void email_cb (UserInterface ui, string profile, int quality) - { - var saved = false; - var command_line = "xdg-email"; - - /* Save text files as PDFs */ - if (profile == "text") - { - /* Open a temporary file */ - var path = get_temporary_filename ("scan", "pdf"); - if (path != null) + if (!is_inhibited (Gtk.ApplicationInhibitFlags.IDLE)) { - var file = File.new_for_path (path); - ui.show_progress_dialog (); + /* If the previous method didn't work, try the one + * provided by Freedesktop. It should work with KDE, + * LXDE, XFCE, and maybe others as well. */ try { - book.save ("pdf", quality, file); + if ((fdss = FreedesktopScreensaver.get_proxy ()) != null) + { + inhibit_cookie = fdss.inhibit ("Simple-Scan", reason); + } } - catch (Error e) - { - ui.hide_progress_dialog (); - warning ("Unable to save email file: %s", e.message); - return; - } - command_line += " --attach %s".printf (path); + catch (IOError error) {} } } else { - for (var i = 0; i < book.n_pages; i++) + /* When finished scanning, uninhibit if inhibit was working */ + if (inhibit_cookie != 0) { - var path = get_temporary_filename ("scan", "jpg"); - if (path == null) - { - saved = false; - break; - } - - var file = File.new_for_path (path); - try - { - book.get_page (i).save ("jpeg", quality, file); - } - catch (Error e) + if (fdss == null) + uninhibit (inhibit_cookie); + else { - warning ("Unable to save email file: %s", e.message); - return; + try + { + fdss.uninhibit (inhibit_cookie); + } + catch (IOError error) {} + fdss = null; } - command_line += " --attach %s".printf (path); - if (!saved) - break; + inhibit_cookie = 0; } } - debug ("Launching email client: %s", command_line); - try - { - Process.spawn_command_line_async (command_line); - } - catch (Error e) - { - warning ("Unable to start email: %s", e.message); - } + app.scanning = is_scanning; + } + + private void scan_cb (AppWindow ui, string? device, ScanOptions options) + { + debug ("Requesting scan at %d dpi from device '%s'", options.dpi, device); + + if (!scanner.is_scanning ()) + append_page (); + + scanner.scan (device, options); + } + + private void cancel_cb (AppWindow ui) + { + scanner.cancel (); } private static void log_cb (string? log_domain, LogLevelFlags log_level, string message) @@ -616,7 +581,7 @@ public class SimpleScan : Gtk.Application Intl.textdomain (GETTEXT_PACKAGE); var c = new OptionContext (/* Arguments and description for --help text */ - _("[DEVICE...] - Scanning utility")); + _("[DEVICE…] — Scanning utility")); c.add_main_entries (options, GETTEXT_PACKAGE); c.add_group (Gtk.get_option_group (true)); try @@ -627,7 +592,7 @@ public class SimpleScan : Gtk.Application { stderr.printf ("%s\n", e.message); stderr.printf (/* Text printed out when an unknown command-line argument provided */ - _("Run '%s --help' to see a full list of available command line options."), args[0]); + _("Run “%s --help” to see a full list of available command line options."), args[0]); stderr.printf ("\n"); return Posix.EXIT_FAILURE; } diff --git a/src/ui.vala b/src/ui.vala deleted file mode 100644 index 554b161..0000000 --- a/src/ui.vala +++ /dev/null @@ -1,2398 +0,0 @@ -/* - * Copyright (C) 2009-2015 Canonical Ltd. - * Author: Robert Ancell , - * Eduard Gotwig - * - * 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. - */ - -[GtkTemplate (ui = "/org/gnome/SimpleScan/simple-scan.ui")] -public class UserInterface : Gtk.ApplicationWindow -{ - private const int DEFAULT_TEXT_DPI = 150; - private const int DEFAULT_PHOTO_DPI = 300; - - 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; - - [GtkChild] - private Gtk.MenuBar menubar; - [GtkChild] - private Gtk.Toolbar toolbar; - [GtkChild] - private Gtk.Menu page_menu; - [GtkChild] - private Gtk.Box main_vbox; - private Gtk.InfoBar info_bar; - private Gtk.Image info_bar_image; - private Gtk.Label info_bar_label; - private Gtk.Button info_bar_close_button; - private Gtk.Button info_bar_change_scanner_button; - private Gtk.Button info_bar_install_button; - [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.ToggleButton crop_button; - [GtkChild] - private Gtk.ToggleToolButton crop_toolbutton; - [GtkChild] - private Gtk.Button stop_button; - [GtkChild] - private Gtk.Button scan_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.Dialog authorize_dialog; - [GtkChild] - private Gtk.Label authorize_label; - [GtkChild] - private Gtk.Entry username_entry; - [GtkChild] - private Gtk.Entry password_entry; - - [GtkChild] - private Gtk.Dialog preferences_dialog; - [GtkChild] - private Gtk.ComboBox device_combo; - [GtkChild] - private Gtk.ComboBox text_dpi_combo; - [GtkChild] - private Gtk.ComboBox photo_dpi_combo; - [GtkChild] - private Gtk.ComboBox page_side_combo; - [GtkChild] - private Gtk.ComboBox paper_size_combo; - [GtkChild] - private Gtk.Scale brightness_scale; - [GtkChild] - private Gtk.Scale contrast_scale; - [GtkChild] - private Gtk.Scale quality_scale; - [GtkChild] - private Gtk.Scale page_delay_scale; - [GtkChild] - private Gtk.ListStore device_model; - [GtkChild] - private Gtk.ListStore text_dpi_model; - [GtkChild] - private Gtk.ListStore photo_dpi_model; - [GtkChild] - private Gtk.ListStore page_side_model; - [GtkChild] - private Gtk.ListStore paper_size_model; - [GtkChild] - private Gtk.Adjustment brightness_adjustment; - [GtkChild] - private Gtk.Adjustment contrast_adjustment; - [GtkChild] - private Gtk.Adjustment quality_adjustment; - [GtkChild] - private Gtk.Adjustment page_delay_adjustment; - private bool setting_devices; - private string? missing_driver = null; - private bool user_selected_device; - - private Gtk.FileChooserDialog? save_dialog; - private ProgressBarDialog progress_dialog; - - private bool have_error; - private string error_title; - private string error_text; - private bool error_change_scanner_hint; - - 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 int default_page_width; - private int default_page_height; - private int default_page_dpi; - private ScanDirection default_page_scan_direction; - - private string document_hint = "photo"; - - private bool scanning_ = false; - public bool scanning - { - get { return scanning_; } - set - { - scanning_ = value; - page_delete_menuitem.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 (int) brightness_adjustment.value; } - set { brightness_adjustment.value = value; } - } - - public int contrast - { - get { return (int) contrast_adjustment.value; } - set { contrast_adjustment.value = value; } - } - - public int quality - { - get { return (int) quality_adjustment.value; } - set { quality_adjustment.value = value; } - } - - public int page_delay - { - get { return (int) page_delay_adjustment.value; } - set { page_delay_adjustment.value = value; } - } - - public string? selected_device - { - owned get - { - Gtk.TreeIter iter; - - if (device_combo.get_active_iter (out iter)) - { - string device; - device_model.get (iter, 0, out device, -1); - return device; - } - - return null; - } - - set - { - Gtk.TreeIter iter; - if (!find_scan_device (value, out iter)) - return; - - device_combo.set_active_iter (iter); - user_selected_device = true; - } - } - - public signal void start_scan (string? device, ScanOptions options); - public signal void stop_scan (); - public signal void email (string profile, int quality); - - public UserInterface () - { - 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 - { - book_view.selected_page = book.get_page (0); - book_needs_saving = true; - book_changed_cb (book); - } - } - - ~UserInterface () - { - book.page_added.disconnect (page_added_cb); - book.reordered.disconnect (reordered_cb); - book.page_removed.disconnect (page_removed_cb); - } - - private bool find_scan_device (string device, out Gtk.TreeIter iter) - { - bool have_iter = false; - - if (device_model.get_iter_first (out iter)) - { - do - { - string d; - device_model.get (iter, 0, out d, -1); - if (d == device) - have_iter = true; - } while (!have_iter && device_model.iter_next (ref iter)); - } - - return have_iter; - } - - private 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); - - username_entry.text = ""; - password_entry.text = ""; - authorize_label.set_text (description); - - authorize_dialog.visible = true; - authorize_dialog.run (); - authorize_dialog.visible = false; - - username = username_entry.text; - password = password_entry.text; - } - - [GtkCallback] - private void device_combo_changed_cb (Gtk.Widget widget) - { - if (setting_devices) - return; - user_selected_device = true; - if (selected_device != null) - settings.set_string ("selected-device", selected_device); - } - - private void update_info_bar () - { - Gtk.MessageType type; - string title, text, image_id; - bool show_close_button = false; - bool show_install_button = false; - bool show_change_scanner_button = false; - - if (have_error) - { - type = Gtk.MessageType.ERROR; - image_id = "dialog-error"; - title = error_title; - text = error_text; - show_close_button = true; - show_change_scanner_button = error_change_scanner_hint; - } - else if (device_model.iter_n_children (null) == 0) - { - type = Gtk.MessageType.WARNING; - image_id = "dialog-warning"; - if (missing_driver == null) - { - /* Warning displayed when no scanners are detected */ - title = _("No scanners detected"); - /* Hint to user on why there are no scanners detected */ - text = _("Please check your scanner is connected and powered on"); - } - else - { - /* Warning displayed when no drivers are installed but a compatible scanner is detected */ - title = _("Additional software needed"); - /* Instructions to install driver software */ - text = _("You need to install driver software for your scanner."); - show_install_button = true; - } - } - else - { - info_bar.visible = false; - return; - } - - info_bar.message_type = type; - info_bar_image.set_from_icon_name (image_id, Gtk.IconSize.DIALOG); - var message = "%s\n\n%s".printf (title, text); - info_bar_label.set_markup (message); - info_bar_close_button.visible = show_close_button; - info_bar_change_scanner_button.visible = show_change_scanner_button; - info_bar_install_button.visible = show_install_button; - info_bar.visible = true; - } - - public void set_scan_devices (List devices, string? missing_driver = null) - { - bool have_selection = false; - int index; - Gtk.TreeIter iter; - - setting_devices = true; - - this.missing_driver = missing_driver; - - /* If the user hasn't chosen a scanner choose the best available one */ - if (user_selected_device) - have_selection = device_combo.active >= 0; - - /* Add new devices */ - index = 0; - foreach (var device in devices) - { - int n_delete = -1; - - /* Find if already exists */ - if (device_model.iter_nth_child (out iter, null, index)) - { - int i = 0; - do - { - string name; - bool matched; - - device_model.get (iter, 0, out name, -1); - matched = name == device.name; - - if (matched) - { - n_delete = i; - break; - } - i++; - } while (device_model.iter_next (ref iter)); - } - - /* If exists, remove elements up to this one */ - if (n_delete >= 0) - { - int i; - - /* Update label */ - device_model.set (iter, 1, device.label, -1); - - for (i = 0; i < n_delete; i++) - { - device_model.iter_nth_child (out iter, null, index); - device_model.remove (iter); - } - } - else - { - device_model.insert (out iter, index); - device_model.set (iter, 0, device.name, 1, device.label, -1); - } - index++; - } - - /* Remove any remaining devices */ - while (device_model.iter_nth_child (out iter, null, index)) - device_model.remove (iter); - - /* Select the previously selected device or the first available device */ - if (!have_selection) - { - var device = settings.get_string ("selected-device"); - if (device != null && find_scan_device (device, out iter)) - device_combo.set_active_iter (iter); - else - device_combo.set_active (0); - } - - setting_devices = false; - - update_info_bar (); - } - - 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 image files */ - _("Image Files")); - filter.add_pixbuf_formats (); - 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); - - 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) */ - 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); - - 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); - }); - box.pack_start (file_type_combo, false, false, 0); - - 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 (); - var format = uri_to_format (uri); - if (format == "jpeg" || format == "png") - { - for (var j = 0; j < book.n_pages; j++) - files.append (book.make_indexed_file (uri, j)); - } - 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 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"; - else - return "jpeg"; - } - - private bool save_document () - { - 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); - - show_progress_dialog (); - try - { - book.save (format, quality, file); - } - catch (Error e) - { - hide_progress_dialog (); - warning ("Error saving file: %s", e.message); - show_error (/* Title of error dialog when save failed */ - _("Failed to save file"), - e.message, - false); - return false; - } - - book_needs_saving = false; - book_uri = uri; - return true; - } - - private bool prompt_to_save (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 (save_document ()) - return true; - else - return false; - case Gtk.ResponseType.NO: - return true; - default: - return false; - } - } - - private void clear_document () - { - book_view.default_page = new Page (default_page_width, - default_page_height, - default_page_dpi, - default_page_scan_direction); - 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; - } - - private void new_document () - { - if (!prompt_to_save (/* 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"))) - return; - - if (scanning) - stop_scan (); - clear_document (); - } - - [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 void set_page_side (ScanType page_side) - { - Gtk.TreeIter iter; - - if (page_side_model.get_iter_first (out iter)) - { - do - { - int s; - page_side_model.get (iter, 0, out s, -1); - if (s == page_side) - { - page_side_combo.set_active_iter (iter); - return; - } - } while (page_side_model.iter_next (ref iter)); - } - } - - private void set_paper_size (int width, int height) - { - Gtk.TreeIter iter; - bool have_iter; - - for (have_iter = paper_size_model.get_iter_first (out iter); - have_iter; - have_iter = paper_size_model.iter_next (ref iter)) - { - int w, h; - paper_size_model.get (iter, 0, out w, 1, out h, -1); - if (w == width && h == height) - break; - } - - if (!have_iter) - have_iter = paper_size_model.get_iter_first (out iter); - if (have_iter) - paper_size_combo.set_active_iter (iter); - } - - private int get_text_dpi () - { - Gtk.TreeIter iter; - int dpi = DEFAULT_TEXT_DPI; - - if (text_dpi_combo.get_active_iter (out iter)) - text_dpi_model.get (iter, 0, out dpi, -1); - - return dpi; - } - - private int get_photo_dpi () - { - Gtk.TreeIter iter; - int dpi = DEFAULT_PHOTO_DPI; - - if (photo_dpi_combo.get_active_iter (out iter)) - photo_dpi_model.get (iter, 0, out dpi, -1); - - return dpi; - } - - private ScanType get_page_side () - { - Gtk.TreeIter iter; - int page_side = ScanType.ADF_BOTH; - - if (page_side_combo.get_active_iter (out iter)) - page_side_model.get (iter, 0, out page_side, -1); - - return (ScanType) page_side; - } - - private bool get_paper_size (out int width, out int height) - { - Gtk.TreeIter iter; - - width = height = 0; - if (paper_size_combo.get_active_iter (out iter)) - { - paper_size_model.get (iter, 0, ref width, 1, ref height, -1); - return true; - } - - return false; - } - - private ScanOptions make_scan_options () - { - var options = new ScanOptions (); - if (document_hint == "text") - { - options.scan_mode = ScanMode.GRAY; - options.dpi = get_text_dpi (); - options.depth = 2; - } - else - { - options.scan_mode = ScanMode.COLOR; - options.dpi = get_photo_dpi (); - options.depth = 8; - } - 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; - 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 = 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 (); - } - - [GtkCallback] - private bool preferences_dialog_delete_event_cb (Gtk.Widget widget, Gdk.EventAny event) - { - return true; - } - - [GtkCallback] - private void preferences_dialog_response_cb (Gtk.Widget widget, int response_id) - { - preferences_dialog.visible = false; - } - - 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; - crop_toolbutton.active = page.has_crop; - - updating_page_menu = false; - } - - private void show_page_cb (BookView view, Page page) - { - var path = get_temporary_filename ("scanned-page", "png"); - if (path == null) - return; - var file = File.new_for_path (path); - - try - { - page.save ("png", quality, 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 crop_button_toggled_cb (Gtk.ToggleButton widget) - { - if (updating_page_menu) - return; - - if (widget.active) - custom_crop_menuitem.active = true; - else - no_crop_menuitem.active = true; - } - - [GtkCallback] - private void crop_toolbutton_toggled_cb (Gtk.ToggleToolButton widget) - { - if (updating_page_menu) - return; - - if (widget.active) - custom_crop_menuitem.active = true; - else - no_crop_menuitem.active = true; - } - - [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 (); - } - - public void save_document_activate_cb () - { - save_document (); - } - - [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_hint, quality); - } - - public void email_document_activate_cb () - { - email (document_hint, quality); - } - - 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 " }; - - /* 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 ."); - - /* 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 bool on_quit () - { - if (!prompt_to_save (/* Text in dialog warning when a document is about to be lost */ - _("Save document before quitting?"), - /* Button in dialog to quit and discard unsaved document */ - _("Quit without Saving"))) - return false; - - destroy (); - - if (save_state_timeout != 0) - save_state (true); - - autosave_manager.cleanup (); - - return true; - } - - [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 info_bar_response_cb (Gtk.InfoBar widget, int response_id) - { - switch (response_id) - { - /* Change scanner */ - case 1: - device_combo.grab_focus (); - preferences_dialog.present (); - break; - /* Install drivers */ - case 2: - install_drivers (); - break; - default: - have_error = false; - error_title = null; - error_text = null; - update_info_bar (); - break; - } - } - - 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 Brother website."); - 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 Samsung website."); - 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 Epson website."); - 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) */ - _("You need to install the %s package(s).").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) - { - return !on_quit (); - } - - private void page_size_changed_cb (Page page) - { - default_page_width = page.width; - default_page_height = page.height; - default_page_dpi = page.dpi; - save_state (); - } - - private void page_scan_direction_changed_cb (Page page) - { - default_page_scan_direction = page.scan_direction; - save_state (); - } - - private void page_added_cb (Book book, Page page) - { - page_size_changed_cb (page); - default_page_scan_direction = page.scan_direction; - page.size_changed.connect (page_size_changed_cb); - page.scan_direction_changed.connect (page_scan_direction_changed_cb); - - update_page_menu (); - } - - private void reordered_cb (Book book) - { - update_page_menu (); - } - - private void page_removed_cb (Book book, Page page) - { - page.size_changed.disconnect (page_size_changed_cb); - page.scan_direction_changed.disconnect (page_scan_direction_changed_cb); - - update_page_menu (); - } - - private void set_dpi_combo (Gtk.ComboBox combo, int default_dpi, int current_dpi) - { - var renderer = new Gtk.CellRendererText (); - combo.pack_start (renderer, true); - combo.add_attribute (renderer, "text", 1); - - var model = combo.model as Gtk.ListStore; - int[] scan_resolutions = {75, 150, 300, 600, 1200, 2400}; - foreach (var dpi in scan_resolutions) - { - string label; - if (dpi == default_dpi) - /* Preferences dialog: Label for default resolution in resolution list */ - label = _("%d dpi (default)").printf (dpi); - else if (dpi == 75) - /* Preferences dialog: Label for minimum resolution in resolution list */ - label = _("%d dpi (draft)").printf (dpi); - else if (dpi == 1200) - /* Preferences dialog: Label for maximum resolution in resolution list */ - label = _("%d dpi (high resolution)").printf (dpi); - else - /* Preferences dialog: Label for resolution value in resolution list (dpi = dots per inch) */ - label = _("%d dpi").printf (dpi); - - Gtk.TreeIter iter; - model.append (out iter); - model.set (iter, 0, dpi, 1, label, -1); - - if (dpi == current_dpi) - combo.set_active_iter (iter); - } - } - - 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 () - { - 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 (is_traditional_desktop ()) - { - set_titlebar (null); - menubar.visible = true; - toolbar.visible = true; - } - else - { - app.add_action_entries (action_entries, this); - - var appmenu = new Menu (); - var section = new Menu (); - appmenu.append_section (null, section); - section.append (_("New Document"), "app.new_document"); - - section = new Menu (); - appmenu.append_section (null, section); - var menu = new Menu (); - section.append_submenu (_("Document"), menu); - menu.append (_("Reorder Pages"), "app.reorder"); - menu.append (_("Save"), "app.save"); - menu.append (_("Email..."), "app.email"); - menu.append (_("Print..."), "app.print"); - - section = new Menu (); - appmenu.append_section (null, section); - section.append (_("Preferences"), "app.preferences"); - - section = new Menu (); - appmenu.append_section (null, section); - section.append (_("Help"), "app.help"); - section.append (_("About"), "app.about"); - section.append (_("Quit"), "app.quit"); - - app.app_menu = appmenu; - - app.add_accelerator ("N", "app.new_document", null); - app.add_accelerator ("S", "app.save", null); - app.add_accelerator ("E", "app.email", null); - app.add_accelerator ("P", "app.print", null); - app.add_accelerator ("F1", "app.help", null); - app.add_accelerator ("Q", "app.quit", null); - } - app.add_window (this); - - /* Add InfoBar (not supported in Glade) */ - info_bar = new Gtk.InfoBar (); - info_bar.response.connect (info_bar_response_cb); - main_vbox.pack_start (info_bar, false, true, 0); - var hbox = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12); - var content_area = info_bar.get_content_area () as Gtk.Container; - content_area.add (hbox); - hbox.visible = true; - - info_bar_image = new Gtk.Image.from_icon_name ("dialog-warning", Gtk.IconSize.DIALOG); - hbox.pack_start (info_bar_image, false, true, 0); - info_bar_image.visible = true; - - info_bar_label = new Gtk.Label (null); - info_bar_label.set_alignment (0.0f, 0.5f); - hbox.pack_start (info_bar_label, true, true, 0); - info_bar_label.visible = true; - - info_bar_close_button = info_bar.add_button (_("_Close"), Gtk.ResponseType.CLOSE) as Gtk.Button; - info_bar_change_scanner_button = info_bar.add_button (/* Button in error infobar to open preferences dialog and change scanner */ - _("Change _Scanner"), 1) as Gtk.Button; - info_bar_install_button = info_bar.add_button (/* Button in error infobar to prompt user to install drivers */ - _("_Install Drivers"), 2) as Gtk.Button; - - Gtk.TreeIter iter; - paper_size_model.append (out iter); - paper_size_model.set (iter, 0, 0, 1, 0, 2, - /* Combo box value for automatic paper size */ - _("Automatic"), -1); - paper_size_model.append (out iter); - paper_size_model.set (iter, 0, 1050, 1, 1480, 2, "A6", -1); - paper_size_model.append (out iter); - paper_size_model.set (iter, 0, 1480, 1, 2100, 2, "A5", -1); - paper_size_model.append (out iter); - paper_size_model.set (iter, 0, 2100, 1, 2970, 2, "A4", -1); - paper_size_model.append (out iter); - paper_size_model.set (iter, 0, 2159, 1, 2794, 2, "Letter", -1); - paper_size_model.append (out iter); - paper_size_model.set (iter, 0, 2159, 1, 3556, 2, "Legal", -1); - paper_size_model.append (out iter); - paper_size_model.set (iter, 0, 1016, 1, 1524, 2, "4×6", -1); - - var dpi = settings.get_int ("text-dpi"); - if (dpi <= 0) - dpi = DEFAULT_TEXT_DPI; - set_dpi_combo (text_dpi_combo, DEFAULT_TEXT_DPI, dpi); - text_dpi_combo.changed.connect (() => { settings.set_int ("text-dpi", get_text_dpi ()); }); - dpi = settings.get_int ("photo-dpi"); - if (dpi <= 0) - dpi = DEFAULT_PHOTO_DPI; - set_dpi_combo (photo_dpi_combo, DEFAULT_PHOTO_DPI, dpi); - photo_dpi_combo.changed.connect (() => { settings.set_int ("photo-dpi", get_photo_dpi ()); }); - - var renderer = new Gtk.CellRendererText (); - device_combo.pack_start (renderer, true); - device_combo.add_attribute (renderer, "text", 1); - - renderer = new Gtk.CellRendererText (); - page_side_combo.pack_start (renderer, true); - page_side_combo.add_attribute (renderer, "text", 1); - set_page_side ((ScanType) settings.get_enum ("page-side")); - page_side_combo.changed.connect (() => { settings.set_enum ("page-side", get_page_side ()); }); - - renderer = new Gtk.CellRendererText (); - paper_size_combo.pack_start (renderer, true); - paper_size_combo.add_attribute (renderer, "text", 2); - var paper_width = settings.get_int ("paper-width"); - var paper_height = settings.get_int ("paper-height"); - set_paper_size (paper_width, paper_height); - paper_size_combo.changed.connect (() => - { - int w, h; - get_paper_size (out w, out h); - settings.set_int ("paper-width", w); - settings.set_int ("paper-height", h); - }); - - var lower = brightness_adjustment.lower; - var darker_label = "%s".printf (_("Darker")); - var upper = brightness_adjustment.upper; - var lighter_label = "%s".printf (_("Lighter")); - brightness_scale.add_mark (lower, Gtk.PositionType.BOTTOM, darker_label); - brightness_scale.add_mark (0, Gtk.PositionType.BOTTOM, null); - brightness_scale.add_mark (upper, Gtk.PositionType.BOTTOM, lighter_label); - brightness = settings.get_int ("brightness"); - brightness_adjustment.value_changed.connect (() => { settings.set_int ("brightness", brightness); }); - - lower = contrast_adjustment.lower; - var less_label = "%s".printf (_("Less")); - upper = contrast_adjustment.upper; - var more_label = "%s".printf (_("More")); - contrast_scale.add_mark (lower, Gtk.PositionType.BOTTOM, less_label); - contrast_scale.add_mark (0, Gtk.PositionType.BOTTOM, null); - contrast_scale.add_mark (upper, Gtk.PositionType.BOTTOM, more_label); - contrast = settings.get_int ("contrast"); - contrast_adjustment.value_changed.connect (() => { settings.set_int ("contrast", contrast); }); - - lower = quality_adjustment.lower; - var minimum_label = "%s".printf (_("Minimum")); - upper = quality_adjustment.upper; - var maximum_label = "%s".printf (_("Maximum")); - quality_scale.add_mark (lower, Gtk.PositionType.BOTTOM, minimum_label); - quality_scale.add_mark (75, Gtk.PositionType.BOTTOM, null); - quality_scale.add_mark (upper, Gtk.PositionType.BOTTOM, maximum_label); - quality = settings.get_int ("jpeg-quality"); - quality_adjustment.value_changed.connect (() => { settings.set_int ("jpeg-quality", quality); }); - - page_delay_scale.add_mark (0, Gtk.PositionType.BOTTOM, null); - page_delay_scale.add_mark (500, Gtk.PositionType.BOTTOM, null); - page_delay_scale.add_mark (1000, Gtk.PositionType.BOTTOM, null); - page_delay_scale.add_mark (2000, Gtk.PositionType.BOTTOM, null); - page_delay_scale.add_mark (4000, Gtk.PositionType.BOTTOM, null); - page_delay_scale.add_mark (6000, Gtk.PositionType.BOTTOM, null); - page_delay_scale.add_mark (8000, Gtk.PositionType.BOTTOM, null); - page_delay_scale.add_mark (10000, Gtk.PositionType.BOTTOM, null); - page_delay = settings.get_int ("page-delay"); - page_delay_scale.format_value.connect ((value) => { return "%.1fs".printf (value / 1000.0); }); - page_delay_adjustment.value_changed.connect (() => { settings.set_int ("page-delay", page_delay); }); - - 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_end (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; - - authorize_dialog.transient_for = this; - 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 (); - } - - progress_dialog = new ProgressBarDialog (this, _("Saving document...")); - book.saving.connect (book_saving_cb); - } - - 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" }; - 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"); - default_page_width = state_get_integer (f, "last-page", "width", 595); - default_page_height = state_get_integer (f, "last-page", "height", 842); - default_page_dpi = state_get_integer (f, "last-page", "dpi", 72); - switch (state_get_string (f, "last-page", "scan-direction", "top-to-bottom")) - { - default: - case "top-to-bottom": - default_page_scan_direction = ScanDirection.TOP_TO_BOTTOM; - break; - case "bottom-to-top": - default_page_scan_direction = ScanDirection.BOTTOM_TO_TOP; - break; - case "left-to-right": - default_page_scan_direction = ScanDirection.LEFT_TO_RIGHT; - break; - case "right-to-left": - default_page_scan_direction = ScanDirection.RIGHT_TO_LEFT; - break; - } - } - - 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 string state_get_string (KeyFile f, string group_name, string key, string default = "") - { - try - { - return f.get_string (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); - f.set_integer ("last-page", "width", default_page_width); - f.set_integer ("last-page", "height", default_page_height); - f.set_integer ("last-page", "dpi", default_page_dpi); - switch (default_page_scan_direction) - { - case ScanDirection.TOP_TO_BOTTOM: - f.set_value ("last-page", "scan-direction", "top-to-bottom"); - break; - case ScanDirection.BOTTOM_TO_TOP: - f.set_value ("last-page", "scan-direction", "bottom-to-top"); - break; - case ScanDirection.LEFT_TO_RIGHT: - f.set_value ("last-page", "scan-direction", "left-to-right"); - break; - case ScanDirection.RIGHT_TO_LEFT: - f.set_value ("last-page", "scan-direction", "right-to-left"); - break; - } - try - { - FileUtils.set_contents (state_filename, f.to_data ()); - } - catch (Error e) - { - warning ("Failed to write state: %s", e.message); - } - } - - private void book_saving_cb (int page_number) - { - /* Prevent GUI from freezing */ - while (Gtk.events_pending ()) - Gtk.main_iteration (); - - var total = (int) book.n_pages; - var fraction = (page_number + 1.0) / total; - var complete = fraction == 1.0; - if (complete) - Timeout.add (500, () => { - progress_dialog.visible = false; - return false; - }); - var message = _("Saving page %d out of %d").printf (page_number + 1, total); - - progress_dialog.fraction = fraction; - progress_dialog.message = message; - } - - public void show_progress_dialog () - { - progress_dialog.visible = true; - } - - public void hide_progress_dialog () - { - progress_dialog.visible = false; - } - - public void show_error (string error_title, string error_text, bool change_scanner_hint) - { - have_error = true; - this.error_title = error_title; - this.error_text = error_text; - error_change_scanner_hint = change_scanner_hint; - update_info_bar (); - } - - public void start () - { - visible = true; - } -} - -private class ProgressBarDialog : Gtk.Window -{ - private Gtk.ProgressBar bar; - - public double fraction - { - get { return bar.fraction; } - set { bar.fraction = value; } - } - - public string message - { - get { return bar.text; } - set { bar.text = value; } - } - - public ProgressBarDialog (Gtk.ApplicationWindow parent, string title) - { - bar = new Gtk.ProgressBar (); - var hbox = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 5); - var vbox = new Gtk.Box (Gtk.Orientation.VERTICAL, 5); - hbox.hexpand = true; - - bar.text = ""; - bar.show_text = true; - bar.set_size_request (225, 25); - set_size_request (250, 50); - - vbox.pack_start (bar, true, false, 0); - hbox.pack_start (vbox, true, false, 0); - add (hbox); - this.title = title; - - transient_for = parent; - set_position (Gtk.WindowPosition.CENTER_ON_PARENT); - modal = true; - resizable = false; - - hbox.visible = true; - vbox.visible = true; - bar.visible = true; - } -} - -// FIXME: Duplicated from simple-scan.vala -private string? get_temporary_filename (string prefix, string extension) -{ - /* NOTE: I'm not sure if this is a 100% safe strategy to use g_file_open_tmp(), close and - * use the filename but it appears to work in practise */ - - var filename = "%sXXXXXX.%s".printf (prefix, extension); - string path; - try - { - var fd = FileUtils.open_tmp (filename, out path); - Posix.close (fd); - } - catch (Error e) - { - warning ("Error saving email attachment: %s", e.message); - return null; - } - - return path; -} - -private class PageIcon : Gtk.DrawingArea -{ - private string text; - private double r; - private double g; - private double b; - private const int MINIMUM_WIDTH = 20; - - public PageIcon (string text, double r = 1.0, double g = 1.0, double b = 1.0) - { - this.text = text; - this.r = r; - this.g = g; - this.b = b; - } - - public override void get_preferred_width (out int minimum_width, out int natural_width) - { - minimum_width = natural_width = MINIMUM_WIDTH; - } - - public override void get_preferred_height (out int minimum_height, out int natural_height) - { - minimum_height = natural_height = (int) Math.round (MINIMUM_WIDTH * Math.SQRT2); - } - - public override void get_preferred_height_for_width (int width, out int minimum_height, out int natural_height) - { - minimum_height = natural_height = (int) (width * Math.SQRT2); - } - - public override void get_preferred_width_for_height (int height, out int minimum_width, out int natural_width) - { - minimum_width = natural_width = (int) (height / Math.SQRT2); - } - - public override bool draw (Cairo.Context c) - { - var w = get_allocated_width (); - var h = get_allocated_height (); - if (w * Math.SQRT2 > h) - w = (int) Math.round (h / Math.SQRT2); - else - h = (int) Math.round (w * Math.SQRT2); - - c.translate ((get_allocated_width () - w) / 2, (get_allocated_height () - h) / 2); - - c.rectangle (0.5, 0.5, w - 1, h - 1); - - c.set_source_rgb (r, g, b); - c.fill_preserve (); - - c.set_line_width (1.0); - c.set_source_rgb (0.0, 0.0, 0.0); - c.stroke (); - - Cairo.TextExtents extents; - c.text_extents (text, out extents); - c.translate ((w - extents.width) * 0.5 - 0.5, (h + extents.height) * 0.5 - 0.5); - c.show_text (text); - - return true; - } -} -- cgit v1.2.3