diff options
author | Jörg Frings-Fürst <debian@jff.email> | 2024-06-30 20:46:13 +0200 |
---|---|---|
committer | Jörg Frings-Fürst <debian@jff.email> | 2024-06-30 20:46:13 +0200 |
commit | 9b3a82a302bd88c64bb714b009d223f8683f7178 (patch) | |
tree | 9c387fef03143f2f5f809672bf51e6495f874050 /src | |
parent | a675d0fb9f307b714d0b9cf19690d2b08b666d7c (diff) | |
parent | bca1cc8681bbaf662dabc961f84b06adc1255e08 (diff) |
Merge branch 'feature/upstream' into develop
Diffstat (limited to 'src')
-rw-r--r-- | src/app-window.vala | 1518 | ||||
-rw-r--r-- | src/authorize-dialog.vala | 71 | ||||
-rw-r--r-- | src/autosave-manager.vala | 10 | ||||
-rw-r--r-- | src/book-view.vala | 176 | ||||
-rw-r--r-- | src/book.vala | 11 | ||||
-rw-r--r-- | src/drivers-dialog.vala | 200 | ||||
-rw-r--r-- | src/meson.build | 6 | ||||
-rw-r--r-- | src/page-icon.vala | 135 | ||||
-rw-r--r-- | src/page-texture.vala | 663 | ||||
-rw-r--r-- | src/page-view.vala | 503 | ||||
-rw-r--r-- | src/page.vala | 30 | ||||
-rw-r--r-- | src/preferences-dialog.vala | 229 | ||||
-rw-r--r-- | src/reorder-pages-dialog.vala | 60 | ||||
-rw-r--r-- | src/simple-scan.vala | 21 |
14 files changed, 1852 insertions, 1781 deletions
diff --git a/src/app-window.vala b/src/app-window.vala index c4f9af7..402065a 100644 --- a/src/app-window.vala +++ b/src/app-window.vala @@ -14,11 +14,13 @@ private const int DEFAULT_TEXT_DPI = 150; private const int DEFAULT_PHOTO_DPI = 300; [GtkTemplate (ui = "/org/gnome/SimpleScan/ui/app-window.ui")] -public class AppWindow : Hdy.ApplicationWindow +public class AppWindow : Adw.ApplicationWindow { private const GLib.ActionEntry[] action_entries = { { "new_document", new_document_cb }, + { "scan_type", scan_type_action_cb, "s", "'single'"}, + { "document_hint", document_hint_action_cb, "s", "'text'"}, { "scan_single", scan_single_cb }, { "scan_adf", scan_adf_cb }, { "scan_batch", scan_batch_cb }, @@ -30,7 +32,7 @@ public class AppWindow : Hdy.ApplicationWindow { "copy_page", copy_page_cb }, { "delete_page", delete_page_cb }, { "reorder", reorder_document_cb }, - { "save", save_document_activate_cb }, + { "save", save_document_cb }, { "email", email_document_cb }, { "print", print_document_cb }, { "preferences", preferences_cb }, @@ -38,6 +40,16 @@ public class AppWindow : Hdy.ApplicationWindow { "about", about_cb }, { "quit", quit_cb } }; + + private GLib.SimpleAction scan_type_action; + private GLib.SimpleAction document_hint_action; + + private GLib.SimpleAction delete_page_action; + private GLib.SimpleAction page_move_left_action; + private GLib.SimpleAction page_move_right_action; + private GLib.SimpleAction copy_to_clipboard_action; + + private CropActions crop_actions; private Settings settings; private ScanType scan_type = ScanType.SINGLE; @@ -48,52 +60,21 @@ public class AppWindow : Hdy.ApplicationWindow private bool user_selected_device; [GtkChild] - private unowned Hdy.HeaderBar header_bar; - [GtkChild] - private unowned Gtk.Menu page_menu; + private unowned Gtk.PopoverMenu page_menu; [GtkChild] private unowned Gtk.Stack stack; [GtkChild] - private unowned Hdy.StatusPage status_page; + private unowned Adw.StatusPage status_page; [GtkChild] private unowned Gtk.Label status_secondary_label; - [GtkChild] - private unowned Gtk.ListStore device_model; + private ListStore device_model; [GtkChild] private unowned Gtk.Box device_buttons_box; [GtkChild] - private unowned Gtk.ComboBox device_combo; + private unowned Gtk.DropDown device_drop_down; [GtkChild] private unowned Gtk.Box main_vbox; [GtkChild] - private unowned Gtk.RadioMenuItem custom_crop_menuitem; - [GtkChild] - private unowned Gtk.RadioMenuItem a3_menuitem; - [GtkChild] - private unowned Gtk.RadioMenuItem a4_menuitem; - [GtkChild] - private unowned Gtk.RadioMenuItem a5_menuitem; - [GtkChild] - private unowned Gtk.RadioMenuItem a6_menuitem; - [GtkChild] - private unowned Gtk.RadioMenuItem letter_menuitem; - [GtkChild] - private unowned Gtk.RadioMenuItem legal_menuitem; - [GtkChild] - private unowned Gtk.RadioMenuItem four_by_six_menuitem; - [GtkChild] - private unowned Gtk.RadioMenuItem no_crop_menuitem; - [GtkChild] - private unowned Gtk.MenuItem page_move_left_menuitem; - [GtkChild] - private unowned Gtk.MenuItem page_move_right_menuitem; - [GtkChild] - private unowned Gtk.MenuItem page_delete_menuitem; - [GtkChild] - private unowned Gtk.MenuItem crop_rotate_menuitem; - [GtkChild] - private unowned Gtk.MenuItem copy_to_clipboard_menuitem; - [GtkChild] private unowned Gtk.Button save_button; [GtkChild] private unowned Gtk.Button stop_button; @@ -101,23 +82,11 @@ public class AppWindow : Hdy.ApplicationWindow private unowned Gtk.Button scan_button; [GtkChild] private unowned Gtk.ActionBar action_bar; - private Gtk.ToggleButton crop_button; - private Gtk.Button delete_button; - - [GtkChild] - private unowned Gtk.Image scan_options_image; - [GtkChild] - private unowned Gtk.Image scan_hint_image; [GtkChild] - private unowned Gtk.RadioButton scan_single_radio; - [GtkChild] - private unowned Gtk.RadioButton scan_adf_radio; - [GtkChild] - private unowned Gtk.RadioButton scan_batch_radio; - [GtkChild] - private unowned Gtk.RadioButton text_radio; + private unowned Gtk.ToggleButton crop_button; + [GtkChild] - private unowned Gtk.RadioButton photo_radio; + private unowned Adw.ButtonContent scan_button_content; [GtkChild] private unowned Gtk.MenuButton menu_button; @@ -156,8 +125,8 @@ public class AppWindow : Hdy.ApplicationWindow { scanning_ = value; stack.set_visible_child_name ("document"); - page_delete_menuitem.sensitive = !value; - delete_button.sensitive = !value; + + delete_page_action.set_enabled (!value); scan_button.visible = !value; stop_button.visible = value; } @@ -191,22 +160,32 @@ public class AppWindow : Hdy.ApplicationWindow public signal void start_scan (string? device, ScanOptions options); public signal void stop_scan (); public signal void redetect (); + + static string get_device_label (ScanDevice device) { + return device.label; + } public AppWindow () { settings = new Settings ("org.gnome.SimpleScan"); - var renderer = new Gtk.CellRendererText (); - renderer.set_property ("xalign", 0.5); - device_combo.pack_start (renderer, true); - device_combo.add_attribute (renderer, "text", 1); + device_model = new ListStore (typeof (ScanDevice)); + device_drop_down.model = device_model; + device_drop_down.expression = new Gtk.CClosureExpression ( + typeof (string), + null, + {}, + (Callback) get_device_label, + null, + null + ); 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 (); @@ -221,29 +200,22 @@ public class AppWindow : Hdy.ApplicationWindow 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_markup ("%s", error_text); - dialog.run (); - dialog.destroy (); + var dialog = new Adw.MessageDialog (this, + error_title, + error_text); + dialog.add_response ("close", _("_Close")); + dialog.set_response_appearance ("close", Adw.ResponseAppearance.SUGGESTED); + dialog.show (); } - public void authorize (string resource, out string username, out string password) + public async AuthorizeDialogResponse authorize (string resource) { /* 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; + var authorize_dialog = new AuthorizeDialog (this, description); authorize_dialog.transient_for = this; - authorize_dialog.run (); - authorize_dialog.destroy (); - username = authorize_dialog.get_username (); - password = authorize_dialog.get_password (); + return yield authorize_dialog.open (); } private void update_scan_status () @@ -265,7 +237,7 @@ public class AppWindow : Hdy.ApplicationWindow status_secondary_label.visible = false; device_buttons_box.visible = true; device_buttons_box.sensitive = true; - device_combo.sensitive = true; + device_drop_down.sensitive = true; } else if (this.missing_driver != null) { @@ -285,7 +257,7 @@ public class AppWindow : Hdy.ApplicationWindow status_secondary_label.visible = true; device_buttons_box.visible = true; device_buttons_box.sensitive = true; - device_combo.sensitive = false; // We would like to be refresh button to be active + device_drop_down.sensitive = false; // We would like to be refresh button to be active } } @@ -294,83 +266,31 @@ public class AppWindow : Hdy.ApplicationWindow have_devices = true; this.missing_driver = missing_driver; + // Ignore selected events during this code, to prevent updating "selected-device" 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; + /* + Technically this could be optimized, but: + a) for the typical amount of scanners that would probably be overkill + b) we rescan only on user action so this is rarely called + */ + device_model.remove_all (); - /* 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 + /* Add new devices */ + foreach (var device in devices) { - device_model.insert (out iter, index); - device_model.set (iter, 0, device.name, 1, device.label, -1); + device_model.append (device); } - 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); + /* Select the previously selected device or the first available device */ + var device_name = settings.get_string ("selected-device"); + + uint position = 0; + if (device_name != null && find_device_by_name (device_name, out position) != null) + device_drop_down.selected = position; else - device_combo.set_active (0); + device_drop_down.selected = 0; } setting_devices = false; @@ -378,29 +298,40 @@ public class AppWindow : Hdy.ApplicationWindow update_scan_status (); } - private bool prompt_to_load_autosaved_book () + private async bool prompt_to_load_autosaved_book () { - var dialog = new Gtk.MessageDialog (this, - Gtk.DialogFlags.MODAL, - Gtk.MessageType.QUESTION, - Gtk.ButtonsType.YES_NO, + var dialog = new Adw.MessageDialog (this, + "", /* Contents of dialog that shows if autosaved book should be loaded. */ _("An autosaved book exists. Do you want to open it?")); - dialog.set_default_response(Gtk.ResponseType.YES); - var response = dialog.run (); - dialog.destroy (); - return response == Gtk.ResponseType.YES; + + dialog.add_response ("no", _("_No")); + dialog.add_response ("yes", _("_Yes")); + + dialog.set_response_appearance ("no", Adw.ResponseAppearance.DESTRUCTIVE); + dialog.set_response_appearance ("yes", Adw.ResponseAppearance.SUGGESTED); + + dialog.set_default_response("yes"); + dialog.show (); + + string response = "yes"; + + SourceFunc callback = prompt_to_load_autosaved_book.callback; + dialog.response.connect((res) => { + response = res; + callback(); + }); + + yield; + + return response == "yes"; } private string? get_selected_device () { - Gtk.TreeIter iter; - - if (device_combo.get_active_iter (out iter)) + if (device_drop_down.selected != Gtk.INVALID_LIST_POSITION) { - string device; - device_model.get (iter, 0, out device, -1); - return device; + return ((ScanDevice) device_model.get_item (device_drop_down.selected)).name; } return null; @@ -408,13 +339,9 @@ public class AppWindow : Hdy.ApplicationWindow private string? get_selected_device_label () { - Gtk.TreeIter iter; - - if (device_combo.get_active_iter (out iter)) + if (device_drop_down.selected != Gtk.INVALID_LIST_POSITION) { - string label; - device_model.get (iter, 1, out label, -1); - return label; + return ((ScanDevice) device_model.get_item (device_drop_down.selected)).label; } return null; @@ -424,32 +351,31 @@ public class AppWindow : Hdy.ApplicationWindow { user_selected_device = true; - Gtk.TreeIter iter; - if (!find_scan_device (device, out iter)) + uint position; + find_device_by_name (device, out position); + + if (position != Gtk.INVALID_LIST_POSITION) return; - device_combo.set_active_iter (iter); + device_drop_down.selected = position; } - private bool find_scan_device (string device, out Gtk.TreeIter iter) + private ScanDevice? find_device_by_name(string name, out uint position) { - bool have_iter = false; - - if (device_model.get_iter_first (out iter)) + for (uint i = 0; i < device_model.get_n_items (); i++) { - 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)); + var item = (ScanDevice?) device_model.get_item (i); + if (item.label == name) { + position = i; + return item; + } } - - return have_iter; + + position = Gtk.INVALID_LIST_POSITION; + return null; } - private string? choose_file_location () + private async string? choose_file_location () { /* Get directory to save to */ string? directory = null; @@ -457,161 +383,110 @@ public class AppWindow : Hdy.ApplicationWindow if (directory == null || directory == "") directory = GLib.Filename.to_uri(Environment.get_user_special_dir (UserDirectory.DOCUMENTS)); + + var save_dialog = new Gtk.FileDialog (); + save_dialog.title = _("Save As…"); + save_dialog.modal = true; + save_dialog.accept_label = _("_Save"); - var save_dialog = new Gtk.FileChooserNative (/* Save dialog: Dialog title */ - _("Save As…"), - this, - Gtk.FileChooserAction.SAVE, - _("_Save"), - _("_Cancel")); - save_dialog.local_only = false; + // TODO(gtk4) + // save_dialog.local_only = false; var save_format = settings.get_string ("save-format"); if (book_uri != null) - save_dialog.set_uri (book_uri); - else { - save_dialog.set_current_folder_uri (directory); + { + save_dialog.initial_file = GLib.File.new_for_uri (book_uri); + } + else + { + save_dialog.initial_folder = GLib.File.new_for_uri (directory); + /* Default filename to use when saving document. */ /* To that filename the extension will be added, eg. "Scanned Document.pdf" */ - save_dialog.set_current_name (_("Scanned Document") + "." + mime_type_to_extension (save_format)); + save_dialog.initial_name = (_("Scanned Document") + "." + mime_type_to_extension (save_format)); } - - /* 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, "application/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, "image/jpeg", - -1); - file_type_store.append (out iter); - file_type_store.set (iter, - /* Save dialog: Label for saving in PNG format */ - 0, _("PNG (lossless)"), - 1, "image/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, "image/webp", - -1); -#endif - + + var filters = new ListStore (typeof (Gtk.FileFilter)); + + var pdf_filter = new Gtk.FileFilter (); + pdf_filter.set_filter_name (_("PDF (multi-page document)")); + pdf_filter.add_pattern ("*.pdf" ); + pdf_filter.add_mime_type ("application/pdf"); + filters.append (pdf_filter); + + var jpeg_filter = new Gtk.FileFilter (); + jpeg_filter.set_filter_name (_("JPEG (compressed)")); + jpeg_filter.add_pattern ("*.jpg" ); + jpeg_filter.add_pattern ("*.jpeg" ); + jpeg_filter.add_mime_type ("image/jpeg"); + filters.append (jpeg_filter); + + var png_filter = new Gtk.FileFilter (); + png_filter.set_filter_name (_("PNG (lossless)")); + png_filter.add_pattern ("*.png" ); + png_filter.add_mime_type ("image/png"); + filters.append (png_filter); + + var webp_filter = new Gtk.FileFilter (); + webp_filter.set_filter_name (_("WebP (compressed)")); + webp_filter.add_pattern ("*.webp" ); + webp_filter.add_mime_type ("image/webp"); + filters.append (webp_filter); + + var all_filter = new Gtk.FileFilter (); + all_filter.set_filter_name (_("All Files")); + all_filter.add_pattern ("*"); + filters.append (all_filter); + + save_dialog.filters = filters; + + switch (save_format) + { + case "application/pdf": + save_dialog.default_filter = pdf_filter; + break; + case "image/jpeg": + save_dialog.default_filter = jpeg_filter; + break; + case "image/png": + save_dialog.default_filter = png_filter; + break; + case "image/webp": + save_dialog.default_filter = webp_filter; + break; + } + var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6); box.visible = true; box.spacing = 10; - 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.add (label); - - 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.add (file_type_combo); + box.set_halign (Gtk.Align.CENTER); - if (file_type_store.get_iter_first (out iter)) - { - do - { - string mime_type; - file_type_store.get (iter, 1, out mime_type, -1); - if (mime_type == save_format) - file_type_combo.set_active_iter (iter); - } while (file_type_store.iter_next (ref iter)); - } - - /* Label in save dialog beside compression slider */ - var quality_label = new Gtk.Label (_("Compression:")); - box.add (quality_label); - - 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 = 250; - quality_scale.draw_value = false; - var minimum_size_label = "<small>%s</small>".printf (_("Minimum size")); - quality_scale.add_mark (quality_adjustment.lower, Gtk.PositionType.BOTTOM, minimum_size_label); - quality_scale.add_mark (75, Gtk.PositionType.BOTTOM, null); - quality_scale.add_mark (90, Gtk.PositionType.BOTTOM, null); - var full_detail_label = "<small>%s</small>".printf (_("Full detail")); - quality_scale.add_mark (quality_adjustment.upper, Gtk.PositionType.BOTTOM, full_detail_label); - quality_adjustment.value = settings.get_int ("jpeg-quality"); - quality_adjustment.value_changed.connect (() => { settings.set_int ("jpeg-quality", (int) quality_adjustment.value); }); - box.add (quality_scale); - - /* Quality not applicable to PNG */ - quality_scale.visible = quality_label.visible = (save_format != "image/png"); - - file_type_combo.changed.connect (() => + while (true) { - var mime_type = ""; - Gtk.TreeIter i; - if (file_type_combo.get_active_iter (out i)) + File? file = null; + try { + file = yield save_dialog.save (this, null); + } + catch (Error e) { - file_type_store.get (i, 1, out mime_type, -1); - settings.set_string ("save-format", mime_type); + warning ("Failed to open save dialog: %s", e.message); } - var filename = save_dialog.get_current_name (); - - /* Replace extension */ - var extension_index = filename.last_index_of_char ('.'); - if (extension_index >= 0) - filename = filename.slice (0, extension_index); - filename = filename + "." + mime_type_to_extension (mime_type); - save_dialog.set_current_name (filename); - - /* Quality not applicable to PNG */ - quality_scale.visible = quality_label.visible = (mime_type != "image/png"); - }); - - while (true) - { - var response = save_dialog.run (); - if (response != Gtk.ResponseType.ACCEPT) + if (file == null) { - save_dialog.destroy (); return null; } - var mime_type = ""; - Gtk.TreeIter i; - if (file_type_combo.get_active_iter (out i)) - file_type_store.get (i, 1, out mime_type, -1); + var uri = file.get_uri (); + + var extension = uri_extension(uri); - var uri = save_dialog.get_uri (); + var mime_type = extension_to_mime_type(extension); + mime_type = mime_type != null ? mime_type : "application/pdf"; - var extension_index = uri.last_index_of_char ('.'); - if (extension_index < 0) + settings.set_string ("save-format", mime_type); + + if (extension == null) uri += "." + mime_type_to_extension (mime_type); /* Check the file(s) don't already exist */ @@ -623,35 +498,58 @@ public class AppWindow : Hdy.ApplicationWindow } else files.append (File.new_for_uri (uri)); + + var overwrite_check = true; + + // We assume that GTK or system file dialog asked about overwrite already so we reask only if there is more than one file or we changed the name + // Ideally in flatpack era we should not modify file name after save dialog is done + // but for the sake of keeping old functionality in tact we leave it as it + if (files.length () > 1 || file.get_uri () != uri) + { + overwrite_check = yield check_overwrite (this, files); + } - if (check_overwrite (save_dialog.transient_for, files)) + if (overwrite_check) { var directory_uri = uri.substring (0, uri.last_index_of ("/") + 1); settings.set_string ("save-directory", directory_uri); - save_dialog.destroy (); return uri; } } - } - private bool check_overwrite (Gtk.Window parent, List<File> files) + private async bool check_overwrite (Gtk.Window parent, List<File> files) { foreach (var file in files) { if (!file.query_exists ()) continue; - var dialog = new Gtk.MessageDialog (parent, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE, + var title = _("A file named “%s” already exists. Do you want to replace it?").printf(file.get_basename ()); + + var dialog = new Adw.MessageDialog (parent, /* 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) + title, + null); + + dialog.add_response ("cancel", _("_Cancel")); + dialog.add_response ("replace", _("_Replace")); + + dialog.set_response_appearance ("replace", Adw.ResponseAppearance.DESTRUCTIVE); + + SourceFunc callback = check_overwrite.callback; + string response = "cancel"; + + dialog.response.connect ((res) => { + response = res; + callback (); + }); + + dialog.show (); + + yield; + + if (response != "replace") return false; } @@ -677,7 +575,7 @@ public class AppWindow : Hdy.ApplicationWindow var extension_lower = extension.down (); if (extension_lower == "pdf") return "application/pdf"; - else if (extension_lower == "jpg") + else if (extension_lower == "jpg" || extension_lower == "jpeg") return "image/jpeg"; else if (extension_lower == "png") return "image/png"; @@ -687,12 +585,20 @@ public class AppWindow : Hdy.ApplicationWindow return null; } - private string uri_to_mime_type (string uri) + private string? uri_extension (string uri) { var extension_index = uri.last_index_of_char ('.'); if (extension_index < 0) + return null; + + return uri.substring (extension_index + 1); + } + + private string uri_to_mime_type (string uri) + { + var extension = uri_extension(uri); + if (extension == null) return "image/jpeg"; - var extension = uri.substring (extension_index + 1); var mime_type = extension_to_mime_type (extension); if (mime_type == null) @@ -703,7 +609,7 @@ public class AppWindow : Hdy.ApplicationWindow private async bool save_document_async () { - var uri = choose_file_location (); + var uri = yield choose_file_location (); if (uri == null) return false; @@ -739,7 +645,7 @@ public class AppWindow : Hdy.ApplicationWindow return false; } save_button.sensitive = true; - progress_bar.destroy_with_delay (500); + progress_bar.remove_with_delay (500, action_bar); book_needs_saving = false; book_uri = uri; @@ -751,29 +657,36 @@ public class AppWindow : Hdy.ApplicationWindow if (!book_needs_saving || (book.n_pages == 0)) 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 (); + var dialog = new Adw.MessageDialog (this, + title, + _("If you don’t save, changes will be permanently lost.")); + + dialog.add_response ("discard", discard_label); + dialog.add_response ("cancel", _("_Cancel")); + dialog.add_response ("save", _("_Save")); + + dialog.set_response_appearance ("discard", Adw.ResponseAppearance.DESTRUCTIVE); + dialog.set_response_appearance ("save", Adw.ResponseAppearance.SUGGESTED); + + dialog.show (); + + string response = "cancel"; + SourceFunc callback = prompt_to_save_async.callback; + dialog.response.connect((res) => { + response = res; + callback (); + }); + + yield; switch (response) { - case Gtk.ResponseType.YES: + case "save": if (yield save_document_async ()) return true; else return false; - case Gtk.ResponseType.NO: + case "discard": return true; default: return false; @@ -786,7 +699,7 @@ public class AppWindow : Hdy.ApplicationWindow book_needs_saving = false; book_uri = null; save_button.sensitive = false; - copy_to_clipboard_menuitem.sensitive = false; + copy_to_clipboard_action.set_enabled (false); update_scan_status (); stack.set_visible_child_name ("startup"); } @@ -814,19 +727,46 @@ public class AppWindow : Hdy.ApplicationWindow { if (uri == "install-firmware") { - install_drivers (); + var dialog = new DriversDialog (this, missing_driver); + dialog.open.begin (() => {}); return true; } return false; } + [GtkCallback] private void new_document_cb () { new_document (); } [GtkCallback] + private void crop_toggle_cb (Gtk.ToggleButton btn) + { + 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 (btn.active) + { + // Avoid overwriting crop name if there is already different crop active + if (!page.has_crop) + set_crop ("custom"); + } + else + { + set_crop (null); + } + } + + [GtkCallback] private void redetect_button_clicked_cb (Gtk.Button button) { have_devices = false; @@ -843,6 +783,31 @@ public class AppWindow : Hdy.ApplicationWindow start_scan (get_selected_device (), options); } + private void scan_type_action_cb (SimpleAction action, Variant? value) + { + var type = value.get_string (); + + switch (type) { + case "single": + set_scan_type (ScanType.SINGLE); + break; + case "adf": + set_scan_type (ScanType.ADF); + break; + case "batch": + set_scan_type (ScanType.BATCH); + break; + default: + return; + } + } + + private void document_hint_action_cb (SimpleAction action, Variant? value) + { + var hint = value.get_string (); + set_document_hint(hint, true); + } + private void scan_single_cb () { var options = make_scan_options (); @@ -871,32 +836,48 @@ public class AppWindow : Hdy.ApplicationWindow private void rotate_left_cb () { - rotate_left_button_clicked_cb (); + if (updating_page_menu) + return; + var page = book_view.selected_page; + if (page != null) + page.rotate_left (); } private void rotate_right_cb () { - rotate_right_button_clicked_cb (); + if (updating_page_menu) + return; + var page = book_view.selected_page; + if (page != null) + page.rotate_right (); } private void move_left_cb () { - page_move_left_menuitem_activate_cb (); + var page = book_view.selected_page; + var index = book.get_page_index (page); + if (index > 0) + book.move_page (page, index - 1); } private void move_right_cb () { - page_move_right_menuitem_activate_cb (); + 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); } private void copy_page_cb () { - copy_to_clipboard_button_clicked_cb (); + var page = book_view.selected_page; + if (page != null) + page.copy_to_clipboard (this); } private void delete_page_cb () { - page_delete_menuitem_activate_cb (); + book_view.book.delete_page (book_view.selected_page); } private void set_scan_type (ScanType scan_type) @@ -906,83 +887,33 @@ public class AppWindow : Hdy.ApplicationWindow switch (scan_type) { case ScanType.SINGLE: - scan_single_radio.active = true; - scan_options_image.icon_name = "scanner-symbolic"; + scan_type_action.set_state ("single"); + scan_button_content.icon_name = "scanner-symbolic"; scan_button.tooltip_text = _("Scan a single page from the scanner"); break; case ScanType.ADF: - scan_adf_radio.active = true; - scan_options_image.icon_name = "scan-type-adf-symbolic"; + scan_type_action.set_state ("adf"); + scan_button_content.icon_name = "scan-type-adf-symbolic"; scan_button.tooltip_text = _("Scan multiple pages from the scanner"); break; case ScanType.BATCH: - scan_batch_radio.active = true; - scan_options_image.icon_name = "scan-type-batch-symbolic"; + scan_type_action.set_state ("batch"); + scan_button_content.icon_name = "scan-type-batch-symbolic"; scan_button.tooltip_text = _("Scan multiple pages from the scanner"); break; } } - [GtkCallback] - private void scan_single_radio_toggled_cb (Gtk.ToggleButton button) - { - if (button.active) - set_scan_type (ScanType.SINGLE); - } - - [GtkCallback] - private void scan_adf_radio_toggled_cb (Gtk.ToggleButton button) - { - if (button.active) - set_scan_type (ScanType.ADF); - } - - [GtkCallback] - private void scan_batch_radio_toggled_cb (Gtk.ToggleButton button) - { - if (button.active) - set_scan_type (ScanType.BATCH); - } - private void set_document_hint (string document_hint, bool save = false) { this.document_hint = document_hint; - if (document_hint == "text") - { - text_radio.active = true; - scan_hint_image.icon_name = "x-office-document-symbolic"; - } - else if (document_hint == "photo") - { - photo_radio.active = true; - scan_hint_image.icon_name = "image-x-generic-symbolic"; - } + document_hint_action.set_state (document_hint); if (save) settings.set_string ("document-type", document_hint); } - [GtkCallback] - private void text_radio_toggled_cb (Gtk.ToggleButton button) - { - if (button.active) - set_document_hint ("text", true); - } - - [GtkCallback] - private void photo_radio_toggled_cb (Gtk.ToggleButton button) - { - if (button.active) - set_document_hint ("photo", true); - } - - [GtkCallback] - private void preferences_button_clicked_cb (Gtk.Button button) - { - preferences_dialog.present (); - } - private ScanOptions make_scan_options () { var options = new ScanOptions (); @@ -1008,7 +939,7 @@ public class AppWindow : Hdy.ApplicationWindow } [GtkCallback] - private void device_combo_changed_cb (Gtk.Widget widget) + private void device_drop_down_changed_cb (Object widget, ParamSpec spec) { if (setting_devices) return; @@ -1045,14 +976,14 @@ public class AppWindow : Hdy.ApplicationWindow var page = book_view.selected_page; if (page == null) { - page_move_left_menuitem.sensitive = false; - page_move_right_menuitem.sensitive = false; + page_move_left_action.set_enabled (false); + page_move_right_action.set_enabled (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; + page_move_left_action.set_enabled (index > 0); + page_move_right_action.set_enabled (index < book.n_pages - 1); } } @@ -1064,33 +995,8 @@ public class AppWindow : Hdy.ApplicationWindow 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 == "A3") - menuitem = a3_menuitem; - else 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_actions.update_current_crop (page.crop_name); crop_button.active = page.has_crop; updating_page_menu = false; @@ -1113,50 +1019,29 @@ public class AppWindow : Hdy.ApplicationWindow 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); - } + var launcher = new Gtk.FileLauncher(file); + launcher.launch.begin (this, null); } - private void show_page_menu_cb (BookView view, Gdk.Event event) + private void show_page_menu_cb (BookView view, Gtk.Widget from, double x, double y) { - page_menu.popup_at_pointer (event); - } + double tx, ty; + from.translate_coordinates(this, x, y, out tx, out ty); - [GtkCallback] - private void rotate_left_button_clicked_cb () - { - if (updating_page_menu) - return; - var page = book_view.selected_page; - if (page != null) - page.rotate_left (); - } + Gdk.Rectangle rect = { x: (int) tx, y: (int) ty, w: 1, h: 1 }; - [GtkCallback] - private void rotate_right_button_clicked_cb () - { - if (updating_page_menu) - return; - var page = book_view.selected_page; - if (page != null) - page.rotate_right (); + page_menu.set_pointing_to (rect); + page_menu.popup (); } private void set_crop (string? crop_name) { - crop_rotate_menuitem.sensitive = crop_name != null; - if (updating_page_menu) return; + if (crop_name == "none") + crop_name = null; + var page = book_view.selected_page; if (page == null) { @@ -1177,73 +1062,17 @@ public class AppWindow : Hdy.ApplicationWindow } else page.set_named_crop (crop_name); + + crop_actions.update_current_crop (crop_name); + crop_button.active = page.has_crop; } - - [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) + + public void crop_set_action_cb (SimpleAction action, Variant? value) { - if (widget.active) - set_crop ("A4"); + set_crop (value.get_string ()); } - [GtkCallback] - private void a3_menuitem_toggled_cb (Gtk.CheckMenuItem widget) - { - if (widget.active) - set_crop ("A3"); - } - - [GtkCallback] - private void crop_rotate_menuitem_activate_cb () + public void crop_rotate_action_cb () { var page = book_view.selected_page; if (page == null) @@ -1251,210 +1080,54 @@ public class AppWindow : Hdy.ApplicationWindow page.rotate_crop (); } - [GtkCallback] - private void page_move_left_menuitem_activate_cb () - { - 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 () - { - 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 () - { - book_view.book.delete_page (book_view.selected_page); - } - - private void reorder_document () + private void reorder_document_cb () { - 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 = C_("dialog title", "Reorder Pages"); + var dialog = new ReorderPagesDialog (); 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 (() => + + /* Button for combining sides in reordering dialog */ + dialog.combine_sides.clicked.connect (() => { book.combine_sides (); - dialog.destroy (); + dialog.close (); }); - 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 (() => + /* Button for combining sides in reverse order in reordering dialog */ + dialog.combine_sides_rev.clicked.connect (() => { book.combine_sides_reverse (); - dialog.destroy (); + dialog.close (); }); - 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 (() => + /* Button for reversing in reordering dialog */ + dialog.reverse.clicked.connect (() => { book.reverse (); - dialog.destroy (); + dialog.close (); }); - 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 (() => + /* Button for keeping the ordering, but flip every second upside down */ + dialog.flip_odd.clicked.connect (() => { - dialog.destroy (); + book.flip_every_second(FlipEverySecond.Odd); + dialog.close (); }); - b.visible = true; - g.attach (b, 1, 2, 1, 1); - /* Label on button for keeping the ordering, but flip every second upside down */ - b = make_reorder_button (_("Flip even pages upside-down"), "R1U2R3U4R5U6-R1R2R3R4R5R6"); - b.clicked.connect (() => + /* Button for keeping the ordering, but flip every second upside down */ + dialog.flip_even.clicked.connect (() => { + dialog.close (); book.flip_every_second(FlipEverySecond.Even); - dialog.destroy (); }); - b.visible = true; - g.attach (b, 0, 3, 1, 1); - - - /* Label on button for keeping the ordering, but flip every second upside down */ - b = make_reorder_button (_("Flip odd pages upside-down"), "U1R2U3R4U5R6-R1R2R3R4R5R6"); - b.clicked.connect (() => - { - book.flip_every_second(FlipEverySecond.Odd); - dialog.destroy (); - }); - b.visible = true; - g.attach (b, 1, 3, 1, 1); dialog.present (); } - private void reorder_document_cb () - { - 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; - label.vexpand = true; - vbox.add (label); - - var rb = make_reorder_box (items); - rb.visible = true; - rb.vexpand = true; - vbox.add (rb); - - 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.Label ("➤"); - a.visible = true; - box.add (a); - 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.add (page_box); - } - if (side == 'U') { - var icon = new PageIcon (side, items[i] - '1', 180); - icon.visible = true; - page_box.add (icon); - } else { - var icon = new PageIcon (side, items[i] - '1', 0); - icon.visible = true; - page_box.add (icon); - } - } - - return box; - } - - [GtkCallback] - private void save_file_button_clicked_cb (Gtk.Widget widget) - { - save_document_async.begin (); - } - - public void save_document_activate_cb () + public void save_document_cb () { save_document_async.begin (); } - [GtkCallback] - private void copy_to_clipboard_button_clicked_cb () - { - 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) @@ -1549,16 +1222,8 @@ public class AppWindow : Hdy.ApplicationWindow 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); - } + var launcher = new Gtk.UriLauncher ("help:simple-scan"); + launcher.launch.begin (this, null); } private void help_cb () @@ -1570,23 +1235,22 @@ public class AppWindow : Hdy.ApplicationWindow { string[] authors = { "Robert Ancell <robert.ancell@canonical.com>" }; - string title = _("About Document Scanner"); - - string description = _("Simple document scanning tool"); - - Gtk.show_about_dialog (this, - "title", title, - "authors", authors, - "translator-credits", _("translator-credits"), - "comments", description, - "copyright", "Copyright © 2009-2018 Canonical Ltd.", - "license-type", Gtk.License.GPL_3_0, - "program-name", _("Document Scanner"), - "logo-icon-name", "org.gnome.SimpleScan", - "version", VERSION, - "website", "https://gitlab.gnome.org/GNOME/simple-scan", - "wrap-license", true); - } + var about = new Adw.AboutWindow () + { + transient_for = this, + developers = authors, + translator_credits = _("translator-credits"), + copyright = "Copyright © 2009-2018 Canonical Ltd.", + license_type = Gtk.License.GPL_3_0, + application_name = _("Document Scanner"), + application_icon = "org.gnome.SimpleScan", + version = VERSION, + website = "https://gitlab.gnome.org/GNOME/simple-scan", + issue_url = "https://gitlab.gnome.org/GNOME/baobab/-/issues/new", + }; + + about.present (); + } private void about_cb () { @@ -1617,200 +1281,29 @@ public class AppWindow : Hdy.ApplicationWindow on_quit (); } - public override void size_allocate (Gtk.Allocation allocation) + public override void size_allocate (int width, int height, int baseline) { - base.size_allocate (allocation); + base.size_allocate (width, height, baseline); if (!window_is_maximized && !window_is_fullscreen) { - get_size (out window_width, out window_height); + window_width = this.get_width(); + window_height = this.get_height(); save_state (); } } - private void install_drivers () + public override void unmap () { - var message = "", instructions = ""; - string[] packages_to_install = {}; - switch (missing_driver) - { - case "brscan": - case "brscan2": - case "brscan3": - case "brscan4": - /* Message to indicate a Brother scanner has been detected */ - message = _("You appear to have a Brother scanner."); - /* Instructions on how to install Brother scanner drivers */ - instructions = _("Drivers for this are available on the <a href=\"http://support.brother.com\">Brother website</a>."); - break; - case "pixma": - /* Message to indicate a Canon Pixma scanner has been detected */ - message = _("You appear to have a Canon scanner, which is supported by the <a href=\"http://www.sane-project.org/man/sane-pixma.5.html\">Pixma SANE backend</a>."); - /* Instructions on how to resolve issue with SANE scanner drivers */ - instructions = _("Please check if your <a href=\"http://www.sane-project.org/sane-supported-devices.html\">scanner is supported by SANE</a>, otherwise report the issue to the <a href=\"https://alioth-lists.debian.net/cgi-bin/mailman/listinfo/sane-devel\">SANE mailing list</a>."); - break; - case "samsung": - /* Message to indicate a Samsung scanner has been detected */ - message = _("You appear to have a Samsung scanner."); - /* Instructions on how to install Samsung scanner drivers. - Because HP acquired Samsung's global printing business in 2017, the support is made on HP site. */ - instructions = _("Drivers for this are available on the <a href=\"https://support.hp.com\">HP website</a> (HP acquired Samsung's printing business)."); - break; - case "hpaio": - case "smfp": - /* Message to indicate a HP scanner has been detected */ - message = _("You appear to have an HP scanner."); - if (missing_driver == "hpaio") - packages_to_install = { "libsane-hpaio" }; - else - /* Instructions on how to install HP scanner drivers. - smfp is rebranded and slightly modified Samsung devices, - for example: HP Laser MFP 135a is rebranded Samsung Xpress SL-M2070. - It require custom drivers, not available in hpaio package */ - instructions = _("Drivers for this are available on the <a href=\"https://support.hp.com\">HP website</a>."); - break; - case "epkowa": - /* Message to indicate an Epson scanner has been detected */ - message = _("You appear to have an Epson scanner."); - /* Instructions on how to install Epson scanner drivers */ - instructions = _("Drivers for this are available on the <a href=\"http://support.epson.com\">Epson website</a>."); - break; - case "lexmark_nscan": - /* Message to indicate a Lexmark scanner has been detected */ - message = _("You appear to have a Lexmark scanner."); - /* Instructions on how to install Lexmark scanner drivers */ - instructions = _("Drivers for this are available on the <a href=\"http://support.lexmark.com\">Lexmark website</a>."); - break; - } - var dialog = new Gtk.Dialog.with_buttons (/* Title of dialog giving instructions on how to install drivers */ - _("Install drivers"), this, Gtk.DialogFlags.MODAL, _("_Close"), Gtk.ResponseType.CLOSE); - dialog.get_content_area ().border_width = 12; - dialog.get_content_area ().spacing = 6; - - var label = new Gtk.Label (message); - label.visible = true; - label.xalign = 0f; - label.vexpand = true; - label.use_markup = true; - dialog.get_content_area ().add (label); - - var instructions_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6); - instructions_box.visible = true; - instructions_box.vexpand = true; - dialog.get_content_area ().add (instructions_box); - - var stack = new Gtk.Stack (); - instructions_box.add (stack); - - 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.add (instructions_label); - - label = new Gtk.Label (/* Message in driver install dialog */ - _("Once installed you will need to restart this app.")); - label.visible = true; - label.xalign = 0f; - label.vexpand = true; - dialog.get_content_area ().border_width = 12; - dialog.get_content_area ().add (label); - - 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; + window_is_maximized = is_maximized (); + window_is_fullscreen = is_fullscreen (); + save_state (); + + base.unmap (); } [GtkCallback] - private bool window_delete_event_cb (Gtk.Widget widget, Gdk.EventAny event) + private bool window_close_request_cb (Gtk.Window window) { on_quit (); return true; /* Let us quit on our own terms */ @@ -1835,13 +1328,13 @@ public class AppWindow : Hdy.ApplicationWindow { save_button.sensitive = true; book_needs_saving = true; - copy_to_clipboard_menuitem.sensitive = true; + copy_to_clipboard_action.set_enabled (true); } private void load () { preferences_dialog = new PreferencesDialog (settings); - preferences_dialog.delete_event.connect (() => { + preferences_dialog.close_request.connect (() => { preferences_dialog.visible = false; return true; }); @@ -1852,13 +1345,18 @@ public class AppWindow : Hdy.ApplicationWindow var app = Application.get_default () as Gtk.Application; - /* 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 = _("Document Scanner"); + crop_actions = new CropActions (this); app.add_action_entries (action_entries, this); + scan_type_action = (GLib.SimpleAction) app.lookup_action("scan_type"); + document_hint_action = (GLib.SimpleAction) app.lookup_action("document_hint"); + + delete_page_action = (GLib.SimpleAction) app.lookup_action("delete_page"); + page_move_left_action = (GLib.SimpleAction) app.lookup_action("move_left"); + page_move_right_action = (GLib.SimpleAction) app.lookup_action("move_right"); + copy_to_clipboard_action = (GLib.SimpleAction) app.lookup_action("copy_page"); + app.set_accels_for_action ("app.new_document", { "<Ctrl>N" }); app.set_accels_for_action ("app.scan_single", { "<Ctrl>1" }); app.set_accels_for_action ("app.scan_adf", { "<Ctrl>F" }); @@ -1894,79 +1392,15 @@ public class AppWindow : Hdy.ApplicationWindow 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_mnemonic (/* Label on new document button */ - _("_New Document")); - button.visible = true; - button.clicked.connect (new_document_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.add (rotate_box); - - button = new Gtk.Button.from_icon_name ("object-rotate-left-symbolic"); - button.visible = true; - button.image.margin_start = 18; - button.image.margin_end = 18; - /* 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.add (button); - - button = new Gtk.Button.from_icon_name ("object-rotate-right-symbolic"); - button.visible = true; - button.image.margin_start = 18; - button.image.margin_end = 18; - /* 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.add (button); - - crop_button = new Gtk.ToggleButton (); - crop_button.visible = true; - var image = new Gtk.Image.from_icon_name ("crop-symbolic", Gtk.IconSize.BUTTON); - image.visible = true; - image.margin_start = 18; - image.margin_end = 18; - 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.add (crop_button); - - delete_button = new Gtk.Button.from_icon_name ("user-trash-symbolic"); - delete_button.visible = true; - delete_button.image.margin_start = 18; - delete_button.image.margin_end = 18; - /* 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.add (delete_button); - 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; book_view.vexpand = true; - main_vbox.add (book_view); + + main_vbox.prepend (book_view); + 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); @@ -2099,34 +1533,45 @@ public class AppWindow : Hdy.ApplicationWindow visible = true; autosave_manager = new AutosaveManager (); autosave_manager.book = book; - - if (autosave_manager.exists () && prompt_to_load_autosaved_book ()) - autosave_manager.load (); - - if (book.n_pages == 0) - book_needs_saving = false; - else + + if (autosave_manager.exists ()) { - stack.set_visible_child_name ("document"); - book_view.selected_page = book.get_page (0); - book_needs_saving = true; - book_changed_cb (book); + prompt_to_load_autosaved_book.begin ((obj, res) => { + bool restore = prompt_to_load_autosaved_book.end (res); + + if (restore) + { + 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); + } + }); } } } -private class CancellableProgressBar : Gtk.HBox +private class CancellableProgressBar : Gtk.Box { private Gtk.ProgressBar bar; private Gtk.Button? button; public CancellableProgressBar (string? text, Cancellable? cancellable) { + this.orientation = Gtk.Orientation.HORIZONTAL; + bar = new Gtk.ProgressBar (); bar.visible = true; bar.set_text (text); bar.set_show_text (true); - pack_start (bar); + prepend (bar); if (cancellable != null) { @@ -2138,7 +1583,7 @@ private class CancellableProgressBar : Gtk.HBox set_visible (false); cancellable.cancel (); }); - pack_start (button); + prepend (button); } } @@ -2147,14 +1592,49 @@ private class CancellableProgressBar : Gtk.HBox bar.set_fraction (fraction); } - public void destroy_with_delay (uint delay) + public void remove_with_delay (uint delay, Gtk.ActionBar parent) { button.set_sensitive (false); Timeout.add (delay, () => { - this.destroy (); + parent.remove (this); return false; }); } } + +private class CropActions +{ + private GLib.SimpleActionGroup group; + + private GLib.SimpleAction crop_set; + private GLib.SimpleAction crop_rotate; + + private GLib.ActionEntry[] crop_entries = + { + { "set", AppWindow.crop_set_action_cb, "s", "'none'" }, + { "rotate", AppWindow.crop_rotate_action_cb }, + }; + + public CropActions (AppWindow window) + { + group = new GLib.SimpleActionGroup (); + group.add_action_entries (crop_entries, window); + + crop_set = (GLib.SimpleAction) group.lookup_action ("set"); + crop_rotate = (GLib.SimpleAction) group.lookup_action ("rotate"); + + window.insert_action_group ("crop", group); + } + + public void update_current_crop (string? crop_name) + { + crop_rotate.set_enabled (crop_name != null); + + if (crop_name == null) + crop_set.set_state ("none"); + else + crop_set.set_state (crop_name); + } +}
\ No newline at end of file diff --git a/src/authorize-dialog.vala b/src/authorize-dialog.vala index 7cb2a66..ab0237a 100644 --- a/src/authorize-dialog.vala +++ b/src/authorize-dialog.vala @@ -11,18 +11,21 @@ */ [GtkTemplate (ui = "/org/gnome/SimpleScan/ui/authorize-dialog.ui")] -private class AuthorizeDialog : Gtk.Dialog +private class AuthorizeDialog : Gtk.Window { [GtkChild] - private unowned Gtk.Label authorize_label; + private unowned Adw.PreferencesGroup preferences_group; [GtkChild] - private unowned Gtk.Entry username_entry; + private unowned Adw.EntryRow username_entry; [GtkChild] - private unowned Gtk.Entry password_entry; + private unowned Adw.PasswordEntryRow password_entry; - public AuthorizeDialog (string title) + public signal void authorized (AuthorizeDialogResponse res); + + public AuthorizeDialog (Gtk.Window parent, string title) { - authorize_label.set_text (title); + preferences_group.set_title (title); + set_transient_for (parent); } public string get_username () @@ -34,4 +37,60 @@ private class AuthorizeDialog : Gtk.Dialog { return password_entry.text; } + + [GtkCallback] + private void authorize_button_cb () + { + authorized (AuthorizeDialogResponse.new_authorized (get_username (), get_password ())); + } + + [GtkCallback] + private void cancel_button_cb () + { + authorized (AuthorizeDialogResponse.new_canceled ()); + } + + public async AuthorizeDialogResponse open() + { + SourceFunc callback = open.callback; + + AuthorizeDialogResponse response = {}; + + authorized.connect ((res) => + { + response = res; + callback (); + }); + + present (); + yield; + close (); + + return response; + } +} + +public struct AuthorizeDialogResponse +{ + public string username; + public string password; + public bool success; + + public static AuthorizeDialogResponse new_canceled () + { + return AuthorizeDialogResponse () + { + success = false, + }; + } + + public static AuthorizeDialogResponse new_authorized (string username, string password) + { + return AuthorizeDialogResponse () + { + username = username, + password = password, + success = true, + }; + } } diff --git a/src/autosave-manager.vala b/src/autosave-manager.vala index 7e92d33..31fd328 100644 --- a/src/autosave-manager.vala +++ b/src/autosave-manager.vala @@ -117,8 +117,16 @@ public class AutosaveManager var pixels_filename = get_value (file, page_name, "pixels-filename"); var has_crop = get_boolean (file, page_name, "has-crop"); var crop_name = get_value (file, page_name, "crop-name"); + if (crop_name == "") - crop_name = null; + { + // If it has no crop name but has crop it probably means that it is a custom crop + if (has_crop) + crop_name = "custom"; + else + crop_name = null; + } + var crop_x = get_integer (file, page_name, "crop-x"); var crop_y = get_integer (file, page_name, "crop-y"); var crop_width = get_integer (file, page_name, "crop-width"); diff --git a/src/book-view.vala b/src/book-view.vala index e16dc92..162bdad 100644 --- a/src/book-view.vala +++ b/src/book-view.vala @@ -22,7 +22,7 @@ public class BookView : Gtk.Box private bool need_layout; private bool laying_out; private bool show_selected_page; - + /* Currently selected page */ private PageView? selected_page_view = null; public Page? selected_page @@ -47,17 +47,24 @@ public class BookView : Gtk.Box } /* Widget being rendered to */ - private Gtk.Widget drawing_area; + private Gtk.DrawingArea drawing_area; /* Horizontal scrollbar */ private Gtk.Scrollbar scroll; private Gtk.Adjustment adjustment; - private Gdk.CursorType cursor; + private new string cursor; + + private Gtk.EventControllerMotion motion_controller; + private Gtk.EventControllerKey key_controller; + private Gtk.GestureClick primary_click_gesture; + private Gtk.GestureClick secondary_click_gesture; + private Gtk.EventControllerFocus focus_controller; + public signal void page_selected (Page? page); public signal void show_page (Page page); - public signal void show_menu (Gdk.Event event); + public signal void show_menu (Gtk.Widget from, double x, double y); public int x_offset { @@ -93,27 +100,47 @@ public class BookView : Gtk.Box need_layout = true; page_data = new HashTable<Page, PageView> (direct_hash, direct_equal); - cursor = Gdk.CursorType.ARROW; + cursor = "arrow"; drawing_area = new Gtk.DrawingArea (); drawing_area.set_size_request (200, 100); drawing_area.can_focus = true; - drawing_area.events = Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventMask.FOCUS_CHANGE_MASK | Gdk.EventMask.STRUCTURE_MASK | Gdk.EventMask.SCROLL_MASK; drawing_area.vexpand = true; - add (drawing_area); + drawing_area.set_draw_func(draw_cb); + + append (drawing_area); scroll = new Gtk.Scrollbar (Gtk.Orientation.HORIZONTAL, null); adjustment = scroll.adjustment; - add (scroll); - - drawing_area.configure_event.connect (configure_cb); - drawing_area.draw.connect (draw_cb); - drawing_area.motion_notify_event.connect (motion_cb); - drawing_area.key_press_event.connect (key_cb); - drawing_area.button_press_event.connect (button_cb); - drawing_area.button_release_event.connect (button_cb); - drawing_area.focus_in_event.connect_after (focus_cb); - drawing_area.focus_out_event.connect_after (focus_cb); + append (scroll); + + drawing_area.resize.connect (drawing_area_resize_cb); + + motion_controller = new Gtk.EventControllerMotion (); + motion_controller.motion.connect (motion_cb); + drawing_area.add_controller(motion_controller); + + key_controller = new Gtk.EventControllerKey (); + key_controller.key_pressed.connect (key_cb); + drawing_area.add_controller(key_controller); + + primary_click_gesture = new Gtk.GestureClick (); + primary_click_gesture.button = Gdk.BUTTON_PRIMARY; + primary_click_gesture.pressed.connect (primary_pressed_cb); + primary_click_gesture.released.connect (primary_released_cb); + drawing_area.add_controller(primary_click_gesture); + + secondary_click_gesture = new Gtk.GestureClick (); + secondary_click_gesture.button = Gdk.BUTTON_SECONDARY; + secondary_click_gesture.pressed.connect (secondary_pressed_cb); + secondary_click_gesture.released.connect (secondary_released_cb); + drawing_area.add_controller(secondary_click_gesture); + + focus_controller = new Gtk.EventControllerFocus (); + focus_controller.enter.connect_after (focus_cb); + focus_controller.leave.connect_after (focus_cb); + drawing_area.add_controller(focus_controller); + adjustment.value_changed.connect (scroll_cb); drawing_area.visible = true; @@ -125,14 +152,15 @@ public class BookView : Gtk.Box book.page_removed.disconnect (remove_cb); book.reordered.disconnect (reorder_cb); book.cleared.disconnect (clear_cb); - drawing_area.configure_event.disconnect (configure_cb); - drawing_area.draw.disconnect (draw_cb); - drawing_area.motion_notify_event.disconnect (motion_cb); - drawing_area.key_press_event.disconnect (key_cb); - drawing_area.button_press_event.disconnect (button_cb); - drawing_area.button_release_event.disconnect (button_cb); - drawing_area.focus_in_event.disconnect (focus_cb); - drawing_area.focus_out_event.disconnect (focus_cb); + drawing_area.resize.disconnect (drawing_area_resize_cb); + motion_controller.motion.disconnect (motion_cb); + key_controller.key_pressed.disconnect (key_cb); + primary_click_gesture.pressed.disconnect (primary_pressed_cb); + primary_click_gesture.released.disconnect (primary_released_cb); + secondary_click_gesture.pressed.disconnect (secondary_pressed_cb); + secondary_click_gesture.released.disconnect (secondary_released_cb); + focus_controller.enter.disconnect (focus_cb); + focus_controller.leave.disconnect (focus_cb); adjustment.value_changed.disconnect (scroll_cb); } @@ -289,10 +317,12 @@ public class BookView : Gtk.Box redraw (); } - private bool configure_cb (Gtk.Widget widget, Gdk.EventConfigure event) + public void drawing_area_resize_cb () { need_layout = true; - return false; + // Let's layout ahead of time + // to avoid "Trying to snapshot GtkGizmo without a current allocation" error + layout (); } private void layout_into (int width, int height, out int book_width, out int book_height) @@ -392,7 +422,7 @@ public class BookView : Gtk.Box /* Try and fit without scrollbar */ var width = (int) allocation.width; - var height = (int) (box_allocation.height - get_border_width () * 2); + var height = (int) (box_allocation.height); int book_width, book_height; layout_into (width, height, out book_width, out book_height); @@ -402,17 +432,17 @@ public class BookView : Gtk.Box /* Re-layout leaving space for scrollbar */ height = allocation.height; layout_into (width, height, out book_width, out book_height); - + /* Set scrollbar limits */ adjustment.lower = 0; adjustment.upper = book_width; adjustment.page_size = allocation.width; - + /* Keep right-aligned */ var max_offset = book_width - allocation.width; if (right_aligned || x_offset > max_offset) x_offset = max_offset; - + scroll.visible = true; } else @@ -433,7 +463,7 @@ public class BookView : Gtk.Box laying_out = false; } - private bool draw_cb (Gtk.Widget widget, Cairo.Context context) + public void draw_cb (Gtk.DrawingArea drawing_area, Cairo.Context context, int width, int height) { layout (); @@ -444,7 +474,7 @@ public class BookView : Gtk.Box for (var i = 0; i < book.n_pages; i++) pages.append (get_nth_page (i)); - var ruler_color = get_style_context ().get_color (get_state_flags ()); + var ruler_color = get_style_context ().get_color (); Gdk.RGBA ruler_color_selected = {}; ruler_color_selected.parse("#3584e4"); /* Gnome Blue 3 */ @@ -470,8 +500,6 @@ public class BookView : Gtk.Box page.width, page.height); } - - return false; } private PageView? get_page_at (int x, int y, out int x_, out int y_) @@ -495,65 +523,86 @@ public class BookView : Gtk.Box return null; } - private bool button_cb (Gtk.Widget widget, Gdk.EventButton event) + private void primary_pressed_cb (Gtk.GestureClick controler, int n_press, double x, double y) + { + button_cb(controler, Gdk.BUTTON_PRIMARY, true, n_press, x, y); + } + + private void primary_released_cb (Gtk.GestureClick controler, int n_press, double x, double y) + { + button_cb(controler, Gdk.BUTTON_PRIMARY, false, n_press, x, y); + } + + private void secondary_pressed_cb (Gtk.GestureClick controler, int n_press, double x, double y) + { + button_cb(controler, Gdk.BUTTON_SECONDARY, true, n_press, x, y); + } + + private void secondary_released_cb (Gtk.GestureClick controler, int n_press, double x, double y) + { + button_cb(controler, Gdk.BUTTON_SECONDARY, false, n_press, x, y); + } + + private void button_cb (Gtk.GestureClick controler, int button, bool press, int n_press, double dx, double dy) { layout (); drawing_area.grab_focus (); int x = 0, y = 0; - if (event.type == Gdk.EventType.BUTTON_PRESS) - select_page_view (get_page_at ((int) (event.x + x_offset), (int) event.y, out x, out y)); + if (press) + select_page_view (get_page_at ((int) ((int) dx + x_offset), (int) dy, out x, out y)); if (selected_page_view == null) - return false; + return; /* Modify page */ - if (event.button == 1) + if (button == Gdk.BUTTON_PRIMARY) { - if (event.type == Gdk.EventType.BUTTON_PRESS) + if (press) selected_page_view.button_press (x, y); - else if (event.type == Gdk.EventType.BUTTON_RELEASE) - selected_page_view.button_release (x, y); - else if (event.type == Gdk.EventType.2BUTTON_PRESS) + else if (press && n_press == 2) show_page (selected_page); + else if (!press) + selected_page_view.button_release (x, y); } /* Show pop-up menu on right click */ - if (event.button == 3) - show_menu (event); - - return false; + if (button == Gdk.BUTTON_SECONDARY) + show_menu (drawing_area, dx, dy); } - private void set_cursor (Gdk.CursorType cursor) + private new void set_cursor (string cursor) { - Gdk.Cursor c; - if (this.cursor == cursor) return; this.cursor = cursor; - c = new Gdk.Cursor.for_display (get_display (), cursor); - drawing_area.get_window ().set_cursor (c); + Gdk.Cursor c = new Gdk.Cursor.from_name (cursor, null); + drawing_area.set_cursor (c); } - private bool motion_cb (Gtk.Widget widget, Gdk.EventMotion event) + private void motion_cb (Gtk.EventControllerMotion controler, double dx, double dy) { - Gdk.CursorType cursor = Gdk.CursorType.ARROW; + string cursor = "arrow"; + + int event_x = (int) dx; + int event_y = (int) dy; + + var event_state = controler.get_current_event_state(); /* Dragging */ - if (selected_page_view != null && (event.state & Gdk.ModifierType.BUTTON1_MASK) != 0) + if (selected_page_view != null && (event_state & Gdk.ModifierType.BUTTON1_MASK) != 0) { - var x = (int) (event.x + x_offset - selected_page_view.x_offset); - var y = (int) (event.y - selected_page_view.y_offset); + var x = (int) (event_x + x_offset - selected_page_view.x_offset); + var y = (int) (event_y - selected_page_view.y_offset); selected_page_view.motion (x, y); cursor = selected_page_view.cursor; } else { int x, y; - var over_page = get_page_at ((int) (event.x + x_offset), (int) event.y, out x, out y); + var over_page = get_page_at ((int) (event_x + x_offset), (int) event_y, out x, out y); if (over_page != null) { over_page.motion (x, y); @@ -562,13 +611,11 @@ public class BookView : Gtk.Box } set_cursor (cursor); - - return false; } - private bool key_cb (Gtk.Widget widget, Gdk.EventKey event) + private bool key_cb (Gtk.EventControllerKey controler, uint keyval, uint keycode, Gdk.ModifierType state) { - switch (event.keyval) + switch (keyval) { case 0xff50: /* FIXME: GDK_Home */ selected_page = book.get_page (0); @@ -588,10 +635,9 @@ public class BookView : Gtk.Box } } - private bool focus_cb (Gtk.Widget widget, Gdk.EventFocus event) + private void focus_cb (Gtk.EventControllerFocus controler) { set_selected_page_view (selected_page_view); - return false; } private void scroll_cb (Gtk.Adjustment adjustment) diff --git a/src/book.vala b/src/book.vala index e25eb35..7f8d048 100644 --- a/src/book.vala +++ b/src/book.vala @@ -237,7 +237,8 @@ private class BookSaver encoder = new ThreadPool<EncodeTask>.with_owned_data (encode_delegate, (int) get_num_processors (), false); /* Configure a writer */ - ThreadFunc<Error?>? write_delegate = null; + Thread<Error?> writer; + switch (mime_type) { case "image/jpeg": @@ -245,13 +246,15 @@ private class BookSaver #if HAVE_WEBP case "image/webp": #endif - write_delegate = write_multifile; + writer = new Thread<Error?> (null, write_multifile); break; case "application/pdf": - write_delegate = write_pdf; + writer = new Thread<Error?> (null, write_pdf); + break; + default: + writer = new Thread<Error?> (null, () => null); break; } - var writer = new Thread<Error?> (null, write_delegate); /* Issue encode tasks */ for (var i = 0; i < n_pages; i++) diff --git a/src/drivers-dialog.vala b/src/drivers-dialog.vala new file mode 100644 index 0000000..5a72376 --- /dev/null +++ b/src/drivers-dialog.vala @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2023 Bartłomiej Maryńczak + * Author: Bartłomiej Maryńczak <marynczakbartlomiej@gmail.com>, + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. See http://www.gnu.org/copyleft/gpl.html the full text of the + * license. + */ + +[GtkTemplate (ui = "/org/gnome/SimpleScan/ui/drivers-dialog.ui")] +private class DriversDialog : Gtk.Window +{ + [GtkChild] + private unowned Gtk.Revealer header_revealer; + + [GtkChild] + private unowned Gtk.Label main_label; + [GtkChild] + private unowned Gtk.Label main_sublabel; + + [GtkChild] + private unowned Gtk.Revealer progress_revealer; + [GtkChild] + private unowned Gtk.ProgressBar progress_bar; + + [GtkChild] + private unowned Gtk.Label result_label; + [GtkChild] + private unowned Gtk.Label result_sublabel; + [GtkChild] + private unowned Gtk.Image result_icon; + + [GtkChild] + private unowned Gtk.Stack stack; + + private uint pulse_timer; + private string? missing_driver; + + public DriversDialog (Gtk.Window parent, string? missing_driver) + { + this.missing_driver = missing_driver; + set_transient_for (parent); + } + + ~DriversDialog () { + pulse_stop (); + } + + private void pulse_start () + { + pulse_stop (); + pulse_timer = GLib.Timeout.add(100, () => { + progress_bar.pulse (); + return Source.CONTINUE; + }); + } + + private void pulse_stop () + { + Source.remove (pulse_timer); + } + + public async void open () + { + var message = "", instructions = ""; + string[] packages_to_install = {}; + switch (missing_driver) + { + case "brscan": + case "brscan2": + case "brscan3": + case "brscan4": + /* Message to indicate a Brother scanner has been detected */ + message = _("You appear to have a Brother scanner."); + /* Instructions on how to install Brother scanner drivers */ + instructions = _("Drivers for this are available on the <a href=\"http://support.brother.com\">Brother website</a>."); + break; + case "pixma": + /* Message to indicate a Canon Pixma scanner has been detected */ + message = _("You appear to have a Canon scanner, which is supported by the <a href=\"http://www.sane-project.org/man/sane-pixma.5.html\">Pixma SANE backend</a>."); + /* Instructions on how to resolve issue with SANE scanner drivers */ + instructions = _("Please check if your <a href=\"http://www.sane-project.org/sane-supported-devices.html\">scanner is supported by SANE</a>, otherwise report the issue to the <a href=\"https://alioth-lists.debian.net/cgi-bin/mailman/listinfo/sane-devel\">SANE mailing list</a>."); + break; + case "samsung": + /* Message to indicate a Samsung scanner has been detected */ + message = _("You appear to have a Samsung scanner."); + /* Instructions on how to install Samsung scanner drivers. + Because HP acquired Samsung's global printing business in 2017, the support is made on HP site. */ + instructions = _("Drivers for this are available on the <a href=\"https://support.hp.com\">HP website</a> (HP acquired Samsung's printing business)."); + break; + case "hpaio": + case "smfp": + /* Message to indicate a HP scanner has been detected */ + message = _("You appear to have an HP scanner."); + if (missing_driver == "hpaio") + packages_to_install = { "libsane-hpaio" }; + else + /* Instructions on how to install HP scanner drivers. + smfp is rebranded and slightly modified Samsung devices, + for example: HP Laser MFP 135a is rebranded Samsung Xpress SL-M2070. + It require custom drivers, not available in hpaio package */ + instructions = _("Drivers for this are available on the <a href=\"https://support.hp.com\">HP website</a>."); + break; + case "epkowa": + /* Message to indicate an Epson scanner has been detected */ + message = _("You appear to have an Epson scanner."); + /* Instructions on how to install Epson scanner drivers */ + instructions = _("Drivers for this are available on the <a href=\"http://support.epson.com\">Epson website</a>."); + break; + case "lexmark_nscan": + /* Message to indicate a Lexmark scanner has been detected */ + message = _("You appear to have a Lexmark scanner."); + /* Instructions on how to install Lexmark scanner drivers */ + instructions = _("Drivers for this are available on the <a href=\"http://support.lexmark.com\">Lexmark website</a>."); + break; + } + + main_label.label = message; + main_sublabel.label = instructions; + + if (packages_to_install.length > 0) + { +#if HAVE_PACKAGEKIT + this.progress_revealer.reveal_child = true; + pulse_start(); + + main_sublabel.set_text (/* Label shown while installing drivers */ + _("Installing drivers…")); + + present (); + + /* Label shown once drivers successfully installed */ + var result_text = _("Drivers installed successfully!"); + var success = true; + try + { + var results = yield install_packages(packages_to_install, () => {}); + + if (results.get_error_code () != null) + { + 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); + success = false; + } + } + catch (Error e) + { + /* Label shown if failed to install drivers */ + result_text = _("Failed to install drivers."); + success = false; + warning ("Failed to install drivers: %s", e.message); + } + + result_label.label = result_text; + + if (success) + { + result_sublabel.label = _("Once installed you will need to restart this app."); + result_icon.icon_name = "emblem-ok-symbolic"; + } + else + { + result_sublabel.visible = false; + result_icon.icon_name = "emblem-important-symbolic"; + } + + stack.set_visible_child_name ("result"); + header_revealer.reveal_child = false; + progress_revealer.reveal_child = false; + pulse_stop (); +#else + main_sublabel.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))); + present (); +#endif + } + } + +#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 +} diff --git a/src/meson.build b/src/meson.build index 240b56d..3cafc7c 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,5 +1,5 @@ vala_args = [ '--pkg=posix', '--vapidir=' + meson.current_source_dir () ] -dependencies = [ glib_dep, gtk_dep, libhandy_dep, zlib_dep, cairo_dep, gdk_pixbuf_dep, gusb_dep, sane_dep ] +dependencies = [ glib_dep, gtk_dep, libadwaita_dep, zlib_dep, cairo_dep, gdk_pixbuf_dep, gusb_dep, sane_dep ] if colord_dep.found () vala_args += [ '-D', 'HAVE_COLORD' ] dependencies += colord_dep @@ -19,11 +19,13 @@ simple_scan = executable ('simple-scan', 'authorize-dialog.vala', 'book.vala', 'book-view.vala', + 'drivers-dialog.vala', 'page.vala', - 'page-icon.vala', + 'page-texture.vala', 'page-view.vala', 'postprocessor.vala', 'preferences-dialog.vala', + 'reorder-pages-dialog.vala', 'simple-scan.vala', 'scanner.vala', 'screensaver.vala', diff --git a/src/page-icon.vala b/src/page-icon.vala deleted file mode 100644 index 3e3fa93..0000000 --- a/src/page-icon.vala +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright (C) 2009-2017 Canonical Ltd. - * Author: Robert Ancell <robert.ancell@canonical.com>, - * Eduard Gotwig <g@ox.io> - * - * This program is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation, either version 3 of the License, or (at your option) any later - * version. See http://www.gnu.org/copyleft/gpl.html the full text of the - * license. - */ - -public class PageIcon : Gtk.DrawingArea -{ - private char side; - private int position; - private int angle; - private const int MINIMUM_WIDTH = 20; - - public PageIcon (char side, int position, int angle) - { - this.side = side; - this.position = position; - this.angle = angle; - } - - 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); - - bool dark = Hdy.StyleManager.get_default ().dark; - bool hc = Hdy.StyleManager.get_default ().high_contrast; - - if (dark && !hc) - c.rectangle (1, 1, w - 2, h - 2); - else - c.rectangle (0, 0, w, h); - - Gdk.RGBA rgba = {}; - - switch (side) - { - case 'F': - /* Purple 2 */ - rgba.parse ("#c061cb"); - break; - case 'B': - /* Orange 3 */ - rgba.parse ("#ff7800"); - break; - case 'U': - /* green 4 */ - rgba.parse ("#5cc02e"); - break; - case 'R': - /* blue 4 */ - rgba.parse ("#0deee7"); - break; - default: - /* Yellow 3 to Red 2 */ - Gdk.RGBA start = {}, end = {}; - start.parse ("#f6d32d"); - end.parse ("#ed333b"); - - double progress = position / 5.0; - rgba.red = start.red + (end.red - start.red) * progress; - rgba.green = start.green + (end.green - start.green) * progress; - rgba.blue = start.blue + (end.blue - start.blue) * progress; - break; - } - - rgba.alpha = 0.3; - - Gdk.cairo_set_source_rgba (c, rgba); - c.fill (); - - c.set_line_width (1.0); - if (hc && dark) - c.set_source_rgba (1, 1, 1, 0.5); - else if (hc) - c.set_source_rgba (0, 0, 0, 0.5); - else - c.set_source_rgba (0, 0, 0, 0.15); - - c.rectangle (0.5, 0.5, w - 1, h - 1); - c.stroke (); - - if (dark) - c.set_source_rgb (1, 1, 1); - else - c.set_source_rgb (0, 0, 0); - - var text = @"$(position + 1)"; - Cairo.TextExtents extents; - - var rad = Math.PI / 180.0 * angle; - c.text_extents (text, out extents); - c.translate ((w - extents.width) * 0.5 - 0.5, extents.height + (h - extents.height) * 0.5 - 0.5); - c.rotate(rad); - // only correct for 0 and 180 degree - var tx = (1.0 - Math.sin(rad)) * extents.width / 2; - var ty = (1.0 - Math.sin(rad)) * extents.height / 2; - c.translate(-tx, +ty); - c.show_text (text); - - return true; - } -} diff --git a/src/page-texture.vala b/src/page-texture.vala new file mode 100644 index 0000000..1e55433 --- /dev/null +++ b/src/page-texture.vala @@ -0,0 +1,663 @@ +/* + * Copyright (C) 2009-2015 Canonical Ltd. + * Author: Robert Ancell <robert.ancell@canonical.com>, + * Bartłomiej Maryńczak <marynczakbartlomiej@gmail.com> + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. See http://www.gnu.org/copyleft/gpl.html the full text of the + * license. + */ + +private class PageToPixbuf : Object +{ + /* Image to render at current resolution */ + public Gdk.Pixbuf? pixbuf { get { return pixbuf_; } } + private Gdk.Pixbuf? pixbuf_ = null; + + /* Direction of currently scanned image */ + private ScanDirection scan_direction; + + /* Next scan line to render */ + private int scan_line; + + /* Dimensions of image to generate */ + public int width; + public int height; + + private static uchar get_sample (uchar[] pixels, int offset, int x, int depth, int sample) + { + // FIXME + return 0xFF; + } + + private static void get_pixel (Page page, int x, int y, uchar[] pixel) + { + switch (page.scan_direction) + { + case ScanDirection.TOP_TO_BOTTOM: + break; + case ScanDirection.BOTTOM_TO_TOP: + x = page.scan_width - x - 1; + y = page.scan_height - y - 1; + break; + case ScanDirection.LEFT_TO_RIGHT: + var t = x; + x = page.scan_width - y - 1; + y = t; + break; + case ScanDirection.RIGHT_TO_LEFT: + var t = x; + x = y; + y = page.scan_height - t - 1; + break; + } + + var depth = page.depth; + var n_channels = page.n_channels; + unowned uchar[] pixels = page.get_pixels (); + var offset = page.rowstride * y; + + /* Optimise for 8 bit images */ + if (depth == 8 && n_channels == 3) + { + var o = offset + x * n_channels; + pixel[0] = pixels[o]; + pixel[1] = pixels[o+1]; + pixel[2] = pixels[o+2]; + return; + } + else if (depth == 8 && n_channels == 1) + { + pixel[0] = pixel[1] = pixel[2] = pixels[offset + x]; + return; + } + + /* Optimise for bitmaps */ + else if (depth == 1 && n_channels == 1) + { + var o = offset + (x / 8); + pixel[0] = pixel[1] = pixel[2] = (pixels[o] & (0x80 >> (x % 8))) != 0 ? 0x00 : 0xFF; + return; + } + + /* Optimise for 2 bit images */ + else if (depth == 2 && n_channels == 1) + { + int block_shift[4] = { 6, 4, 2, 0 }; + + var o = offset + (x / 4); + var sample = (pixels[o] >> block_shift[x % 4]) & 0x3; + sample = sample * 255 / 3; + + pixel[0] = pixel[1] = pixel[2] = (uchar) sample; + return; + } + + /* Use slow method */ + pixel[0] = get_sample (pixels, offset, x, depth, x * n_channels); + pixel[1] = get_sample (pixels, offset, x, depth, x * n_channels + 1); + pixel[2] = get_sample (pixels, offset, x, depth, x * n_channels + 2); + } + + private static void set_pixel (Page page, double l, double r, double t, double b, uchar[] output, int offset) + { + /* Decimation: + * + * Target pixel is defined by (t,l)-(b,r) + * It touches 16 pixels in original image + * It completely covers 4 pixels in original image (T,L)-(B,R) + * Add covered pixels and add weighted partially covered pixels. + * Divide by total area. + * + * l L R r + * +-----+-----+-----+-----+ + * | | | | | + * t | +--+-----+-----+---+ | + * T +--+--+-----+-----+---+-+ + * | | | | | | | + * | | | | | | | + * +--+--+-----+-----+---+-+ + * | | | | | | | + * | | | | | | | + * B +--+--+-----+-----+---+-+ + * | | | | | | | + * b | +--+-----+-----+---+ | + * +-----+-----+-----+-----+ + * + * + * Interpolation: + * + * l r + * +-----+-----+-----+-----+ + * | | | | | + * | | | | | + * +-----+-----+-----+-----+ + * t | | +-+--+ | | + * | | | | | | | + * +-----+---+-+--+--+-----+ + * b | | +-+--+ | | + * | | | | | + * +-----+-----+-----+-----+ + * | | | | | + * | | | | | + * +-----+-----+-----+-----+ + * + * Same again, just no completely covered pixels. + */ + + var L = (int) l; + if (L != l) + L++; + var R = (int) r; + var T = (int) t; + if (T != t) + T++; + var B = (int) b; + + var red = 0.0; + var green = 0.0; + var blue = 0.0; + + /* Target can fit inside one source pixel + * +-----+ + * | | + * | +--+| +-----+-----+ +-----+ +-----+ +-----+ + * +-+--++ or | +-++ | or | +-+ | or | +--+| or | +--+ + * | +--+| | +-++ | | +-+ | | | || | | | + * | | +-----+-----+ +-----+ +-+--++ +--+--+ + * +-----+ + */ + if ((r - l <= 1.0 && (int)r == (int)l) || (b - t <= 1.0 && (int)b == (int)t)) + { + /* Inside */ + if ((int)l == (int)r || (int)t == (int)b) + { + uchar p[3]; + get_pixel (page, (int)l, (int)t, p); + output[offset] = p[0]; + output[offset+1] = p[1]; + output[offset+2] = p[2]; + return; + } + + /* Stradling horizontal edge */ + if (L > R) + { + uchar p[3]; + get_pixel (page, R, T-1, p); + red += p[0] * (r-l)*(T-t); + green += p[1] * (r-l)*(T-t); + blue += p[2] * (r-l)*(T-t); + for (var y = T; y < B; y++) + { + get_pixel (page, R, y, p); + red += p[0] * (r-l); + green += p[1] * (r-l); + blue += p[2] * (r-l); + } + get_pixel (page, R, B, p); + red += p[0] * (r-l)*(b-B); + green += p[1] * (r-l)*(b-B); + blue += p[2] * (r-l)*(b-B); + } + /* Stradling vertical edge */ + else + { + uchar p[3]; + get_pixel (page, L - 1, B, p); + red += p[0] * (b-t)*(L-l); + green += p[1] * (b-t)*(L-l); + blue += p[2] * (b-t)*(L-l); + for (var x = L; x < R; x++) { + get_pixel (page, x, B, p); + red += p[0] * (b-t); + green += p[1] * (b-t); + blue += p[2] * (b-t); + } + get_pixel (page, R, B, p); + red += p[0] * (b-t)*(r-R); + green += p[1] * (b-t)*(r-R); + blue += p[2] * (b-t)*(r-R); + } + + var scale = 1.0 / ((r - l) * (b - t)); + output[offset] = (uchar)(red * scale + 0.5); + output[offset+1] = (uchar)(green * scale + 0.5); + output[offset+2] = (uchar)(blue * scale + 0.5); + return; + } + + /* Add the middle pixels */ + for (var x = L; x < R; x++) + { + for (var y = T; y < B; y++) + { + uchar p[3]; + get_pixel (page, x, y, p); + red += p[0]; + green += p[1]; + blue += p[2]; + } + } + + /* Add the weighted top and bottom pixels */ + for (var x = L; x < R; x++) + { + if (t != T) + { + uchar p[3]; + get_pixel (page, x, T - 1, p); + red += p[0] * (T - t); + green += p[1] * (T - t); + blue += p[2] * (T - t); + } + + if (b != B) + { + uchar p[3]; + get_pixel (page, x, B, p); + red += p[0] * (b - B); + green += p[1] * (b - B); + blue += p[2] * (b - B); + } + } + + /* Add the left and right pixels */ + for (var y = T; y < B; y++) + { + if (l != L) + { + uchar p[3]; + get_pixel (page, L - 1, y, p); + red += p[0] * (L - l); + green += p[1] * (L - l); + blue += p[2] * (L - l); + } + + if (r != R) + { + uchar p[3]; + get_pixel (page, R, y, p); + red += p[0] * (r - R); + green += p[1] * (r - R); + blue += p[2] * (r - R); + } + } + + /* Add the corner pixels */ + if (l != L && t != T) + { + uchar p[3]; + get_pixel (page, L - 1, T - 1, p); + red += p[0] * (L - l)*(T - t); + green += p[1] * (L - l)*(T - t); + blue += p[2] * (L - l)*(T - t); + } + if (r != R && t != T) + { + uchar p[3]; + get_pixel (page, R, T - 1, p); + red += p[0] * (r - R)*(T - t); + green += p[1] * (r - R)*(T - t); + blue += p[2] * (r - R)*(T - t); + } + if (r != R && b != B) + { + uchar p[3]; + get_pixel (page, R, B, p); + red += p[0] * (r - R)*(b - B); + green += p[1] * (r - R)*(b - B); + blue += p[2] * (r - R)*(b - B); + } + if (l != L && b != B) + { + uchar p[3]; + get_pixel (page, L - 1, B, p); + red += p[0] * (L - l)*(b - B); + green += p[1] * (L - l)*(b - B); + blue += p[2] * (L - l)*(b - B); + } + + /* Scale pixel values and clamp in range [0, 255] */ + var scale = 1.0 / ((r - l) * (b - t)); + output[offset] = (uchar)(red * scale + 0.5); + output[offset+1] = (uchar)(green * scale + 0.5); + output[offset+2] = (uchar)(blue * scale + 0.5); + } + + public static void update_preview (Page page, ref Gdk.Pixbuf? output_image, int output_width, int output_height, + ScanDirection scan_direction, int old_scan_line, int scan_line) + { + var input_width = page.width; + var input_height = page.height; + + /* Create new image if one does not exist or has changed size */ + int L, R, T, B; + if (output_image == null || + output_image.width != output_width || + output_image.height != output_height) + { + output_image = new Gdk.Pixbuf (Gdk.Colorspace.RGB, + false, + 8, + output_width, + output_height); + + /* Update entire image */ + L = 0; + R = output_width - 1; + T = 0; + B = output_height - 1; + } + /* Otherwise only update changed area */ + else + { + switch (scan_direction) + { + case ScanDirection.TOP_TO_BOTTOM: + L = 0; + R = output_width - 1; + T = (int)((double)old_scan_line * output_height / input_height); + B = (int)((double)scan_line * output_height / input_height + 0.5); + break; + case ScanDirection.LEFT_TO_RIGHT: + L = (int)((double)old_scan_line * output_width / input_width); + R = (int)((double)scan_line * output_width / input_width + 0.5); + T = 0; + B = output_height - 1; + break; + case ScanDirection.BOTTOM_TO_TOP: + L = 0; + R = output_width - 1; + T = (int)((double)(input_height - scan_line) * output_height / input_height); + B = (int)((double)(input_height - old_scan_line) * output_height / input_height + 0.5); + break; + case ScanDirection.RIGHT_TO_LEFT: + L = (int)((double)(input_width - scan_line) * output_width / input_width); + R = (int)((double)(input_width - old_scan_line) * output_width / input_width + 0.5); + T = 0; + B = output_height - 1; + break; + default: + L = R = B = T = 0; + break; + } + } + + /* FIXME: There's an off by one error in there somewhere... */ + if (R >= output_width) + R = output_width - 1; + if (B >= output_height) + B = output_height - 1; + + return_if_fail (L >= 0); + return_if_fail (R < output_width); + return_if_fail (T >= 0); + return_if_fail (B < output_height); + return_if_fail (output_image != null); + + unowned uchar[] output = output_image.get_pixels (); + var output_rowstride = output_image.rowstride; + var output_n_channels = output_image.n_channels; + + if (!page.has_data) + { + for (var x = L; x <= R; x++) + for (var y = T; y <= B; y++) + { + var o = output_rowstride * y + x * output_n_channels; + output[o] = output[o+1] = output[o+2] = 0xFF; + } + return; + } + + /* Update changed area */ + for (var x = L; x <= R; x++) + { + var l = (double)x * input_width / output_width; + var r = (double)(x + 1) * input_width / output_width; + + for (var y = T; y <= B; y++) + { + var t = (double)y * input_height / output_height; + var b = (double)(y + 1) * input_height / output_height; + + set_pixel (page, + l, r, t, b, + output, output_rowstride * y + x * output_n_channels); + } + } + } + + public void update (Page page) + { + var old_scan_line = scan_line; + scan_line = page.scan_line; + + /* Delete old image if scan direction changed */ + var left_steps = scan_direction - page.scan_direction; + if (left_steps != 0 && pixbuf_ != null) + pixbuf_ = null; + scan_direction = page.scan_direction; + + update_preview (page, + ref pixbuf_, + width, + height, + page.scan_direction, + old_scan_line, + scan_line); + } +} + +/** + * Just update texture contents + */ +private class TextureUpdateTask +{ + public Page page; +} + +/** + * Resize the texture + */ +private class TextureResizeTask: TextureUpdateTask +{ + public int width { get; private set; } + public int height { get; private set; } + + public TextureResizeTask (int width, int height) + { + this.width = width; + this.height = height; + } +} + +public class PageViewTexture : Object +{ + public Gdk.Pixbuf? pixbuf { get; private set; } + public signal void new_buffer (); + + private int requested_width; + private int requested_height; + private TextureUpdateTask queued = null; + + private bool in_proggres; + + private ThreadPool<TextureUpdateTask> resize_pool; + + private Page page; + + public PageViewTexture (Page page) + { + this.page = page; + + try { + resize_pool = new ThreadPool<TextureUpdateTask>.with_owned_data (thread_func, 1, false); + } + catch (ThreadError error) + { + // Pool is non-exclusive so this should never happen + } + } + + /** + * Notify that data needs updating (eg. pixels changed during scanning process) + */ + public void request_update () + { + queued = new TextureUpdateTask (); + } + + /** + * Set size of the page, ignored if size did not change. + */ + public void request_resize (int width, int height) + { + if (requested_width == width && requested_height == height) + { + return; + } + + requested_width = width; + requested_height = height; + + queued = new TextureResizeTask (requested_width, requested_height); + } + + public void queue_update () throws ThreadError + { + if (in_proggres || queued == null) + { + return; + } + + in_proggres = true; + + // We copy the page as it will be sent to resize thread + queued.page = page.copy (); + resize_pool.add (queued); + + queued = null; + } + + private PageToPixbuf page_view = new PageToPixbuf (); + private void thread_func(owned TextureUpdateTask task) + { + if (task is TextureResizeTask) + { + page_view.width = task.width; + page_view.height = task.height; + } + + page_view.update (task.page); + + Gdk.Pixbuf? new_pixbuf = null; + if (page_view.pixbuf != null) + { + // We are sending this buffer back to main thread, therefore copy + new_pixbuf = page_view.pixbuf.copy (); + } + + + Idle.add(() => { + new_pixbuf_cb (new_pixbuf); + return false; + }); + } + + private void new_pixbuf_cb (Gdk.Pixbuf? pixbuf) + { + in_proggres = false; + this.pixbuf = pixbuf; + new_buffer (); + } +} + +public class PagePaintable: Gdk.Paintable, Object +{ + private Page page; + private PageViewTexture page_texture; + private Gdk.Texture? texture; + + public PagePaintable (Page page) + { + this.page = page; + page.pixels_changed.connect (pixels_changed); + page.size_changed.connect (pixels_changed); + page.scan_direction_changed.connect (pixels_changed); + + page_texture = new PageViewTexture (page); + page_texture.new_buffer.connect (texture_updated); + + pixels_changed (); + } + + ~PagePaintable () + { + page.pixels_changed.disconnect (pixels_changed); + page.size_changed.disconnect (pixels_changed); + page.scan_direction_changed.disconnect (pixels_changed); + page_texture.new_buffer.disconnect (texture_updated); + } + + private void pixels_changed () + { + page_texture.request_update (); + try { + page_texture.queue_update (); + } + catch (Error e) + { + warning ("Failed to queue_update of the texture: %s", e.message); + invalidate_contents (); + } + } + + private void texture_updated () + { + if (page_texture.pixbuf != null) + texture = Gdk.Texture.for_pixbuf(page_texture.pixbuf); + else + texture = null; + + invalidate_contents (); + } + + public override double get_intrinsic_aspect_ratio () + { + return (double) page.width / (double) page.height; + } + + public void snapshot (Gdk.Snapshot gdk_snapshot, double width, double height) { + var snapshot = (Gtk.Snapshot) gdk_snapshot; + + var rect = Graphene.Rect(); + rect.size.width = (float) width; + rect.size.height = (float) height; + + page_texture.request_resize ((int) width, (int) height); + + try { + page_texture.queue_update (); + } + catch (Error e) + { + warning ("Failed to queue_update of the texture: %s", e.message); + // Ask for another redraw + invalidate_contents (); + } + + if (texture != null) + { + snapshot.append_texture(texture, rect); + } + else + { + snapshot.append_color ({1.0f, 1.0f, 1.0f, 1.0f}, rect); + } + } + +}
\ No newline at end of file diff --git a/src/page-view.vala b/src/page-view.vala index 148dcca..9ef83de 100644 --- a/src/page-view.vala +++ b/src/page-view.vala @@ -29,7 +29,7 @@ public class PageView : Object public Page page { get; private set; } /* Image to render at current resolution */ - private Gdk.Pixbuf? image = null; + private PageViewTexture page_texture; /* Border around image */ private bool selected_ = false; @@ -52,12 +52,6 @@ public class PageView : Object /* True if image needs to be regenerated */ private bool update_image = true; - /* Direction of currently scanned image */ - private ScanDirection scan_direction; - - /* Next scan line to render */ - private int scan_line; - /* Dimensions of image to generate */ private int width_; private int height_; @@ -75,7 +69,7 @@ public class PageView : Object private int selected_crop_h; /* Cursor over this page */ - public Gdk.CursorType cursor { get; private set; default = Gdk.CursorType.ARROW; } + public string cursor { get; private set; default = "arrow"; } private int animate_n_segments = 7; private int animate_segment; @@ -92,6 +86,9 @@ public class PageView : Object page.crop_changed.connect (page_overlay_changed_cb); page.scan_line_changed.connect (page_overlay_changed_cb); page.scan_direction_changed.connect (scan_direction_changed_cb); + + page_texture = new PageViewTexture(page); + page_texture.new_buffer.connect (new_buffer_cb); } ~PageView () @@ -101,411 +98,13 @@ public class PageView : Object page.crop_changed.disconnect (page_overlay_changed_cb); page.scan_line_changed.disconnect (page_overlay_changed_cb); page.scan_direction_changed.disconnect (scan_direction_changed_cb); - } - - private uchar get_sample (uchar[] pixels, int offset, int x, int depth, int sample) - { - // FIXME - return 0xFF; - } - - private void get_pixel (Page page, int x, int y, uchar[] pixel) - { - switch (page.scan_direction) - { - case ScanDirection.TOP_TO_BOTTOM: - break; - case ScanDirection.BOTTOM_TO_TOP: - x = page.scan_width - x - 1; - y = page.scan_height - y - 1; - break; - case ScanDirection.LEFT_TO_RIGHT: - var t = x; - x = page.scan_width - y - 1; - y = t; - break; - case ScanDirection.RIGHT_TO_LEFT: - var t = x; - x = y; - y = page.scan_height - t - 1; - break; - } - - var depth = page.depth; - var n_channels = page.n_channels; - unowned uchar[] pixels = page.get_pixels (); - var offset = page.rowstride * y; - - /* Optimise for 8 bit images */ - if (depth == 8 && n_channels == 3) - { - var o = offset + x * n_channels; - pixel[0] = pixels[o]; - pixel[1] = pixels[o+1]; - pixel[2] = pixels[o+2]; - return; - } - else if (depth == 8 && n_channels == 1) - { - pixel[0] = pixel[1] = pixel[2] = pixels[offset + x]; - return; - } - - /* Optimise for bitmaps */ - else if (depth == 1 && n_channels == 1) - { - var o = offset + (x / 8); - pixel[0] = pixel[1] = pixel[2] = (pixels[o] & (0x80 >> (x % 8))) != 0 ? 0x00 : 0xFF; - return; - } - - /* Optimise for 2 bit images */ - else if (depth == 2 && n_channels == 1) - { - int block_shift[4] = { 6, 4, 2, 0 }; - - var o = offset + (x / 4); - var sample = (pixels[o] >> block_shift[x % 4]) & 0x3; - sample = sample * 255 / 3; - - pixel[0] = pixel[1] = pixel[2] = (uchar) sample; - return; - } - - /* Use slow method */ - pixel[0] = get_sample (pixels, offset, x, depth, x * n_channels); - pixel[1] = get_sample (pixels, offset, x, depth, x * n_channels + 1); - pixel[2] = get_sample (pixels, offset, x, depth, x * n_channels + 2); - } - - private void set_pixel (Page page, double l, double r, double t, double b, uchar[] output, int offset) - { - /* Decimation: - * - * Target pixel is defined by (t,l)-(b,r) - * It touches 16 pixels in original image - * It completely covers 4 pixels in original image (T,L)-(B,R) - * Add covered pixels and add weighted partially covered pixels. - * Divide by total area. - * - * l L R r - * +-----+-----+-----+-----+ - * | | | | | - * t | +--+-----+-----+---+ | - * T +--+--+-----+-----+---+-+ - * | | | | | | | - * | | | | | | | - * +--+--+-----+-----+---+-+ - * | | | | | | | - * | | | | | | | - * B +--+--+-----+-----+---+-+ - * | | | | | | | - * b | +--+-----+-----+---+ | - * +-----+-----+-----+-----+ - * - * - * Interpolation: - * - * l r - * +-----+-----+-----+-----+ - * | | | | | - * | | | | | - * +-----+-----+-----+-----+ - * t | | +-+--+ | | - * | | | | | | | - * +-----+---+-+--+--+-----+ - * b | | +-+--+ | | - * | | | | | - * +-----+-----+-----+-----+ - * | | | | | - * | | | | | - * +-----+-----+-----+-----+ - * - * Same again, just no completely covered pixels. - */ - - var L = (int) l; - if (L != l) - L++; - var R = (int) r; - var T = (int) t; - if (T != t) - T++; - var B = (int) b; - - var red = 0.0; - var green = 0.0; - var blue = 0.0; - - /* Target can fit inside one source pixel - * +-----+ - * | | - * | +--+| +-----+-----+ +-----+ +-----+ +-----+ - * +-+--++ or | +-++ | or | +-+ | or | +--+| or | +--+ - * | +--+| | +-++ | | +-+ | | | || | | | - * | | +-----+-----+ +-----+ +-+--++ +--+--+ - * +-----+ - */ - if ((r - l <= 1.0 && (int)r == (int)l) || (b - t <= 1.0 && (int)b == (int)t)) - { - /* Inside */ - if ((int)l == (int)r || (int)t == (int)b) - { - uchar p[3]; - get_pixel (page, (int)l, (int)t, p); - output[offset] = p[0]; - output[offset+1] = p[1]; - output[offset+2] = p[2]; - return; - } - - /* Stradling horizontal edge */ - if (L > R) - { - uchar p[3]; - get_pixel (page, R, T-1, p); - red += p[0] * (r-l)*(T-t); - green += p[1] * (r-l)*(T-t); - blue += p[2] * (r-l)*(T-t); - for (var y = T; y < B; y++) - { - get_pixel (page, R, y, p); - red += p[0] * (r-l); - green += p[1] * (r-l); - blue += p[2] * (r-l); - } - get_pixel (page, R, B, p); - red += p[0] * (r-l)*(b-B); - green += p[1] * (r-l)*(b-B); - blue += p[2] * (r-l)*(b-B); - } - /* Stradling vertical edge */ - else - { - uchar p[3]; - get_pixel (page, L - 1, B, p); - red += p[0] * (b-t)*(L-l); - green += p[1] * (b-t)*(L-l); - blue += p[2] * (b-t)*(L-l); - for (var x = L; x < R; x++) { - get_pixel (page, x, B, p); - red += p[0] * (b-t); - green += p[1] * (b-t); - blue += p[2] * (b-t); - } - get_pixel (page, R, B, p); - red += p[0] * (b-t)*(r-R); - green += p[1] * (b-t)*(r-R); - blue += p[2] * (b-t)*(r-R); - } - - var scale = 1.0 / ((r - l) * (b - t)); - output[offset] = (uchar)(red * scale + 0.5); - output[offset+1] = (uchar)(green * scale + 0.5); - output[offset+2] = (uchar)(blue * scale + 0.5); - return; - } - - /* Add the middle pixels */ - for (var x = L; x < R; x++) - { - for (var y = T; y < B; y++) - { - uchar p[3]; - get_pixel (page, x, y, p); - red += p[0]; - green += p[1]; - blue += p[2]; - } - } - - /* Add the weighted top and bottom pixels */ - for (var x = L; x < R; x++) - { - if (t != T) - { - uchar p[3]; - get_pixel (page, x, T - 1, p); - red += p[0] * (T - t); - green += p[1] * (T - t); - blue += p[2] * (T - t); - } - - if (b != B) - { - uchar p[3]; - get_pixel (page, x, B, p); - red += p[0] * (b - B); - green += p[1] * (b - B); - blue += p[2] * (b - B); - } - } - - /* Add the left and right pixels */ - for (var y = T; y < B; y++) - { - if (l != L) - { - uchar p[3]; - get_pixel (page, L - 1, y, p); - red += p[0] * (L - l); - green += p[1] * (L - l); - blue += p[2] * (L - l); - } - - if (r != R) - { - uchar p[3]; - get_pixel (page, R, y, p); - red += p[0] * (r - R); - green += p[1] * (r - R); - blue += p[2] * (r - R); - } - } - /* Add the corner pixels */ - if (l != L && t != T) - { - uchar p[3]; - get_pixel (page, L - 1, T - 1, p); - red += p[0] * (L - l)*(T - t); - green += p[1] * (L - l)*(T - t); - blue += p[2] * (L - l)*(T - t); - } - if (r != R && t != T) - { - uchar p[3]; - get_pixel (page, R, T - 1, p); - red += p[0] * (r - R)*(T - t); - green += p[1] * (r - R)*(T - t); - blue += p[2] * (r - R)*(T - t); - } - if (r != R && b != B) - { - uchar p[3]; - get_pixel (page, R, B, p); - red += p[0] * (r - R)*(b - B); - green += p[1] * (r - R)*(b - B); - blue += p[2] * (r - R)*(b - B); - } - if (l != L && b != B) - { - uchar p[3]; - get_pixel (page, L - 1, B, p); - red += p[0] * (L - l)*(b - B); - green += p[1] * (L - l)*(b - B); - blue += p[2] * (L - l)*(b - B); - } - - /* Scale pixel values and clamp in range [0, 255] */ - var scale = 1.0 / ((r - l) * (b - t)); - output[offset] = (uchar)(red * scale + 0.5); - output[offset+1] = (uchar)(green * scale + 0.5); - output[offset+2] = (uchar)(blue * scale + 0.5); + page_texture.new_buffer.disconnect (new_buffer_cb); } - - private void update_preview (Page page, ref Gdk.Pixbuf? output_image, int output_width, int output_height, - ScanDirection scan_direction, int old_scan_line, int scan_line) + + private void new_buffer_cb() { - var input_width = page.width; - var input_height = page.height; - - /* Create new image if one does not exist or has changed size */ - int L, R, T, B; - if (output_image == null || - output_image.width != output_width || - output_image.height != output_height) - { - output_image = new Gdk.Pixbuf (Gdk.Colorspace.RGB, - false, - 8, - output_width, - output_height); - - /* Update entire image */ - L = 0; - R = output_width - 1; - T = 0; - B = output_height - 1; - } - /* Otherwise only update changed area */ - else - { - switch (scan_direction) - { - case ScanDirection.TOP_TO_BOTTOM: - L = 0; - R = output_width - 1; - T = (int)((double)old_scan_line * output_height / input_height); - B = (int)((double)scan_line * output_height / input_height + 0.5); - break; - case ScanDirection.LEFT_TO_RIGHT: - L = (int)((double)old_scan_line * output_width / input_width); - R = (int)((double)scan_line * output_width / input_width + 0.5); - T = 0; - B = output_height - 1; - break; - case ScanDirection.BOTTOM_TO_TOP: - L = 0; - R = output_width - 1; - T = (int)((double)(input_height - scan_line) * output_height / input_height); - B = (int)((double)(input_height - old_scan_line) * output_height / input_height + 0.5); - break; - case ScanDirection.RIGHT_TO_LEFT: - L = (int)((double)(input_width - scan_line) * output_width / input_width); - R = (int)((double)(input_width - old_scan_line) * output_width / input_width + 0.5); - T = 0; - B = output_height - 1; - break; - default: - L = R = B = T = 0; - break; - } - } - - /* FIXME: There's an off by one error in there somewhere... */ - if (R >= output_width) - R = output_width - 1; - if (B >= output_height) - B = output_height - 1; - - return_if_fail (L >= 0); - return_if_fail (R < output_width); - return_if_fail (T >= 0); - return_if_fail (B < output_height); - return_if_fail (output_image != null); - - unowned uchar[] output = output_image.get_pixels (); - var output_rowstride = output_image.rowstride; - var output_n_channels = output_image.n_channels; - - if (!page.has_data) - { - for (var x = L; x <= R; x++) - for (var y = T; y <= B; y++) - { - var o = output_rowstride * y + x * output_n_channels; - output[o] = output[o+1] = output[o+2] = 0xFF; - } - return; - } - - /* Update changed area */ - for (var x = L; x <= R; x++) - { - var l = (double)x * input_width / output_width; - var r = (double)(x + 1) * input_width / output_width; - - for (var y = T; y <= B; y++) - { - var t = (double)y * input_height / output_height; - var b = (double)(y + 1) * input_height / output_height; - - set_pixel (page, - l, r, t, b, - output, output_rowstride * y + x * output_n_channels); - } - } + changed (); } private int get_preview_width () @@ -518,30 +117,6 @@ public class PageView : Object return height_ - (border_width + ruler_width) * 2; } - private void update_page_view () - { - if (!update_image) - return; - - var old_scan_line = scan_line; - var scan_line = page.scan_line; - - /* Delete old image if scan direction changed */ - var left_steps = scan_direction - page.scan_direction; - if (left_steps != 0 && image != null) - image = null; - scan_direction = page.scan_direction; - - update_preview (page, - ref image, - get_preview_width (), - get_preview_height (), - page.scan_direction, old_scan_line, scan_line); - - update_image = false; - this.scan_line = scan_line; - } - private int page_to_screen_x (int x) { return (int) ((double)x * get_preview_width () / page.width + 0.5); @@ -642,38 +217,39 @@ public class PageView : Object public void motion (int x, int y) { var location = get_crop_location (x, y); - Gdk.CursorType cursor; + + string cursor; switch (location) { case CropLocation.MIDDLE: - cursor = Gdk.CursorType.HAND1; + cursor = "hand1"; break; case CropLocation.TOP: - cursor = Gdk.CursorType.TOP_SIDE; + cursor = "top_side"; break; case CropLocation.BOTTOM: - cursor = Gdk.CursorType.BOTTOM_SIDE; + cursor = "bottom_side"; break; case CropLocation.LEFT: - cursor = Gdk.CursorType.LEFT_SIDE; + cursor = "left_side"; break; case CropLocation.RIGHT: - cursor = Gdk.CursorType.RIGHT_SIDE; + cursor = "right_side"; break; case CropLocation.TOP_LEFT: - cursor = Gdk.CursorType.TOP_LEFT_CORNER; + cursor = "top_left_corner"; break; case CropLocation.TOP_RIGHT: - cursor = Gdk.CursorType.TOP_RIGHT_CORNER; + cursor = "top_right_corner"; break; case CropLocation.BOTTOM_LEFT: - cursor = Gdk.CursorType.BOTTOM_LEFT_CORNER; + cursor = "bottom_left_corner"; break; case CropLocation.BOTTOM_RIGHT: - cursor = Gdk.CursorType.BOTTOM_RIGHT_CORNER; + cursor = "bottom_right_corner"; break; default: - cursor = Gdk.CursorType.ARROW; + cursor = "arrow"; break; } @@ -828,7 +404,18 @@ public class PageView : Object public void render (Cairo.Context context, Gdk.RGBA ruler_color) { update_animation (); - update_page_view (); + + page_texture.request_resize (get_preview_width (), get_preview_height ()); + + try { + page_texture.queue_update (); + } + catch (Error e) + { + warning ("Failed to queue_update of the texture: %s", e.message); + // Ask for another redraw + changed (); + } var w = get_preview_width (); var h = get_preview_height (); @@ -838,8 +425,26 @@ public class PageView : Object /* Draw image */ context.translate (border_width + ruler_width, border_width + ruler_width); - Gdk.cairo_set_source_pixbuf (context, image, 0, 0); - context.paint (); + + if (page_texture.pixbuf != null) + { + float x_scale = (float) w / (float) page_texture.pixbuf.width; + float y_scale = (float) h / (float) page_texture.pixbuf.height; + + context.save (); + context.scale(x_scale, y_scale); + + // context.rectangle (0, 0.0, w, h); + Gdk.cairo_set_source_pixbuf (context, page_texture.pixbuf, 0, 0); + context.paint (); + context.restore (); + } + else + { + Gdk.cairo_set_source_rgba (context, {1.0f, 1.0f, 1.0f, 1.0f}); + context.rectangle (0, 0.0, w, h); + context.fill (); + } /* Draw page border */ Gdk.cairo_set_source_rgba (context, ruler_color); @@ -1037,6 +642,7 @@ public class PageView : Object { /* Regenerate image */ update_image = true; + page_texture.request_update (); changed (); } @@ -1057,6 +663,7 @@ public class PageView : Object { /* Regenerate image */ update_image = true; + page_texture.request_update (); size_changed (); changed (); } diff --git a/src/page.vala b/src/page.vala index cfe70e1..67c8010 100644 --- a/src/page.vala +++ b/src/page.vala @@ -219,6 +219,31 @@ public class Page : Object this.crop_width = (crop_x + crop_width > scan_width) ? scan_width : crop_width; this.crop_height = (crop_y + crop_height > scan_height) ? scan_height : crop_height; } + + public Page copy() + { + var copy = new Page.from_data ( + scan_width, + scan_height, + rowstride, + n_channels, + depth, + dpi, + scan_direction, + color_profile, + pixels, + has_crop, + crop_name, + crop_x, + crop_y, + crop_width, + crop_height + ); + + copy.scan_line = scan_line; + + return copy; + } public void set_page_info (ScanPageInfo info) { @@ -649,10 +674,9 @@ public class Page : Object 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 clipboard = window.get_clipboard(); var image = get_image (true); - clipboard.set_image (image); + clipboard.set_value (image); } public void save_png (File file) throws Error diff --git a/src/preferences-dialog.vala b/src/preferences-dialog.vala index 02bbaf8..07fcf42 100644 --- a/src/preferences-dialog.vala +++ b/src/preferences-dialog.vala @@ -10,48 +10,75 @@ * license. */ +private class DpiItem: Object +{ + public int dpi; + public string label; + + public DpiItem(int dpi, string label) + { + this.dpi = dpi; + this.label = label; + } +} + +private class PaperSizeItem: Object +{ + public string label; + public int width; + public int height; + + public PaperSizeItem(string label, int width, int height) + { + this.label = label; + this.width = width; + this.height = height; + } +} + [GtkTemplate (ui = "/org/gnome/SimpleScan/ui/preferences-dialog.ui")] -private class PreferencesDialog : Hdy.PreferencesWindow +private class PreferencesDialog : Adw.PreferencesWindow { private Settings settings; [GtkChild] - private unowned Gtk.ComboBox text_dpi_combo; + private unowned Adw.ComboRow text_dpi_row; [GtkChild] - private unowned Gtk.ComboBox photo_dpi_combo; + private unowned Adw.ComboRow photo_dpi_row; [GtkChild] - private unowned Gtk.ComboBox paper_size_combo; + private unowned Adw.ComboRow paper_size_row; [GtkChild] private unowned Gtk.Scale brightness_scale; [GtkChild] private unowned Gtk.Scale contrast_scale; [GtkChild] - private unowned Gtk.RadioButton page_delay_0s_button; - [GtkChild] - private unowned Gtk.RadioButton page_delay_3s_button; - [GtkChild] - private unowned Gtk.RadioButton page_delay_6s_button; + private unowned Gtk.Scale compression_scale; [GtkChild] - private unowned Gtk.RadioButton page_delay_10s_button; + private unowned Gtk.ToggleButton page_delay_0s_button; [GtkChild] - private unowned Gtk.RadioButton page_delay_15s_button; + private unowned Gtk.ToggleButton page_delay_3s_button; [GtkChild] - private unowned Gtk.ListStore text_dpi_model; + private unowned Gtk.ToggleButton page_delay_6s_button; [GtkChild] - private unowned Gtk.ListStore photo_dpi_model; + private unowned Gtk.ToggleButton page_delay_10s_button; [GtkChild] - private unowned Gtk.RadioButton front_side_button; + private unowned Gtk.ToggleButton page_delay_15s_button; + private ListStore text_dpi_model; + private ListStore photo_dpi_model; [GtkChild] - private unowned Gtk.RadioButton back_side_button; + private unowned Gtk.ToggleButton front_side_button; [GtkChild] - private unowned Gtk.RadioButton both_side_button; + private unowned Gtk.ToggleButton back_side_button; [GtkChild] - private unowned Gtk.ListStore paper_size_model; + private unowned Gtk.ToggleButton both_side_button; + private ListStore paper_size_model; [GtkChild] private unowned Gtk.Adjustment brightness_adjustment; [GtkChild] private unowned Gtk.Adjustment contrast_adjustment; [GtkChild] + private unowned Gtk.Adjustment compression_adjustment; + [GtkChild] private unowned Gtk.Switch postproc_enable_switch; [GtkChild] private unowned Gtk.Entry postproc_script_entry; @@ -60,50 +87,77 @@ private class PreferencesDialog : Hdy.PreferencesWindow [GtkChild] private unowned Gtk.Switch postproc_keep_original_switch; + static string get_dpi_label (DpiItem device) { + return device.label; + } + + static string get_page_size_label (PaperSizeItem size) { + return size.label; + } + public PreferencesDialog (Settings settings) { 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, 2970, 1, 4200, 2, "A3", -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); + paper_size_row.expression = new Gtk.CClosureExpression ( + typeof (string), + null, + {}, + (Callback) get_page_size_label, + null, + null + ); + + paper_size_model = new ListStore (typeof (PaperSizeItem)); + /* Combo box value for automatic paper size */ + paper_size_model.append (new PaperSizeItem (_("Automatic"), 0, 0)); + paper_size_model.append (new PaperSizeItem ("A6", 1050, 1480)); + paper_size_model.append (new PaperSizeItem ("A5", 1480, 2100)); + paper_size_model.append (new PaperSizeItem ("A4", 2100, 2970)); + paper_size_model.append (new PaperSizeItem ("A3", 2970, 4200)); + paper_size_model.append (new PaperSizeItem ("Letter", 2159, 2794)); + paper_size_model.append (new PaperSizeItem ("Legal", 2159, 3556)); + paper_size_model.append (new PaperSizeItem ("4×6", 1016, 1524)); + paper_size_row.model = paper_size_model; + + text_dpi_row.expression = new Gtk.CClosureExpression ( + typeof (string), + null, + {}, + (Callback) get_dpi_label, + null, + null + ); + text_dpi_model = new ListStore (typeof (DpiItem)); + text_dpi_row.model = text_dpi_model; + + photo_dpi_row.expression = new Gtk.CClosureExpression ( + typeof (string), + null, + {}, + (Callback) get_dpi_label, + null, + null + ); + photo_dpi_model = new ListStore (typeof (DpiItem)); + photo_dpi_row.model = photo_dpi_model; 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 ()); }); + set_dpi_combo (text_dpi_row, DEFAULT_TEXT_DPI, dpi); + text_dpi_row.notify["selected"].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_dpi_combo (photo_dpi_row, DEFAULT_PHOTO_DPI, dpi); + photo_dpi_row.notify["selected"].connect (() => { settings.set_int ("photo-dpi", get_photo_dpi ()); }); set_page_side ((ScanSide) settings.get_enum ("page-side")); front_side_button.toggled.connect ((button) => { if (button.active) settings.set_enum ("page-side", ScanSide.FRONT); }); back_side_button.toggled.connect ((button) => { if (button.active) settings.set_enum ("page-side", ScanSide.BACK); }); both_side_button.toggled.connect ((button) => { if (button.active) settings.set_enum ("page-side", ScanSide.BOTH); }); - var 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 = "<small>%s</small>".printf (_("Darker")); var upper = brightness_adjustment.upper; @@ -123,11 +177,20 @@ private class PreferencesDialog : Hdy.PreferencesWindow 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 minimum_size_label = "<small>%s</small>".printf (_("Minimum size")); + compression_scale.add_mark (compression_adjustment.lower, Gtk.PositionType.BOTTOM, minimum_size_label); + compression_scale.add_mark (75, Gtk.PositionType.BOTTOM, null); + compression_scale.add_mark (90, Gtk.PositionType.BOTTOM, null); + var full_detail_label = "<small>%s</small>".printf (_("Full detail")); + compression_scale.add_mark (compression_adjustment.upper, Gtk.PositionType.BOTTOM, full_detail_label); + compression_adjustment.value = settings.get_int ("jpeg-quality"); + compression_adjustment.value_changed.connect (() => { settings.set_int ("jpeg-quality", (int) compression_adjustment.value); }); 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 (() => + paper_size_row.notify["selected"].connect (() => { int w, h; get_paper_size (out w, out h); @@ -199,55 +262,47 @@ private class PreferencesDialog : Hdy.PreferencesWindow 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)) + for (uint i = 0; i < paper_size_model.n_items; i++) { - int w, h; - paper_size_model.get (iter, 0, out w, 1, out h, -1); - if (w == width && h == height) - break; + var item = paper_size_model.get_item (i) as PaperSizeItem; + if (item.width == width && item.height == height) + { + paper_size_row.selected = i; + 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); + if (text_dpi_row.selected != Gtk.INVALID_LIST_POSITION) + { + var item = text_dpi_model.get_item (text_dpi_row.selected) as DpiItem; + return item.dpi; + } - return dpi; + return DEFAULT_TEXT_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); + if (photo_dpi_row.selected != Gtk.INVALID_LIST_POSITION) + { + var item = photo_dpi_model.get_item (photo_dpi_row.selected) as DpiItem; + return item.dpi; + } - return dpi; + return DEFAULT_PHOTO_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)) + if (paper_size_row.selected != Gtk.INVALID_LIST_POSITION) { - paper_size_model.get (iter, 0, ref width, 1, ref height, -1); + var item = paper_size_model.get_item (paper_size_row.selected) as PaperSizeItem; + width = item.width; + height = item.height; return true; } @@ -302,16 +357,15 @@ private class PreferencesDialog : Hdy.PreferencesWindow page_delay_0s_button.active = true; } - private void set_dpi_combo (Gtk.ComboBox combo, int default_dpi, int current_dpi) + private void set_dpi_combo (Adw.ComboRow 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; + var model = combo.model as ListStore; int[] scan_resolutions = {75, 150, 200, 300, 600, 1200, 2400}; - foreach (var dpi in scan_resolutions) + + for (var i = 0; i < scan_resolutions.length; i++) { + var dpi = scan_resolutions[i]; + string label; if (dpi == default_dpi) /* Preferences dialog: Label for default resolution in resolution list */ @@ -325,13 +379,12 @@ private class PreferencesDialog : Hdy.PreferencesWindow 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); + + model.append (new DpiItem (dpi, label)); if (dpi == current_dpi) - combo.set_active_iter (iter); + combo.selected = i; + } } } diff --git a/src/reorder-pages-dialog.vala b/src/reorder-pages-dialog.vala new file mode 100644 index 0000000..eb249ca --- /dev/null +++ b/src/reorder-pages-dialog.vala @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2023 Bartłomiej Maryńczak + * Author: Bartłomiej Maryńczak <marynczakbartlomiej@gmail.com>, + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. See http://www.gnu.org/copyleft/gpl.html the full text of the + * license. + */ + +[GtkTemplate (ui = "/org/gnome/SimpleScan/ui/reorder-pages-item.ui")] +private class ReorderPagesItem : Gtk.Button +{ + [GtkChild] + private unowned Gtk.Label title; + [GtkChild] + private unowned Gtk.Image before_image; + [GtkChild] + private unowned Gtk.Image after_image; + + public new string label + { + get { return title.label; } + set { title.label = value; } + } + + public string before + { + get { return before_image.get_icon_name (); } + set { before_image.icon_name = value; } + } + + public string after + { + get { return after_image.get_icon_name (); } + set { after_image.icon_name = value; } + } +} + + +[GtkTemplate (ui = "/org/gnome/SimpleScan/ui/reorder-pages-dialog.ui")] +private class ReorderPagesDialog : Gtk.Window +{ + [GtkChild] + public unowned ReorderPagesItem combine_sides; + [GtkChild] + public unowned ReorderPagesItem combine_sides_rev; + [GtkChild] + public unowned ReorderPagesItem flip_odd; + [GtkChild] + public unowned ReorderPagesItem flip_even; + [GtkChild] + public unowned ReorderPagesItem reverse; + + public ReorderPagesDialog () + { + add_binding_action (Gdk.Key.Escape, 0, "window.close", null); + } +} diff --git a/src/simple-scan.vala b/src/simple-scan.vala index 5deedcf..c2e2da2 100644 --- a/src/simple-scan.vala +++ b/src/simple-scan.vala @@ -9,7 +9,7 @@ * license. */ -public class SimpleScan : Gtk.Application +public class SimpleScan : Adw.Application { static bool show_version; static bool debug_enabled; @@ -40,7 +40,7 @@ public class SimpleScan : Gtk.Application public SimpleScan (ScanDevice? device = null) { /* The inhibit () method use this */ - Object (application_id: "org.gnome.SimpleScan"); + Object (application_id: "simple-scan"); register_session = true; default_device = device; @@ -50,9 +50,6 @@ public class SimpleScan : Gtk.Application { base.startup (); - Hdy.init (); - Hdy.StyleManager.get_default ().color_scheme = PREFER_LIGHT; - app = new AppWindow (); book = app.book; app.start_scan.connect (scan_cb); @@ -1580,9 +1577,14 @@ public class SimpleScan : Gtk.Application private void authorize_cb (Scanner scanner, string resource) { - string username, password; - app.authorize (resource, out username, out password); - scanner.authorize (username, password); + app.authorize.begin (resource, (obj, res) => + { + var data = app.authorize.end(res); + if (data.success) + { + scanner.authorize (data.username, data.password); + } + }); } private Page append_page (int width = 100, int height = 100, int dpi = 100) @@ -1956,7 +1958,6 @@ public class SimpleScan : Gtk.Application var c = new OptionContext (/* Arguments and description for --help text */ _("[DEVICE…] — Scanning utility")); c.add_main_entries (options, GETTEXT_PACKAGE); - c.add_group (Gtk.get_option_group (true)); try { c.parse (ref args); @@ -2014,7 +2015,7 @@ public class SimpleScan : Gtk.Application debug ("Starting %s %s, PID=%i", args[0], VERSION, Posix.getpid ()); - Gtk.init (ref args); + Gtk.init (); var app = new SimpleScan (device); return app.run (); |