diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/app-window.ui (renamed from src/simple-scan.ui) | 1044 | ||||
-rw-r--r-- | src/app-window.vala (renamed from src/ui.vala) | 1196 | ||||
-rw-r--r-- | src/authorize-dialog.ui | 141 | ||||
-rw-r--r-- | src/authorize-dialog.vala | 37 | ||||
-rw-r--r-- | src/book-view.vala | 38 | ||||
-rw-r--r-- | src/book.vala | 781 | ||||
-rw-r--r-- | src/help-overlay.ui | 128 | ||||
-rw-r--r-- | src/libwebp.vapi | 55 | ||||
-rw-r--r-- | src/libwebpmux.vapi | 128 | ||||
-rw-r--r-- | src/meson.build | 9 | ||||
-rw-r--r-- | src/page.vala | 63 | ||||
-rw-r--r-- | src/preferences-dialog.ui | 608 | ||||
-rw-r--r-- | src/preferences-dialog.vala | 534 | ||||
-rw-r--r-- | src/screensaver.vala | 25 | ||||
-rw-r--r-- | src/simple-scan.gresource.xml | 7 | ||||
-rw-r--r-- | src/simple-scan.vala | 175 |
16 files changed, 2851 insertions, 2118 deletions
diff --git a/src/simple-scan.ui b/src/app-window.ui index 6e1d15a..bbdf1c7 100644 --- a/src/simple-scan.ui +++ b/src/app-window.ui @@ -1,170 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.20.0 --> <interface> - <!-- interface-requires gtk+ 3.10 --> - <object class="GtkDialog" id="authorize_dialog"> - <property name="can_focus">False</property> - <property name="border_width">12</property> - <property name="resizable">False</property> - <property name="modal">True</property> - <property name="type_hint">normal</property> - <property name="urgency_hint">True</property> - <child internal-child="vbox"> - <object class="GtkBox" id="dialog-vbox1"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="orientation">vertical</property> - <property name="spacing">12</property> - <child internal-child="action_area"> - <object class="GtkButtonBox" id="dialog-action_area1"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="layout_style">end</property> - <child> - <object class="GtkButton" id="authorize_button"> - <property name="label" translatable="yes" comments="Button to submit authorization dialog">_Authorize</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">True</property> - <property name="use_underline">True</property> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">False</property> - <property name="position">0</property> - </packing> - </child> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="pack_type">end</property> - <property name="position">0</property> - </packing> - </child> - <child> - <object class="GtkBox" id="vbox5"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="border_width">5</property> - <property name="orientation">vertical</property> - <property name="spacing">12</property> - <child> - <object class="GtkLabel" id="authorize_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="label" comments="This label is set dynamically and is not translated">To connect to ? you need to authorize</property> - </object> - <packing> - <property name="expand">True</property> - <property name="fill">True</property> - <property name="position">0</property> - </packing> - </child> - <child> - <object class="GtkGrid" id="grid2"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="row_spacing">6</property> - <property name="column_spacing">6</property> - <child> - <object class="GtkEntry" id="username_entry"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="hexpand">True</property> - <property name="invisible_char">●</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">0</property> - <property name="width">1</property> - <property name="height">1</property> - </packing> - </child> - <child> - <object class="GtkEntry" id="password_entry"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="hexpand">True</property> - <property name="visibility">False</property> - <property name="invisible_char">●</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">1</property> - <property name="width">1</property> - <property name="height">1</property> - </packing> - </child> - <child> - <object class="GtkLabel" id="username_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="label" translatable="yes" comments="Label beside username entry">_Username for resource:</property> - <property name="use_underline">True</property> - <property name="mnemonic_widget">username_entry</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">0</property> - <property name="width">1</property> - <property name="height">1</property> - </packing> - </child> - <child> - <object class="GtkLabel" id="password_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="label" translatable="yes" comments="Label beside password entry">_Password:</property> - <property name="use_underline">True</property> - <property name="mnemonic_widget">password_entry</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">1</property> - <property name="width">1</property> - <property name="height">1</property> - </packing> - </child> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="position">1</property> - </packing> - </child> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="position">1</property> - </packing> - </child> - </object> - </child> - <action-widgets> - <action-widget response="0">authorize_button</action-widget> - </action-widgets> - </object> - <object class="GtkAdjustment" id="brightness_adjustment"> - <property name="lower">-100</property> - <property name="upper">100</property> - <property name="step_increment">1</property> - <property name="page_increment">10</property> - </object> - <object class="GtkAdjustment" id="contrast_adjustment"> - <property name="lower">-100</property> - <property name="upper">100</property> - <property name="step_increment">1</property> - <property name="page_increment">10</property> - </object> - <object class="GtkListStore" id="device_model"> - <columns> - <!-- column-name device_name --> - <column type="gchararray"/> - <!-- column-name label --> - <column type="gchararray"/> - </columns> - </object> + <requires lib="gtk+" version="3.12"/> <object class="GtkImage" id="email_image"> <property name="visible">True</property> <property name="can_focus">False</property> @@ -175,70 +12,77 @@ <property name="can_focus">False</property> <property name="stock">gtk-help</property> </object> - <object class="GtkListStore" id="page_side_model"> - <columns> - <!-- column-name side --> - <column type="gint"/> - <!-- column-name label --> - <column type="gchararray"/> - </columns> - <data> - <row> - <col id="0">3</col> - <col id="1" translatable="yes" comments="Combo box label for scanning both sides of a page">Front and Back</col> - </row> - <row> - <col id="0">1</col> - <col id="1" translatable="yes" comments="Combo box label for scanning the front side of a page">Front</col> - </row> - <row> - <col id="0">2</col> - <col id="1" translatable="yes" comments="Combo box label for scanning the back side of a page">Back</col> - </row> - </data> - </object> - <object class="GtkListStore" id="paper_size_model"> - <columns> - <!-- column-name width --> - <column type="gint"/> - <!-- column-name height --> - <column type="gint"/> - <!-- column-name label --> - <column type="gchararray"/> - </columns> - </object> - <object class="GtkListStore" id="photo_dpi_model"> - <columns> - <!-- column-name dpi --> - <column type="gint"/> - <!-- column-name label --> - <column type="gchararray"/> - </columns> - </object> - <object class="GtkAdjustment" id="quality_adjustment"> - <property name="upper">100</property> - <property name="step_increment">1</property> - <property name="page_increment">10</property> - </object> - <object class="GtkAdjustment" id="page_delay_adjustment"> - <property name="lower">0</property> - <property name="upper">10000</property> - <property name="step_increment">100</property> - <property name="page_increment">1000</property> + <object class="GtkMenu" id="scan_button_hb_menu"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkMenuItem" id="scan_single_button_hb_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes" comments="Toolbar scan menu item to scan a single page from the scanner">Single _Page</property> + <property name="use_underline">True</property> + <signal name="activate" handler="scan_button_clicked_cb" swapped="no"/> + </object> + </child> + <child> + <object class="GtkMenuItem" id="scan_all_button_hb_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes" comments="Toolbar scan menu item to scan all pages from a document feeder">All Pages From _Feeder</property> + <property name="use_underline">True</property> + <signal name="activate" handler="continuous_scan_button_clicked_cb" swapped="no"/> + </object> + </child> + <child> + <object class="GtkMenuItem" id="batch_button_hb_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes" comments="Toolbar scan menu item to scan continuously from the flatbed">_Multiple Pages From Flatbed</property> + <property name="use_underline">True</property> + <signal name="activate" handler="batch_button_clicked_cb" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="menuitem3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkRadioMenuItem" id="text_button_hb_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Text</property> + <property name="use_underline">True</property> + <property name="draw_as_radio">True</property> + <signal name="toggled" handler="text_menuitem_toggled_cb" swapped="no"/> + </object> + </child> + <child> + <object class="GtkRadioMenuItem" id="photo_button_hb_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Photo</property> + <property name="use_underline">True</property> + <property name="active">True</property> + <property name="draw_as_radio">True</property> + <property name="group">text_button_hb_menuitem</property> + <signal name="toggled" handler="photo_menuitem_toggled_cb" swapped="no"/> + </object> + </child> </object> - <template class="UserInterface" parent="GtkApplicationWindow"> + <template class="AppWindow" parent="GtkApplicationWindow"> <property name="can_focus">False</property> <property name="title" translatable="yes" comments="Title of scan window">Simple Scan</property> <property name="icon_name">scanner</property> <signal name="delete-event" handler="window_delete_event_cb" swapped="no"/> <child> - <object class="GtkBox" id="main_vbox"> + <object class="GtkBox"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="orientation">vertical</property> <child> <object class="GtkMenuBar" id="menubar"> - <property name="visible">False</property> <property name="can_focus">False</property> <child> <object class="GtkMenuItem" id="document_menuitem"> @@ -257,8 +101,8 @@ <property name="can_focus">False</property> <property name="use_underline">True</property> <property name="use_stock">True</property> - <accelerator key="n" signal="activate" modifiers="GDK_CONTROL_MASK"/> <signal name="activate" handler="new_button_clicked_cb" swapped="no"/> + <accelerator key="n" signal="activate" modifiers="GDK_CONTROL_MASK"/> </object> </child> <child> @@ -278,18 +122,18 @@ <property name="can_focus">False</property> <property name="label" translatable="yes" comments="Scan menu item to scan a single page from the scanner">Single _Page</property> <property name="use_underline">True</property> - <accelerator key="1" signal="activate" modifiers="GDK_CONTROL_MASK"/> <signal name="activate" handler="scan_button_clicked_cb" swapped="no"/> + <accelerator key="1" signal="activate" modifiers="GDK_CONTROL_MASK"/> </object> - </child> + </child> <child> <object class="GtkMenuItem" id="scan_all_menuitem"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="label" translatable="yes" comments="Scan menu item to scan all pages from a document feeder">All Pages From _Feeder</property> <property name="use_underline">True</property> - <accelerator key="f" signal="activate" modifiers="GDK_CONTROL_MASK"/> <signal name="activate" handler="continuous_scan_button_clicked_cb" swapped="no"/> + <accelerator key="f" signal="activate" modifiers="GDK_CONTROL_MASK"/> </object> </child> <child> @@ -298,10 +142,10 @@ <property name="can_focus">False</property> <property name="label" translatable="yes" comments="Scan menu item to scan continuously from the flatbed">_Multiple Pages From Flatbed</property> <property name="use_underline">True</property> - <accelerator key="m" signal="activate" modifiers="GDK_CONTROL_MASK"/> <signal name="activate" handler="batch_button_clicked_cb" swapped="no"/> + <accelerator key="m" signal="activate" modifiers="GDK_CONTROL_MASK"/> </object> - </child> + </child> <child> <object class="GtkMenuItem" id="stop_scan_menuitem"> <property name="visible">True</property> @@ -309,8 +153,8 @@ <property name="can_focus">False</property> <property name="label" translatable="yes" comments="Menu entry to stop current scan">_Stop Scan</property> <property name="use_underline">True</property> - <accelerator key="Escape" signal="activate"/> <signal name="activate" handler="stop_scan_button_clicked_cb" swapped="no"/> + <accelerator key="Escape" signal="activate"/> </object> </child> <child> @@ -362,8 +206,8 @@ <property name="can_focus">False</property> <property name="use_underline">True</property> <property name="use_stock">True</property> - <accelerator key="s" signal="activate" modifiers="GDK_CONTROL_MASK"/> <signal name="activate" handler="save_file_button_clicked_cb" swapped="no"/> + <accelerator key="s" signal="activate" modifiers="GDK_CONTROL_MASK"/> </object> </child> <child> @@ -375,8 +219,8 @@ <property name="use_underline">True</property> <property name="image">email_image</property> <property name="use_stock">False</property> - <accelerator key="e" signal="activate" modifiers="GDK_CONTROL_MASK"/> <signal name="activate" handler="email_button_clicked_cb" swapped="no"/> + <accelerator key="e" signal="activate" modifiers="GDK_CONTROL_MASK"/> </object> </child> <child> @@ -387,8 +231,8 @@ <property name="can_focus">False</property> <property name="use_underline">True</property> <property name="use_stock">True</property> - <accelerator key="p" signal="activate" modifiers="GDK_CONTROL_MASK"/> <signal name="activate" handler="print_button_clicked_cb" swapped="no"/> + <accelerator key="p" signal="activate" modifiers="GDK_CONTROL_MASK"/> </object> </child> <child> @@ -420,9 +264,9 @@ <property name="can_focus">False</property> <property name="use_underline">True</property> <property name="use_stock">True</property> + <signal name="activate" handler="quit_menuitem_activate_cb" swapped="no"/> <accelerator key="w" signal="activate" modifiers="GDK_CONTROL_MASK"/> <accelerator key="q" signal="activate" modifiers="GDK_CONTROL_MASK"/> - <signal name="activate" handler="quit_menuitem_activate_cb" swapped="no"/> </object> </child> </object> @@ -445,8 +289,8 @@ <property name="can_focus">False</property> <property name="label" translatable="yes" comments="Menu item to rotate page to left (anti-clockwise)">Rotate _Left</property> <property name="use_underline">True</property> - <accelerator key="bracketleft" signal="activate"/> <signal name="activate" handler="rotate_left_button_clicked_cb" swapped="no"/> + <accelerator key="bracketleft" signal="activate"/> </object> </child> <child> @@ -455,8 +299,8 @@ <property name="can_focus">False</property> <property name="label" translatable="yes" comments="Menu item to rotate page to right (clockwise)">Rotate _Right</property> <property name="use_underline">True</property> - <accelerator key="bracketright" signal="activate"/> <signal name="activate" handler="rotate_right_button_clicked_cb" swapped="no"/> + <accelerator key="bracketright" signal="activate"/> </object> </child> <child> @@ -582,8 +426,8 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="label" translatable="yes" comments="Menu item to move the selected page to the left">Move Left</property> - <accelerator key="less" signal="activate"/> <signal name="activate" handler="page_move_left_menuitem_activate_cb" swapped="no"/> + <accelerator key="less" signal="activate"/> </object> </child> <child> @@ -592,8 +436,8 @@ <property name="can_focus">False</property> <property name="label" translatable="yes" comments="Menu item to move the selected page to the right">Move Right</property> <property name="use_underline">True</property> - <accelerator key="greater" signal="activate"/> <signal name="activate" handler="page_move_right_menuitem_activate_cb" swapped="no"/> + <accelerator key="greater" signal="activate"/> </object> </child> <child> @@ -604,8 +448,8 @@ <property name="can_focus">False</property> <property name="use_underline">True</property> <property name="use_stock">True</property> - <accelerator key="c" signal="activate" modifiers="GDK_CONTROL_MASK"/> <signal name="activate" handler="copy_to_clipboard_button_clicked_cb" swapped="no"/> + <accelerator key="c" signal="activate" modifiers="GDK_CONTROL_MASK"/> </object> </child> <child> @@ -615,8 +459,8 @@ <property name="can_focus">False</property> <property name="use_underline">True</property> <property name="use_stock">True</property> - <accelerator key="Delete" signal="activate"/> <signal name="activate" handler="page_delete_menuitem_activate_cb" swapped="no"/> + <accelerator key="Delete" signal="activate"/> </object> </child> </object> @@ -641,8 +485,8 @@ <property name="use_underline">True</property> <property name="image">help_image</property> <property name="use_stock">False</property> - <accelerator key="F1" signal="activate"/> <signal name="activate" handler="help_contents_menuitem_activate_cb" swapped="no"/> + <accelerator key="F1" signal="activate"/> </object> </child> <child> @@ -668,26 +512,7 @@ </child> <child> <object class="GtkToolbar" id="toolbar"> - <property name="visible">False</property> <property name="can_focus">False</property> - <style> - <class name="primary-toolbar"/> - </style> - <child> - <object class="GtkToolButton" id="new_toolbutton"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="tooltip_text" translatable="yes" comments="Tooltip for new document button">Start a new document</property> - <property name="label" translatable="yes">New</property> - <property name="use_underline">True</property> - <property name="stock_id">gtk-new</property> - <signal name="clicked" handler="new_button_clicked_cb" swapped="no"/> - </object> - <packing> - <property name="expand">False</property> - <property name="homogeneous">True</property> - </packing> - </child> <child> <object class="GtkMenuToolButton" id="scan_toolbutton"> <property name="visible">True</property> @@ -738,601 +563,253 @@ <property name="homogeneous">True</property> </packing> </child> + <style> + <class name="primary-toolbar"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkStack" id="stack"> + <property name="visible">True</property> + <property name="can_focus">False</property> <child> - <object class="GtkSeparatorToolItem" id="toolbutton2"> - <property name="visible">True</property> - <property name="can_focus">False</property> - </object> - <packing> - <property name="expand">False</property> - <property name="homogeneous">True</property> - </packing> - </child> - <child> - <object class="GtkToolButton" id="rotate_left_toolbutton"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="tooltip_text" translatable="yes" comments="Tooltip for rotate left (counter-clockwise) button">Rotate the page to the left (counter-clockwise)</property> - <property name="label" translatable="yes" comments="Label on rotate page left (anti-clockwise) item">Rotate Left</property> - <property name="use_underline">True</property> - <property name="icon_name">object-rotate-left</property> - <signal name="clicked" handler="rotate_left_button_clicked_cb" swapped="no"/> - </object> - <packing> - <property name="expand">False</property> - <property name="homogeneous">True</property> - </packing> - </child> - <child> - <object class="GtkToolButton" id="rotate_right_toolbutton"> + <object class="GtkAlignment"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="tooltip_text" translatable="yes" comments="Tooltip for rotate right (clockwise) button">Rotate the page to the right (clockwise)</property> - <property name="label" translatable="yes" comments="Label on rotate page right (clockwise) item">Rotate Right</property> - <property name="use_underline">True</property> - <property name="icon_name">object-rotate-right</property> - <signal name="clicked" handler="rotate_right_button_clicked_cb" swapped="no"/> + <property name="xscale">0</property> + <property name="yscale">0</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">10</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="opacity">0.5</property> + <property name="pixel_size">120</property> + <property name="icon_name">scanner-symbolic</property> + <property name="icon_size">6</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="status_primary_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes" comments="Label shown when searching for scanners">Searching for Scanners…</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="1.5"/> + </attributes> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="status_secondary_label"> + <property name="visible">False</property> + <property name="can_focus">False</property> + <property name="track_visited_links">False</property> + <signal name="activate_link" handler="status_label_activate_link_cb" swapped="no"/> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> </object> <packing> - <property name="expand">False</property> - <property name="homogeneous">True</property> + <property name="name">startup</property> </packing> </child> <child> - <object class="GtkToggleToolButton" id="crop_toolbutton"> + <object class="GtkBox" id="main_vbox"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="tooltip_text" translatable="yes">Crop the selected page</property> - <property name="is_important">True</property> - <property name="label" translatable="yes">Crop</property> - <property name="use_underline">True</property> - <property name="icon_name">object-crop</property> - <signal name="toggled" handler="crop_toolbutton_toggled_cb" swapped="no"/> + <property name="orientation">vertical</property> + <child> + <placeholder/> + </child> + <child> + <object class="GtkActionBar" id="action_bar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">1</property> + </packing> + </child> </object> <packing> - <property name="expand">False</property> - <property name="homogeneous">True</property> + <property name="name">document</property> + <property name="position">1</property> </packing> </child> </object> <packing> - <property name="expand">False</property> + <property name="expand">True</property> <property name="fill">True</property> - <property name="position">1</property> + <property name="position">2</property> </packing> </child> - <child> - <placeholder/> - </child> </object> </child> <child type="titlebar"> - <object class="GtkHeaderBar" id="headerbar"> + <object class="GtkHeaderBar" id="header_bar"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="vexpand">True</property> <property name="show_close_button">True</property> - <property name="title" translatable="yes" comments="Title of scan window">Simple Scan</property> - <style> - <class name="titlebar"/> - </style> <child> <object class="GtkBox" id="open_box"> <property name="visible">True</property> - <property name="orientation">horizontal</property> + <property name="can_focus">False</property> <property name="valign">center</property> - <style> - <class name="linked"/> - </style> <child> <object class="GtkButton" id="stop_button"> - <property name="visible">False</property> - <property name="tooltip_text" translatable="yes" comments="Tooltip for stop button">Stop the current scan</property> <property name="label" translatable="yes">Stop</property> + <property name="can_focus">False</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes" comments="Tooltip for stop button">Stop the current scan</property> <property name="use_underline">True</property> <signal name="clicked" handler="stop_scan_button_clicked_cb" swapped="no"/> <style> <class name="text-button"/> + <class name="destructive-action"/> </style> </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> </child> <child> <object class="GtkButton" id="scan_button"> + <property name="label" translatable="yes" comments="Label on scan toolbar item">Scan</property> <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="receives_default">False</property> <property name="tooltip_text" translatable="yes" comments="Tooltip for scan toolbar button">Scan a single page from the scanner</property> - <property name="label" translatable="yes" comments="Label on scan toolbar item">Scan</property> <property name="use_underline">True</property> <signal name="clicked" handler="scan_button_clicked_cb" swapped="no"/> <style> <class name="text-button"/> + <class name="suggested-action"/> </style> </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> </child> <child> <object class="GtkMenuButton" id="open_button"> <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="receives_default">False</property> <property name="popup">scan_button_hb_menu</property> + <child> + <placeholder/> + </child> <style> <class name="text-button"/> </style> </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> </child> - </object> - <packing> - <property name="pack_type">start</property> - </packing> - </child> - <child> - <object class="GtkButton" id="save_button"> - <property name="visible">True</property> - <property name="sensitive">False</property> - <property name="can_focus">False</property> - <property name="tooltip_text" translatable="yes" comments="Tooltip for save toolbar button">Save document to a file</property> - <property name="use_underline">True</property> - <signal name="clicked" handler="save_file_button_clicked_cb" swapped="no"/> <style> - <class name="image-button"/> + <class name="linked"/> </style> - <child> - <object class="GtkImage" id="save_image"> - <property name="visible">True</property> - <property name="icon_size">1</property> - <property name="icon_name">document-save-symbolic</property> - </object> - </child> </object> - <packing> - </packing> </child> <child> - <object class="GtkButton" id="new_button"> + <object class="GtkMenuButton" id="menu_button"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="tooltip_text" translatable="yes" comments="Tooltip for new document button">Start a new document</property> + <property name="receives_default">False</property> <property name="use_underline">True</property> - <signal name="clicked" handler="new_button_clicked_cb" swapped="no"/> - <style> - <class name="image-button"/> - </style> <child> - <object class="GtkImage" id="new_image"> + <object class="GtkImage"> <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">open-menu-symbolic</property> <property name="icon_size">1</property> - <property name="icon_name">document-new-symbolic</property> </object> </child> - </object> - <packing> - </packing> - </child> - <child> - <object class="GtkBox" id="rotate_box"> - <property name="visible">True</property> - <property name="orientation">horizontal</property> - <property name="valign">center</property> <style> - <class name="linked"/> + <class name="image-button"/> </style> - <child> - <object class="GtkButton" id="rotate_right_button"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="tooltip_text" translatable="yes" comments="Tooltip for rotate right (clockwise) button">Rotate the page to the right (clockwise)</property> - <property name="use_underline">True</property> - <signal name="clicked" handler="rotate_right_button_clicked_cb" swapped="no"/> - <style> - <class name="image-button"/> - </style> - <child> - <object class="GtkImage" id="objectrotateright-button"> - <property name="visible">True</property> - <property name="icon_size">1</property> - <property name="icon_name">object-rotate-right-symbolic</property> - </object> - </child> - </object> - <packing> - <property name="pack_type">end</property> - </packing> - </child> - <child> - <object class="GtkButton" id="rotate_left_button"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="tooltip_text" translatable="yes" comments="Tooltip for rotate left (counter-clockwise) button">Rotate the page to the left (counter-clockwise)</property> - <property name="use_underline">True</property> - <signal name="clicked" handler="rotate_left_button_clicked_cb" swapped="no"/> - <style> - <class name="image-button"/> - </style> - <child> - <object class="GtkImage" id="objectrotateleft-button"> - <property name="visible">True</property> - <property name="icon_size">1</property> - <property name="icon_name">object-rotate-left-symbolic</property> - </object> - </child> - </object> - <packing> - <property name="pack_type">end</property> - </packing> - </child> </object> <packing> <property name="pack_type">end</property> + <property name="position">2</property> </packing> </child> <child> - <object class="GtkToggleButton" id="crop_button"> + <object class="GtkButton" id="save_button"> <property name="visible">True</property> + <property name="sensitive">False</property> <property name="can_focus">False</property> - <property name="tooltip_text" translatable="yes">Crop the selected page</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes" comments="Tooltip for save toolbar button">Save document to a file</property> <property name="use_underline">True</property> - <signal name="toggled" handler="crop_button_toggled_cb" swapped="no"/> - <style> - <class name="image-button"/> - </style> + <signal name="clicked" handler="save_file_button_clicked_cb" swapped="no"/> <child> - <object class="GtkImage" id="objectcrop-button"> + <object class="GtkImage" id="save_image"> <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">document-save-symbolic</property> <property name="icon_size">1</property> - <property name="icon_name">edit-cut-symbolic</property> </object> </child> + <style> + <class name="image-button"/> + </style> </object> <packing> <property name="pack_type">end</property> + <property name="position">3</property> </packing> </child> + <style> + <class name="titlebar"/> + </style> </object> - <packing> - </packing> </child> </template> - <object class="GtkListStore" id="text_dpi_model"> - <columns> - <!-- column-name dpi --> - <column type="gint"/> - <!-- column-name label --> - <column type="gchararray"/> - </columns> - </object> - <object class="GtkDialog" id="preferences_dialog"> - <property name="can_focus">False</property> - <property name="border_width">7</property> - <property name="title" translatable="yes" comments="Title of preferences dialog">Preferences</property> - <property name="resizable">False</property> - <property name="icon_name">scanner</property> - <property name="type_hint">normal</property> - <signal name="delete-event" handler="preferences_dialog_delete_event_cb" swapped="no"/> - <signal name="response" handler="preferences_dialog_response_cb" swapped="no"/> - <child internal-child="vbox"> - <object class="GtkBox" id="dialog-vbox2"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="orientation">vertical</property> - <property name="spacing">2</property> - <child internal-child="action_area"> - <object class="GtkButtonBox" id="dialog-action_area2"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="layout_style">end</property> - <child> - <object class="GtkButton" id="preferences_close_button"> - <property name="label">gtk-close</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">True</property> - <property name="use_stock">True</property> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">False</property> - <property name="position">0</property> - </packing> - </child> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="pack_type">end</property> - <property name="position">0</property> - </packing> - </child> - <child> - <object class="GtkGrid" id="grid3"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="border_width">5</property> - <property name="row_spacing">6</property> - <property name="column_spacing">6</property> - <child> - <object class="GtkLabel" id="source_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="xalign">0</property> - <property name="label" translatable="yes" comments="Label beside scan source combo box">Scan S_ource:</property> - <property name="use_underline">True</property> - <property name="mnemonic_widget">device_combo</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">0</property> - <property name="width">1</property> - <property name="height">1</property> - </packing> - </child> - <child> - <object class="GtkComboBox" id="device_combo"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="hexpand">True</property> - <property name="model">device_model</property> - <signal name="changed" handler="device_combo_changed_cb" swapped="no"/> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">0</property> - <property name="width">1</property> - <property name="height">1</property> - </packing> - </child> - <child> - <object class="GtkLabel" id="text_dpi_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="xalign">0</property> - <property name="label" translatable="yes" comments="Label beside scan source combo box">_Text Resolution:</property> - <property name="use_underline">True</property> - <property name="mnemonic_widget">text_dpi_combo</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">1</property> - <property name="width">1</property> - <property name="height">1</property> - </packing> - </child> - <child> - <object class="GtkLabel" id="photo_dpi_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="xalign">0</property> - <property name="label" translatable="yes" comments="Label beside scan source combo box">_Photo Resolution:</property> - <property name="use_underline">True</property> - <property name="mnemonic_widget">photo_dpi_combo</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">2</property> - <property name="width">1</property> - <property name="height">1</property> - </packing> - </child> - <child> - <object class="GtkComboBox" id="text_dpi_combo"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="hexpand">True</property> - <property name="model">text_dpi_model</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">1</property> - <property name="width">1</property> - <property name="height">1</property> - </packing> - </child> - <child> - <object class="GtkComboBox" id="photo_dpi_combo"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="hexpand">True</property> - <property name="model">photo_dpi_model</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">2</property> - <property name="width">1</property> - <property name="height">1</property> - </packing> - </child> - <child> - <object class="GtkLabel" id="page_side_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="xalign">0</property> - <property name="label" translatable="yes" comments="Label beside scan side combo box">Scan Side:</property> - <property name="use_underline">True</property> - <property name="mnemonic_widget">photo_dpi_combo</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">3</property> - <property name="width">1</property> - <property name="height">1</property> - </packing> - </child> - <child> - <object class="GtkComboBox" id="page_side_combo"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="hexpand">True</property> - <property name="model">page_side_model</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">3</property> - <property name="width">1</property> - <property name="height">1</property> - </packing> - </child> - <child> - <object class="GtkLabel" id="paper_size_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="xalign">0</property> - <property name="label" translatable="yes" comments="Label beside page size combo box">Page Size:</property> - <property name="use_underline">True</property> - <property name="mnemonic_widget">photo_dpi_combo</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">4</property> - <property name="width">1</property> - <property name="height">1</property> - </packing> - </child> - <child> - <object class="GtkComboBox" id="paper_size_combo"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="hexpand">True</property> - <property name="model">paper_size_model</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">4</property> - <property name="width">1</property> - <property name="height">1</property> - </packing> - </child> - <child> - <object class="GtkLabel" id="brightness_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="xalign">0</property> - <property name="label" translatable="yes" comments="Label beside brightness scale">Brightness:</property> - <property name="use_underline">True</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">5</property> - <property name="width">1</property> - <property name="height">1</property> - </packing> - </child> - <child> - <object class="GtkScale" id="brightness_scale"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="hexpand">True</property> - <property name="adjustment">brightness_adjustment</property> - <property name="draw_value">False</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">5</property> - <property name="width">1</property> - <property name="height">1</property> - </packing> - </child> - <child> - <object class="GtkLabel" id="contrast_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="xalign">0</property> - <property name="label" translatable="yes" comments="Label beside contrast scale">Contrast:</property> - <property name="use_underline">True</property> - <property name="mnemonic_widget">contrast_scale</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">6</property> - <property name="width">1</property> - <property name="height">1</property> - </packing> - </child> - <child> - <object class="GtkScale" id="contrast_scale"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="hexpand">True</property> - <property name="adjustment">contrast_adjustment</property> - <property name="draw_value">False</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">6</property> - <property name="width">1</property> - <property name="height">1</property> - </packing> - </child> - <child> - <object class="GtkLabel" id="quality_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="xalign">0</property> - <property name="label" translatable="yes" comments="Label beside quality scale">Quality:</property> - <property name="use_underline">True</property> - <property name="mnemonic_widget">quality_scale</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">7</property> - <property name="width">1</property> - <property name="height">1</property> - </packing> - </child> - <child> - <object class="GtkScale" id="quality_scale"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="hexpand">True</property> - <property name="adjustment">quality_adjustment</property> - <property name="draw_value">False</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">7</property> - <property name="width">1</property> - <property name="height">1</property> - </packing> - </child> - <child> - <object class="GtkLabel" id="page_delay_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="xalign">0</property> - <property name="label" translatable="yes" comments="Label beside page delay scale">Delay between pages:</property> - <property name="use_underline">True</property> - <property name="mnemonic_widget">page_delay_scale</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">8</property> - <property name="width">1</property> - <property name="height">1</property> - </packing> - </child> - <child> - <object class="GtkScale" id="page_delay_scale"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="hexpand">True</property> - <property name="adjustment">page_delay_adjustment</property> - <property name="draw_value">True</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">8</property> - <property name="width">1</property> - <property name="height">1</property> - </packing> - </child> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="position">1</property> - </packing> - </child> - </object> - </child> - <action-widgets> - <action-widget response="1">preferences_close_button</action-widget> - </action-widgets> - </object> <object class="GtkMenu" id="scan_button_menu"> <property name="visible">True</property> <property name="can_focus">False</property> @@ -1362,7 +839,7 @@ <property name="use_underline">True</property> <signal name="activate" handler="batch_button_clicked_cb" swapped="no"/> </object> - </child> + </child> <child> <object class="GtkSeparatorMenuItem" id="menuitem1"> <property name="visible">True</property> @@ -1392,63 +869,4 @@ </object> </child> </object> - <object class="GtkMenu" id="scan_button_hb_menu"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <child> - <object class="GtkMenuItem" id="scan_single_button_hb_menuitem"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="label" translatable="yes" comments="Toolbar scan menu item to scan a single page from the scanner">Single _Page</property> - <property name="use_underline">True</property> - <signal name="activate" handler="scan_button_clicked_cb" swapped="no"/> - </object> - </child> - <child> - <object class="GtkMenuItem" id="scan_all_button_hb_menuitem"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="label" translatable="yes" comments="Toolbar scan menu item to scan all pages from a document feeder">All Pages From _Feeder</property> - <property name="use_underline">True</property> - <signal name="activate" handler="continuous_scan_button_clicked_cb" swapped="no"/> - </object> - </child> - <child> - <object class="GtkMenuItem" id="batch_button_hb_menuitem"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="label" translatable="yes" comments="Toolbar scan menu item to scan continuously from the flatbed">_Multiple Pages From Flatbed</property> - <property name="use_underline">True</property> - <signal name="activate" handler="batch_button_clicked_cb" swapped="no"/> - </object> - </child> - <child> - <object class="GtkSeparatorMenuItem" id="menuitem3"> - <property name="visible">True</property> - <property name="can_focus">False</property> - </object> - </child> - <child> - <object class="GtkRadioMenuItem" id="text_button_hb_menuitem"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="label" translatable="yes">Text</property> - <property name="use_underline">True</property> - <property name="draw_as_radio">True</property> - <signal name="toggled" handler="text_menuitem_toggled_cb" swapped="no"/> - </object> - </child> - <child> - <object class="GtkRadioMenuItem" id="photo_button_hb_menuitem"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="label" translatable="yes">Photo</property> - <property name="use_underline">True</property> - <property name="active">True</property> - <property name="draw_as_radio">True</property> - <property name="group">text_button_hb_menuitem</property> - <signal name="toggled" handler="photo_menuitem_toggled_cb" swapped="no"/> - </object> - </child> - </object> </interface> diff --git a/src/ui.vala b/src/app-window.vala index 554b161..2cd75ee 100644 --- a/src/ui.vala +++ b/src/app-window.vala @@ -10,12 +10,12 @@ * license. */ -[GtkTemplate (ui = "/org/gnome/SimpleScan/simple-scan.ui")] -public class UserInterface : Gtk.ApplicationWindow -{ - private const int DEFAULT_TEXT_DPI = 150; - private const int DEFAULT_PHOTO_DPI = 300; +private const int DEFAULT_TEXT_DPI = 150; +private const int DEFAULT_PHOTO_DPI = 300; +[GtkTemplate (ui = "/org/gnome/SimpleScan/app-window.ui")] +public class AppWindow : Gtk.ApplicationWindow +{ private const GLib.ActionEntry[] action_entries = { { "new_document", new_document_activate_cb }, @@ -31,6 +31,10 @@ public class UserInterface : Gtk.ApplicationWindow private Settings settings; + private PreferencesDialog preferences_dialog; + + [GtkChild] + private Gtk.HeaderBar header_bar; [GtkChild] private Gtk.MenuBar menubar; [GtkChild] @@ -38,13 +42,13 @@ public class UserInterface : Gtk.ApplicationWindow [GtkChild] private Gtk.Menu page_menu; [GtkChild] + private Gtk.Stack stack; + [GtkChild] + private Gtk.Label status_primary_label; + [GtkChild] + private Gtk.Label status_secondary_label; + [GtkChild] private Gtk.Box main_vbox; - private Gtk.InfoBar info_bar; - private Gtk.Image info_bar_image; - private Gtk.Label info_bar_label; - private Gtk.Button info_bar_close_button; - private Gtk.Button info_bar_change_scanner_button; - private Gtk.Button info_bar_install_button; [GtkChild] private Gtk.RadioMenuItem custom_crop_menuitem; [GtkChild] @@ -86,13 +90,13 @@ public class UserInterface : Gtk.ApplicationWindow [GtkChild] private Gtk.ToolButton stop_toolbutton; [GtkChild] - private Gtk.ToggleButton crop_button; - [GtkChild] - private Gtk.ToggleToolButton crop_toolbutton; - [GtkChild] private Gtk.Button stop_button; [GtkChild] private Gtk.Button scan_button; + [GtkChild] + private Gtk.ActionBar action_bar; + private Gtk.ToggleButton crop_button; + private Gtk.Button delete_button; [GtkChild] private Gtk.RadioMenuItem text_button_menuitem; @@ -108,63 +112,11 @@ public class UserInterface : Gtk.ApplicationWindow private Gtk.RadioMenuItem photo_menuitem; [GtkChild] - private Gtk.Dialog authorize_dialog; - [GtkChild] - private Gtk.Label authorize_label; - [GtkChild] - private Gtk.Entry username_entry; - [GtkChild] - private Gtk.Entry password_entry; + private Gtk.MenuButton menu_button; - [GtkChild] - private Gtk.Dialog preferences_dialog; - [GtkChild] - private Gtk.ComboBox device_combo; - [GtkChild] - private Gtk.ComboBox text_dpi_combo; - [GtkChild] - private Gtk.ComboBox photo_dpi_combo; - [GtkChild] - private Gtk.ComboBox page_side_combo; - [GtkChild] - private Gtk.ComboBox paper_size_combo; - [GtkChild] - private Gtk.Scale brightness_scale; - [GtkChild] - private Gtk.Scale contrast_scale; - [GtkChild] - private Gtk.Scale quality_scale; - [GtkChild] - private Gtk.Scale page_delay_scale; - [GtkChild] - private Gtk.ListStore device_model; - [GtkChild] - private Gtk.ListStore text_dpi_model; - [GtkChild] - private Gtk.ListStore photo_dpi_model; - [GtkChild] - private Gtk.ListStore page_side_model; - [GtkChild] - private Gtk.ListStore paper_size_model; - [GtkChild] - private Gtk.Adjustment brightness_adjustment; - [GtkChild] - private Gtk.Adjustment contrast_adjustment; - [GtkChild] - private Gtk.Adjustment quality_adjustment; - [GtkChild] - private Gtk.Adjustment page_delay_adjustment; - private bool setting_devices; private string? missing_driver = null; - private bool user_selected_device; private Gtk.FileChooserDialog? save_dialog; - private ProgressBarDialog progress_dialog; - - private bool have_error; - private string error_title; - private string error_text; - private bool error_change_scanner_hint; public Book book { get; private set; } private bool book_needs_saving; @@ -186,10 +138,6 @@ public class UserInterface : Gtk.ApplicationWindow private BookView book_view; private bool updating_page_menu; - private int default_page_width; - private int default_page_height; - private int default_page_dpi; - private ScanDirection default_page_scan_direction; private string document_hint = "photo"; @@ -200,7 +148,9 @@ public class UserInterface : Gtk.ApplicationWindow set { scanning_ = value; + stack.set_visible_child_name ("document"); page_delete_menuitem.sensitive = !value; + delete_button.sensitive = !value; stop_scan_menuitem.sensitive = value; stop_toolbutton.sensitive = value; scan_button.visible = !value; @@ -211,66 +161,38 @@ public class UserInterface : Gtk.ApplicationWindow private int window_width; private int window_height; private bool window_is_maximized; - private bool window_is_fullscreen; + private bool window_is_fullscreen; private uint save_state_timeout; public int brightness { - get { return (int) brightness_adjustment.value; } - set { brightness_adjustment.value = value; } + get { return preferences_dialog.get_brightness (); } + set { preferences_dialog.set_brightness (value); } } public int contrast { - get { return (int) contrast_adjustment.value; } - set { contrast_adjustment.value = value; } - } - - public int quality - { - get { return (int) quality_adjustment.value; } - set { quality_adjustment.value = value; } + get { return preferences_dialog.get_contrast (); } + set { preferences_dialog.set_contrast (value); } } public int page_delay { - get { return (int) page_delay_adjustment.value; } - set { page_delay_adjustment.value = value; } + get { return preferences_dialog.get_page_delay (); } + set { preferences_dialog.set_page_delay (value); } } public string? selected_device { - owned get - { - Gtk.TreeIter iter; - - if (device_combo.get_active_iter (out iter)) - { - string device; - device_model.get (iter, 0, out device, -1); - return device; - } - - return null; - } - - set - { - Gtk.TreeIter iter; - if (!find_scan_device (value, out iter)) - return; - - device_combo.set_active_iter (iter); - user_selected_device = true; - } + owned get { return preferences_dialog.get_selected_device (); } + set { preferences_dialog.set_selected_device (value); } } public signal void start_scan (string? device, ScanOptions options); public signal void stop_scan (); - public signal void email (string profile, int quality); - public UserInterface () + public AppWindow () { settings = new Settings ("org.gnome.SimpleScan"); @@ -291,38 +213,21 @@ public class UserInterface : Gtk.ApplicationWindow 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); } } - ~UserInterface () + ~AppWindow () { book.page_added.disconnect (page_added_cb); book.reordered.disconnect (reordered_cb); book.page_removed.disconnect (page_removed_cb); } - private bool find_scan_device (string device, out Gtk.TreeIter iter) - { - bool have_iter = false; - - if (device_model.get_iter_first (out iter)) - { - do - { - string d; - device_model.get (iter, 0, out d, -1); - if (d == device) - have_iter = true; - } while (!have_iter && device_model.iter_next (ref iter)); - } - - return have_iter; - } - - private void show_error_dialog (string error_title, string error_text) + public void show_error_dialog (string error_title, string error_text) { var dialog = new Gtk.MessageDialog (this, Gtk.DialogFlags.MODAL, @@ -337,164 +242,47 @@ public class UserInterface : Gtk.ApplicationWindow public void authorize (string resource, out string username, out string password) { - /* Label in authorization dialog. '%s' is replaced with the name of the resource requesting authorization */ - var description = _("Username and password required to access '%s'").printf (resource); - - username_entry.text = ""; - password_entry.text = ""; - authorize_label.set_text (description); - + /* Label in authorization dialog. “%s” is replaced with the name of the resource requesting authorization */ + var description = _("Username and password required to access “%s”").printf (resource); + var authorize_dialog = new AuthorizeDialog (description); authorize_dialog.visible = true; + authorize_dialog.transient_for = this; authorize_dialog.run (); - authorize_dialog.visible = false; + authorize_dialog.destroy (); - username = username_entry.text; - password = password_entry.text; + username = authorize_dialog.get_username (); + password = authorize_dialog.get_password (); } - [GtkCallback] - private void device_combo_changed_cb (Gtk.Widget widget) + public void set_scan_devices (List<ScanDevice> devices, string? missing_driver = null) { - if (setting_devices) - return; - user_selected_device = true; - if (selected_device != null) - settings.set_string ("selected-device", selected_device); - } + this.missing_driver = missing_driver; - private void update_info_bar () - { - Gtk.MessageType type; - string title, text, image_id; - bool show_close_button = false; - bool show_install_button = false; - bool show_change_scanner_button = false; + preferences_dialog.set_scan_devices (devices); - if (have_error) + if (devices != null) { - type = Gtk.MessageType.ERROR; - image_id = "dialog-error"; - title = error_title; - text = error_text; - show_close_button = true; - show_change_scanner_button = error_change_scanner_hint; + status_primary_label.set_text (/* Label shown when detected a scanner */ + _("Ready to Scan")); + status_secondary_label.set_text (preferences_dialog.get_selected_device_label ()); + status_secondary_label.visible = true; } - else if (device_model.iter_n_children (null) == 0) + else if (missing_driver != null) { - type = Gtk.MessageType.WARNING; - image_id = "dialog-warning"; - if (missing_driver == null) - { - /* Warning displayed when no scanners are detected */ - title = _("No scanners detected"); - /* Hint to user on why there are no scanners detected */ - text = _("Please check your scanner is connected and powered on"); - } - else - { - /* Warning displayed when no drivers are installed but a compatible scanner is detected */ - title = _("Additional software needed"); - /* Instructions to install driver software */ - text = _("You need to install driver software for your scanner."); - show_install_button = true; - } + status_primary_label.set_text (/* Warning displayed when no drivers are installed but a compatible scanner is detected */ + _("Additional software needed")); + /* Instructions to install driver software */ + status_secondary_label.set_markup (_("You need to <a href=\"install-firmware\">install driver software</a> for your scanner.")); + status_secondary_label.visible = true; } else { - info_bar.visible = false; - return; - } - - info_bar.message_type = type; - info_bar_image.set_from_icon_name (image_id, Gtk.IconSize.DIALOG); - var message = "<big><b>%s</b></big>\n\n%s".printf (title, text); - info_bar_label.set_markup (message); - info_bar_close_button.visible = show_close_button; - info_bar_change_scanner_button.visible = show_change_scanner_button; - info_bar_install_button.visible = show_install_button; - info_bar.visible = true; - } - - public void set_scan_devices (List<ScanDevice> devices, string? missing_driver = null) - { - bool have_selection = false; - int index; - Gtk.TreeIter iter; - - setting_devices = true; - - this.missing_driver = missing_driver; - - /* If the user hasn't chosen a scanner choose the best available one */ - if (user_selected_device) - have_selection = device_combo.active >= 0; - - /* Add new devices */ - index = 0; - foreach (var device in devices) - { - int n_delete = -1; - - /* Find if already exists */ - if (device_model.iter_nth_child (out iter, null, index)) - { - int i = 0; - do - { - string name; - bool matched; - - device_model.get (iter, 0, out name, -1); - matched = name == device.name; - - if (matched) - { - n_delete = i; - break; - } - i++; - } while (device_model.iter_next (ref iter)); - } - - /* If exists, remove elements up to this one */ - if (n_delete >= 0) - { - int i; - - /* Update label */ - device_model.set (iter, 1, device.label, -1); - - for (i = 0; i < n_delete; i++) - { - device_model.iter_nth_child (out iter, null, index); - device_model.remove (iter); - } - } - else - { - device_model.insert (out iter, index); - device_model.set (iter, 0, device.name, 1, device.label, -1); - } - index++; - } - - /* Remove any remaining devices */ - while (device_model.iter_nth_child (out iter, null, index)) - device_model.remove (iter); - - /* Select the previously selected device or the first available device */ - if (!have_selection) - { - var device = settings.get_string ("selected-device"); - if (device != null && find_scan_device (device, out iter)) - device_combo.set_active_iter (iter); - else - device_combo.set_active (0); + /* Warning displayed when no scanners are detected */ + status_primary_label.set_text (_("No scanners detected")); + /* Hint to user on why there are no scanners detected */ + status_secondary_label.set_text (_("Please check your scanner is connected and powered on")); + status_secondary_label.visible = true; } - - setting_devices = false; - - update_info_bar (); } private string choose_file_location () @@ -507,12 +295,12 @@ public class UserInterface : Gtk.ApplicationWindow directory = Environment.get_user_special_dir (UserDirectory.DOCUMENTS); save_dialog = new Gtk.FileChooserDialog (/* Save dialog: Dialog title */ - _("Save As..."), + _("Save As…"), this, Gtk.FileChooserAction.SAVE, _("_Cancel"), Gtk.ResponseType.CANCEL, _("_Save"), Gtk.ResponseType.ACCEPT, - null); + null); save_dialog.local_only = false; if (book_uri != null) save_dialog.set_uri (book_uri); @@ -524,9 +312,13 @@ public class UserInterface : Gtk.ApplicationWindow /* Filter to only show images by default */ var filter = new Gtk.FileFilter (); - filter.set_filter_name (/* Save dialog: Filter name to show only image files */ + filter.set_filter_name (/* Save dialog: Filter name to show only supported image files */ _("Image Files")); - filter.add_pixbuf_formats (); + 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 (); @@ -555,12 +347,20 @@ public class UserInterface : Gtk.ApplicationWindow 0, _("PNG (lossless)"), 1, ".png", -1); +#if HAVE_WEBP + file_type_store.append (out iter); + file_type_store.set (iter, + /* Save dialog: Label for sabing in WEBP format */ + 0, _("WebP (compressed)"), + 1, ".webp", + -1); +#endif var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6); box.visible = true; save_dialog.set_extra_widget (box); - /* Label in save dialog beside combo box to choose file format (PDF, JPEG, PNG) */ + /* Label in save dialog beside combo box to choose file format (PDF, JPEG, PNG, WEBP) */ var label = new Gtk.Label (_("File format:")); label.visible = true; box.pack_start (label, false, false, 0); @@ -570,6 +370,23 @@ public class UserInterface : Gtk.ApplicationWindow var renderer = new Gtk.CellRendererText (); file_type_combo.pack_start (renderer, true); file_type_combo.add_attribute (renderer, "text", 0); + box.pack_start (file_type_combo, false, true, 0); + + /* Label in save dialog beside compression slider */ + var quality_label = new Gtk.Label (_("Compression:")); + box.pack_start (quality_label, false, false, 0); + + var quality_adjustment = new Gtk.Adjustment (75, 0, 100, 1, 10, 0); + var quality_scale = new Gtk.Scale (Gtk.Orientation.HORIZONTAL, quality_adjustment); + quality_scale.width_request = 200; + quality_scale.draw_value = false; + quality_scale.add_mark (0, Gtk.PositionType.BOTTOM, null); + quality_scale.add_mark (75, Gtk.PositionType.BOTTOM, null); + quality_scale.add_mark (90, Gtk.PositionType.BOTTOM, null); + quality_scale.add_mark (100, Gtk.PositionType.BOTTOM, null); + quality_adjustment.value = settings.get_int ("jpeg-quality"); + quality_adjustment.value_changed.connect (() => { settings.set_int ("jpeg-quality", (int) quality_adjustment.value); }); + box.pack_start (quality_scale, false, false, 0); file_type_combo.set_active (0); file_type_combo.changed.connect (() => @@ -588,8 +405,10 @@ public class UserInterface : Gtk.ApplicationWindow filename = filename.slice (0, extension_index); filename = filename + extension; save_dialog.set_current_name (filename); + + /* Quality not applicable to PNG */ + quality_scale.visible = quality_label.visible = (extension != ".png"); }); - box.pack_start (file_type_combo, false, false, 0); string? uri = null; while (true) @@ -615,10 +434,14 @@ public class UserInterface : Gtk.ApplicationWindow /* Check the file(s) don't already exist */ var files = new List<File> (); var format = uri_to_format (uri); +#if HAVE_WEBP + if (format == "jpeg" || format == "png" || format == "webp") +#else if (format == "jpeg" || format == "png") +#endif { for (var j = 0; j < book.n_pages; j++) - files.append (book.make_indexed_file (uri, j)); + files.append (make_indexed_file (uri, j, book.n_pages)); } else files.append (File.new_for_uri (uri)); @@ -650,7 +473,7 @@ public class UserInterface : Gtk.ApplicationWindow _("_Replace"), Gtk.ResponseType.ACCEPT); var response = dialog.run (); dialog.destroy (); - + if (response != Gtk.ResponseType.ACCEPT) return false; } @@ -665,11 +488,15 @@ public class UserInterface : Gtk.ApplicationWindow return "pdf"; else if (uri_lower.has_suffix (".png")) return "png"; +#if HAVE_WEBP + else if (uri_lower.has_suffix (".webp")) + return "webp"; +#endif else return "jpeg"; } - private bool save_document () + private async bool save_document_async () { var uri = choose_file_location (); if (uri == null) @@ -681,28 +508,34 @@ public class UserInterface : Gtk.ApplicationWindow var format = uri_to_format (uri); - show_progress_dialog (); + var cancellable = new Cancellable (); + var progress_bar = new CancellableProgressBar (_("Saving"), cancellable); + action_bar.pack_end (progress_bar); + progress_bar.visible = true; try { - book.save (format, quality, file); + yield book.save_async (format, settings.get_int ("jpeg-quality"), file, (fraction) => + { + progress_bar.set_fraction (fraction); + }, cancellable); } catch (Error e) { - hide_progress_dialog (); + progress_bar.destroy (); warning ("Error saving file: %s", e.message); - show_error (/* Title of error dialog when save failed */ - _("Failed to save file"), - e.message, - false); + show_error_dialog (/* Title of error dialog when save failed */ + _("Failed to save file"), + e.message); return false; } + progress_bar.destroy_with_delay (500); book_needs_saving = false; book_uri = uri; return true; } - private bool prompt_to_save (string title, string discard_label) + private async bool prompt_to_save_async (string title, string discard_label) { if (!book_needs_saving) return true; @@ -714,7 +547,7 @@ public class UserInterface : Gtk.ApplicationWindow "%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.")); + _("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); @@ -725,7 +558,7 @@ public class UserInterface : Gtk.ApplicationWindow switch (response) { case Gtk.ResponseType.YES: - if (save_document ()) + if (yield save_document_async ()) return true; else return false; @@ -738,32 +571,47 @@ public class UserInterface : Gtk.ApplicationWindow private void clear_document () { - book_view.default_page = new Page (default_page_width, - default_page_height, - default_page_dpi, - default_page_scan_direction); book.clear (); book_needs_saving = false; book_uri = null; save_menuitem.sensitive = false; email_menuitem.sensitive = false; - print_menuitem.sensitive = false; + print_menuitem.sensitive = false; save_button.sensitive = false; save_toolbutton.sensitive = false; copy_to_clipboard_menuitem.sensitive = false; + status_primary_label.set_text (/* Label shown when detected a scanner */ + _("Ready to Scan")); + stack.set_visible_child_name ("startup"); } private void new_document () { - if (!prompt_to_save (/* Text in dialog warning when a document is about to be lost */ - _("Save current document?"), - /* Button in dialog to create new document and discard unsaved document */ - _("Discard Changes"))) - return; + prompt_to_save_async.begin (/* Text in dialog warning when a document is about to be lost */ + _("Save current document?"), + /* Button in dialog to create new document and discard unsaved document */ + _("Discard Changes"), (obj, res) => + { + if (!prompt_to_save_async.end(res)) + return; - if (scanning) - stop_scan (); - clear_document (); + if (scanning) + stop_scan (); + + clear_document (); + }); + } + + [GtkCallback] + private bool status_label_activate_link_cb (Gtk.Label label, string uri) + { + if (uri == "install-firmware") + { + install_drivers (); + return true; + } + + return false; } [GtkCallback] @@ -812,109 +660,22 @@ public class UserInterface : Gtk.ApplicationWindow set_document_hint ("photo", true); } - private void set_page_side (ScanType page_side) - { - Gtk.TreeIter iter; - - if (page_side_model.get_iter_first (out iter)) - { - do - { - int s; - page_side_model.get (iter, 0, out s, -1); - if (s == page_side) - { - page_side_combo.set_active_iter (iter); - return; - } - } while (page_side_model.iter_next (ref iter)); - } - } - - private void set_paper_size (int width, int height) - { - Gtk.TreeIter iter; - bool have_iter; - - for (have_iter = paper_size_model.get_iter_first (out iter); - have_iter; - have_iter = paper_size_model.iter_next (ref iter)) - { - int w, h; - paper_size_model.get (iter, 0, out w, 1, out h, -1); - if (w == width && h == height) - break; - } - - if (!have_iter) - have_iter = paper_size_model.get_iter_first (out iter); - if (have_iter) - paper_size_combo.set_active_iter (iter); - } - - private int get_text_dpi () - { - Gtk.TreeIter iter; - int dpi = DEFAULT_TEXT_DPI; - - if (text_dpi_combo.get_active_iter (out iter)) - text_dpi_model.get (iter, 0, out dpi, -1); - - return dpi; - } - - private int get_photo_dpi () - { - Gtk.TreeIter iter; - int dpi = DEFAULT_PHOTO_DPI; - - if (photo_dpi_combo.get_active_iter (out iter)) - photo_dpi_model.get (iter, 0, out dpi, -1); - - return dpi; - } - - private ScanType get_page_side () - { - Gtk.TreeIter iter; - int page_side = ScanType.ADF_BOTH; - - if (page_side_combo.get_active_iter (out iter)) - page_side_model.get (iter, 0, out page_side, -1); - - return (ScanType) page_side; - } - - private bool get_paper_size (out int width, out int height) - { - Gtk.TreeIter iter; - - width = height = 0; - if (paper_size_combo.get_active_iter (out iter)) - { - paper_size_model.get (iter, 0, ref width, 1, ref height, -1); - return true; - } - - return false; - } - private ScanOptions make_scan_options () { var options = new ScanOptions (); if (document_hint == "text") { options.scan_mode = ScanMode.GRAY; - options.dpi = get_text_dpi (); + options.dpi = preferences_dialog.get_text_dpi (); options.depth = 2; } else { options.scan_mode = ScanMode.COLOR; - options.dpi = get_photo_dpi (); + options.dpi = preferences_dialog.get_photo_dpi (); options.depth = 8; } - get_paper_size (out options.paper_width, out options.paper_height); + preferences_dialog.get_paper_size (out options.paper_width, out options.paper_height); options.brightness = brightness; options.contrast = contrast; options.page_delay = page_delay; @@ -927,6 +688,8 @@ public class UserInterface : Gtk.ApplicationWindow { var options = make_scan_options (); options.type = ScanType.SINGLE; + status_primary_label.set_text (/* Label shown when scan started */ + _("Contacting scanner…")); start_scan (selected_device, options); } @@ -944,7 +707,7 @@ public class UserInterface : Gtk.ApplicationWindow else { var options = make_scan_options (); - options.type = get_page_side (); + options.type = preferences_dialog.get_page_side (); start_scan (selected_device, options); } } @@ -968,18 +731,6 @@ public class UserInterface : Gtk.ApplicationWindow preferences_dialog.present (); } - [GtkCallback] - private bool preferences_dialog_delete_event_cb (Gtk.Widget widget, Gdk.EventAny event) - { - return true; - } - - [GtkCallback] - private void preferences_dialog_response_cb (Gtk.Widget widget, int response_id) - { - preferences_dialog.visible = false; - } - private void update_page_menu () { var page = book_view.selected_page; @@ -1030,21 +781,18 @@ public class UserInterface : Gtk.ApplicationWindow menuitem.active = true; crop_button.active = page.has_crop; - crop_toolbutton.active = page.has_crop; updating_page_menu = false; } private void show_page_cb (BookView view, Page page) { - var path = get_temporary_filename ("scanned-page", "png"); - if (path == null) - return; - var file = File.new_for_path (path); - + File file; try { - page.save ("png", quality, file); + var dir = DirUtils.make_tmp ("simple-scan-XXXXXX"); + file = File.new_for_path (Path.build_filename (dir, "scan.png")); + page.save_png (file); } catch (Error e) { @@ -1135,30 +883,6 @@ public class UserInterface : Gtk.ApplicationWindow } [GtkCallback] - private void crop_button_toggled_cb (Gtk.ToggleButton widget) - { - if (updating_page_menu) - return; - - if (widget.active) - custom_crop_menuitem.active = true; - else - no_crop_menuitem.active = true; - } - - [GtkCallback] - private void crop_toolbutton_toggled_cb (Gtk.ToggleToolButton widget) - { - if (updating_page_menu) - return; - - if (widget.active) - custom_crop_menuitem.active = true; - else - no_crop_menuitem.active = true; - } - - [GtkCallback] private void four_by_six_menuitem_toggled_cb (Gtk.CheckMenuItem widget) { if (widget.active) @@ -1407,12 +1131,12 @@ public class UserInterface : Gtk.ApplicationWindow [GtkCallback] private void save_file_button_clicked_cb (Gtk.Widget widget) { - save_document (); + save_document_async.begin (); } public void save_document_activate_cb () { - save_document (); + save_document_async.begin (); } [GtkCallback] @@ -1451,12 +1175,38 @@ public class UserInterface : Gtk.ApplicationWindow [GtkCallback] private void email_button_clicked_cb (Gtk.Widget widget) { - email (document_hint, quality); + email_document_async.begin (); } public void email_document_activate_cb () { - email (document_hint, quality); + email_document_async.begin (); + } + + private async void email_document_async () + { + try + { + var dir = DirUtils.make_tmp ("simple-scan-XXXXXX"); + var type = document_hint == "text" ? "pdf" : "jpeg"; + var file = File.new_for_path (Path.build_filename (dir, "scan." + type)); + yield book.save_async (type, settings.get_int ("jpeg-quality"), file, null, null); + var command_line = "xdg-email"; + if (type == "pdf") + command_line += "--attach %s".printf (file.get_path ()); + else + { + for (var i = 0; i < book.n_pages; i++) { + var indexed_file = make_indexed_file (file.get_uri (), i, book.n_pages); + command_line += " --attach %s".printf (indexed_file.get_path ()); + } + } + Process.spawn_command_line_async (command_line); + } + catch (Error e) + { + warning ("Unable to email document: %s", e.message); + } } private void print_document () @@ -1552,22 +1302,23 @@ public class UserInterface : Gtk.ApplicationWindow show_about (); } - private bool on_quit () + private void on_quit () { - if (!prompt_to_save (/* Text in dialog warning when a document is about to be lost */ - _("Save document before quitting?"), - /* Button in dialog to quit and discard unsaved document */ - _("Quit without Saving"))) - return false; - - destroy (); + prompt_to_save_async.begin (/* Text in dialog warning when a document is about to be lost */ + _("Save document before quitting?"), + /* Text in dialog warning when a document is about to be lost */ + _("Quit without Saving"), (obj, res) => + { + if (!prompt_to_save_async.end(res)) + return; - if (save_state_timeout != 0) - save_state (true); + destroy (); - autosave_manager.cleanup (); + if (save_state_timeout != 0) + save_state (true); - return true; + autosave_manager.cleanup (); + }); } [GtkCallback] @@ -1592,28 +1343,6 @@ public class UserInterface : Gtk.ApplicationWindow } } - private void info_bar_response_cb (Gtk.InfoBar widget, int response_id) - { - switch (response_id) - { - /* Change scanner */ - case 1: - device_combo.grab_focus (); - preferences_dialog.present (); - break; - /* Install drivers */ - case 2: - install_drivers (); - break; - default: - have_error = false; - error_title = null; - error_text = null; - update_info_bar (); - break; - } - } - private void install_drivers () { var message = "", instructions = ""; @@ -1660,7 +1389,7 @@ public class UserInterface : Gtk.ApplicationWindow var instructions_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6); instructions_box.visible = true; dialog.get_content_area ().pack_start (instructions_box, true, true, 0); - + var stack = new Gtk.Stack (); instructions_box.pack_start (stack, false, false, 0); @@ -1674,14 +1403,14 @@ public class UserInterface : Gtk.ApplicationWindow var instructions_label = new Gtk.Label (instructions); instructions_label.visible = true; - instructions_label.xalign = 0f; + instructions_label.xalign = 0f; instructions_label.use_markup = true; instructions_box.pack_start (instructions_label, false, false, 0); label = new Gtk.Label (/* Message in driver install dialog */ _("Once installed you will need to restart Simple Scan.")); label.visible = true; - label.xalign = 0f; + label.xalign = 0f; dialog.get_content_area ().border_width = 12; dialog.get_content_area ().pack_start (label, true, true, 0); @@ -1691,7 +1420,7 @@ public class UserInterface : Gtk.ApplicationWindow stack.visible = true; spinner.active = true; instructions_label.set_text (/* Label shown while installing drivers */ - _("Installing drivers...")); + _("Installing drivers…")); install_packages.begin (packages_to_install, () => {}, (object, result) => { status_label.visible = true; @@ -1722,7 +1451,7 @@ public class UserInterface : Gtk.ApplicationWindow }); #else instructions_label.set_text (/* Label shown to prompt user to install packages (when PackageKit not available) */ - _("You need to install the %s package(s).").printf (string.joinv (", ", packages_to_install))); + 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 } @@ -1773,30 +1502,12 @@ public class UserInterface : Gtk.ApplicationWindow [GtkCallback] private bool window_delete_event_cb (Gtk.Widget widget, Gdk.EventAny event) { - return !on_quit (); - } - - private void page_size_changed_cb (Page page) - { - default_page_width = page.width; - default_page_height = page.height; - default_page_dpi = page.dpi; - save_state (); - } - - private void page_scan_direction_changed_cb (Page page) - { - default_page_scan_direction = page.scan_direction; - save_state (); + on_quit (); + return true; /* Let us quit on our own terms */ } private void page_added_cb (Book book, Page page) { - page_size_changed_cb (page); - default_page_scan_direction = page.scan_direction; - page.size_changed.connect (page_size_changed_cb); - page.scan_direction_changed.connect (page_scan_direction_changed_cb); - update_page_menu (); } @@ -1807,50 +1518,14 @@ public class UserInterface : Gtk.ApplicationWindow private void page_removed_cb (Book book, Page page) { - page.size_changed.disconnect (page_size_changed_cb); - page.scan_direction_changed.disconnect (page_scan_direction_changed_cb); - update_page_menu (); } - private void set_dpi_combo (Gtk.ComboBox combo, int default_dpi, int current_dpi) - { - var renderer = new Gtk.CellRendererText (); - combo.pack_start (renderer, true); - combo.add_attribute (renderer, "text", 1); - - var model = combo.model as Gtk.ListStore; - int[] scan_resolutions = {75, 150, 300, 600, 1200, 2400}; - foreach (var dpi in scan_resolutions) - { - string label; - if (dpi == default_dpi) - /* Preferences dialog: Label for default resolution in resolution list */ - label = _("%d dpi (default)").printf (dpi); - else if (dpi == 75) - /* Preferences dialog: Label for minimum resolution in resolution list */ - label = _("%d dpi (draft)").printf (dpi); - else if (dpi == 1200) - /* Preferences dialog: Label for maximum resolution in resolution list */ - label = _("%d dpi (high resolution)").printf (dpi); - else - /* Preferences dialog: Label for resolution value in resolution list (dpi = dots per inch) */ - label = _("%d dpi").printf (dpi); - - Gtk.TreeIter iter; - model.append (out iter); - model.set (iter, 0, dpi, 1, label, -1); - - if (dpi == current_dpi) - combo.set_active_iter (iter); - } - } - private void book_changed_cb (Book book) { save_menuitem.sensitive = true; email_menuitem.sensitive = true; - print_menuitem.sensitive = true; + print_menuitem.sensitive = true; save_button.sensitive = true; save_toolbutton.sensitive = true; book_needs_saving = true; @@ -1859,13 +1534,19 @@ public class UserInterface : Gtk.ApplicationWindow private void load () { + var use_header_bar = !is_traditional_desktop (); + + preferences_dialog = new PreferencesDialog (settings, use_header_bar); + preferences_dialog.delete_event.connect (() => { return true; }); + preferences_dialog.response.connect (() => { preferences_dialog.visible = false; }); + Gtk.IconTheme.get_default ().append_search_path (ICON_DIR); Gtk.Window.set_default_icon_name ("scanner"); var app = Application.get_default () as Gtk.Application; - if (is_traditional_desktop ()) + if (!use_header_bar) { set_titlebar (null); menubar.visible = true; @@ -1873,28 +1554,22 @@ public class UserInterface : Gtk.ApplicationWindow } else { + /* Set HeaderBar title here because Glade doesn't keep it translated */ + /* https://bugzilla.gnome.org/show_bug.cgi?id=782753 */ + /* Title of scan window */ + header_bar.title = _("Simple Scan"); + app.add_action_entries (action_entries, this); var appmenu = new Menu (); - var section = new Menu (); - appmenu.append_section (null, section); - section.append (_("New Document"), "app.new_document"); - section = new Menu (); - appmenu.append_section (null, section); - var menu = new Menu (); - section.append_submenu (_("Document"), menu); - menu.append (_("Reorder Pages"), "app.reorder"); - menu.append (_("Save"), "app.save"); - menu.append (_("Email..."), "app.email"); - menu.append (_("Print..."), "app.print"); - - section = new Menu (); + var section = new Menu (); appmenu.append_section (null, section); section.append (_("Preferences"), "app.preferences"); section = new Menu (); appmenu.append_section (null, section); + section.append (_("Keyboard Shortcuts"), "win.show-help-overlay"); section.append (_("Help"), "app.help"); section.append (_("About"), "app.about"); section.append (_("Quit"), "app.quit"); @@ -1907,127 +1582,73 @@ public class UserInterface : Gtk.ApplicationWindow app.add_accelerator ("<Ctrl>P", "app.print", null); app.add_accelerator ("F1", "app.help", null); app.add_accelerator ("<Ctrl>Q", "app.quit", null); + + var gear_menu = new Menu (); + section = new Menu (); + gear_menu.append_section (null, section); + section.append (_("Email"), "app.email"); + section.append (_("Reorder Pages"), "app.reorder"); + section.append (_("Preferences"), "app.preferences"); + menu_button.set_menu_model (gear_menu); } app.add_window (this); - /* Add InfoBar (not supported in Glade) */ - info_bar = new Gtk.InfoBar (); - info_bar.response.connect (info_bar_response_cb); - main_vbox.pack_start (info_bar, false, true, 0); - var hbox = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12); - var content_area = info_bar.get_content_area () as Gtk.Container; - content_area.add (hbox); - hbox.visible = true; - - info_bar_image = new Gtk.Image.from_icon_name ("dialog-warning", Gtk.IconSize.DIALOG); - hbox.pack_start (info_bar_image, false, true, 0); - info_bar_image.visible = true; - - info_bar_label = new Gtk.Label (null); - info_bar_label.set_alignment (0.0f, 0.5f); - hbox.pack_start (info_bar_label, true, true, 0); - info_bar_label.visible = true; - - info_bar_close_button = info_bar.add_button (_("_Close"), Gtk.ResponseType.CLOSE) as Gtk.Button; - info_bar_change_scanner_button = info_bar.add_button (/* Button in error infobar to open preferences dialog and change scanner */ - _("Change _Scanner"), 1) as Gtk.Button; - info_bar_install_button = info_bar.add_button (/* Button in error infobar to prompt user to install drivers */ - _("_Install Drivers"), 2) as Gtk.Button; + /* Populate ActionBar (not supported in Glade) */ + /* https://bugzilla.gnome.org/show_bug.cgi?id=769966 */ + var button = new Gtk.Button.with_label (/* Label on new document button */ + _("Start Again…")); + button.visible = true; + button.clicked.connect (new_button_clicked_cb); + action_bar.pack_start (button); - Gtk.TreeIter iter; - paper_size_model.append (out iter); - paper_size_model.set (iter, 0, 0, 1, 0, 2, - /* Combo box value for automatic paper size */ - _("Automatic"), -1); - paper_size_model.append (out iter); - paper_size_model.set (iter, 0, 1050, 1, 1480, 2, "A6", -1); - paper_size_model.append (out iter); - paper_size_model.set (iter, 0, 1480, 1, 2100, 2, "A5", -1); - paper_size_model.append (out iter); - paper_size_model.set (iter, 0, 2100, 1, 2970, 2, "A4", -1); - paper_size_model.append (out iter); - paper_size_model.set (iter, 0, 2159, 1, 2794, 2, "Letter", -1); - paper_size_model.append (out iter); - paper_size_model.set (iter, 0, 2159, 1, 3556, 2, "Legal", -1); - paper_size_model.append (out iter); - paper_size_model.set (iter, 0, 1016, 1, 1524, 2, "4×6", -1); - - var dpi = settings.get_int ("text-dpi"); - if (dpi <= 0) - dpi = DEFAULT_TEXT_DPI; - set_dpi_combo (text_dpi_combo, DEFAULT_TEXT_DPI, dpi); - text_dpi_combo.changed.connect (() => { settings.set_int ("text-dpi", get_text_dpi ()); }); - dpi = settings.get_int ("photo-dpi"); - if (dpi <= 0) - dpi = DEFAULT_PHOTO_DPI; - set_dpi_combo (photo_dpi_combo, DEFAULT_PHOTO_DPI, dpi); - photo_dpi_combo.changed.connect (() => { settings.set_int ("photo-dpi", get_photo_dpi ()); }); + var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 10); + box.visible = true; + action_bar.set_center_widget (box); + + var rotate_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); + rotate_box.get_style_context ().add_class (Gtk.STYLE_CLASS_LINKED); + rotate_box.visible = true; + box.pack_start (rotate_box, false, true, 0); + + button = new Gtk.Button.from_icon_name ("object-rotate-left-symbolic"); + button.visible = true; + /* Tooltip for rotate left (counter-clockwise) button */ + button.tooltip_text = _("Rotate the page to the left (counter-clockwise)"); + button.clicked.connect (rotate_left_button_clicked_cb); + rotate_box.pack_start (button, false, true, 0); + + button = new Gtk.Button.from_icon_name ("object-rotate-right-symbolic"); + button.visible = true; + /* Tooltip for rotate right (clockwise) button */ + button.tooltip_text = _("Rotate the page to the right (clockwise)"); + button.clicked.connect (rotate_right_button_clicked_cb); + rotate_box.pack_start (button, false, true, 0); + + crop_button = new Gtk.ToggleButton (); + crop_button.visible = true; + var image = new Gtk.Image.from_icon_name ("edit-cut-symbolic", Gtk.IconSize.BUTTON); + image.visible = true; + crop_button.add (image); + /* Tooltip for crop button */ + crop_button.tooltip_text = _("Crop the selected page"); + crop_button.toggled.connect ((widget) => + { + if (updating_page_menu) + return; - var renderer = new Gtk.CellRendererText (); - device_combo.pack_start (renderer, true); - device_combo.add_attribute (renderer, "text", 1); - - renderer = new Gtk.CellRendererText (); - page_side_combo.pack_start (renderer, true); - page_side_combo.add_attribute (renderer, "text", 1); - set_page_side ((ScanType) settings.get_enum ("page-side")); - page_side_combo.changed.connect (() => { settings.set_enum ("page-side", get_page_side ()); }); - - renderer = new Gtk.CellRendererText (); - paper_size_combo.pack_start (renderer, true); - paper_size_combo.add_attribute (renderer, "text", 2); - var paper_width = settings.get_int ("paper-width"); - var paper_height = settings.get_int ("paper-height"); - set_paper_size (paper_width, paper_height); - paper_size_combo.changed.connect (() => - { - int w, h; - get_paper_size (out w, out h); - settings.set_int ("paper-width", w); - settings.set_int ("paper-height", h); + if (widget.active) + custom_crop_menuitem.active = true; + else + no_crop_menuitem.active = true; }); + box.pack_start (crop_button, false, true, 0); - var lower = brightness_adjustment.lower; - var darker_label = "<small>%s</small>".printf (_("Darker")); - var upper = brightness_adjustment.upper; - var lighter_label = "<small>%s</small>".printf (_("Lighter")); - brightness_scale.add_mark (lower, Gtk.PositionType.BOTTOM, darker_label); - brightness_scale.add_mark (0, Gtk.PositionType.BOTTOM, null); - brightness_scale.add_mark (upper, Gtk.PositionType.BOTTOM, lighter_label); - brightness = settings.get_int ("brightness"); - brightness_adjustment.value_changed.connect (() => { settings.set_int ("brightness", brightness); }); - - lower = contrast_adjustment.lower; - var less_label = "<small>%s</small>".printf (_("Less")); - upper = contrast_adjustment.upper; - var more_label = "<small>%s</small>".printf (_("More")); - contrast_scale.add_mark (lower, Gtk.PositionType.BOTTOM, less_label); - contrast_scale.add_mark (0, Gtk.PositionType.BOTTOM, null); - contrast_scale.add_mark (upper, Gtk.PositionType.BOTTOM, more_label); - contrast = settings.get_int ("contrast"); - contrast_adjustment.value_changed.connect (() => { settings.set_int ("contrast", contrast); }); - - lower = quality_adjustment.lower; - var minimum_label = "<small>%s</small>".printf (_("Minimum")); - upper = quality_adjustment.upper; - var maximum_label = "<small>%s</small>".printf (_("Maximum")); - quality_scale.add_mark (lower, Gtk.PositionType.BOTTOM, minimum_label); - quality_scale.add_mark (75, Gtk.PositionType.BOTTOM, null); - quality_scale.add_mark (upper, Gtk.PositionType.BOTTOM, maximum_label); - quality = settings.get_int ("jpeg-quality"); - quality_adjustment.value_changed.connect (() => { settings.set_int ("jpeg-quality", quality); }); - - page_delay_scale.add_mark (0, Gtk.PositionType.BOTTOM, null); - page_delay_scale.add_mark (500, Gtk.PositionType.BOTTOM, null); - page_delay_scale.add_mark (1000, Gtk.PositionType.BOTTOM, null); - page_delay_scale.add_mark (2000, Gtk.PositionType.BOTTOM, null); - page_delay_scale.add_mark (4000, Gtk.PositionType.BOTTOM, null); - page_delay_scale.add_mark (6000, Gtk.PositionType.BOTTOM, null); - page_delay_scale.add_mark (8000, Gtk.PositionType.BOTTOM, null); - page_delay_scale.add_mark (10000, Gtk.PositionType.BOTTOM, null); - page_delay = settings.get_int ("page-delay"); - page_delay_scale.format_value.connect ((value) => { return "%.1fs".printf (value / 1000.0); }); - page_delay_adjustment.value_changed.connect (() => { settings.set_int ("page-delay", page_delay); }); + delete_button = new Gtk.Button.from_icon_name ("user-trash-symbolic"); + delete_button.visible = true; + /* Tooltip for delete button */ + delete_button.tooltip_text = _("Delete the selected page"); + delete_button.clicked.connect (() => { book_view.book.delete_page (book_view.selected_page); }); + box.pack_start (delete_button, false, true, 0); var document_type = settings.get_string ("document-type"); if (document_type != null) @@ -2035,13 +1656,12 @@ public class UserInterface : Gtk.ApplicationWindow book_view = new BookView (book); book_view.border_width = 18; - main_vbox.pack_end (book_view, true, true, 0); + main_vbox.pack_start (book_view, true, true, 0); book_view.page_selected.connect (page_selected_cb); book_view.show_page.connect (show_page_cb); book_view.show_menu.connect (show_page_menu_cb); book_view.visible = true; - authorize_dialog.transient_for = this; preferences_dialog.transient_for = this; /* Load previous state */ @@ -2060,9 +1680,6 @@ public class UserInterface : Gtk.ApplicationWindow debug ("Restoring window to fullscreen"); fullscreen (); } - - progress_dialog = new ProgressBarDialog (this, _("Saving document...")); - book.saving.connect (book_saving_cb); } private bool is_desktop (string name) @@ -2080,12 +1697,12 @@ public class UserInterface : Gtk.ApplicationWindow private bool is_traditional_desktop () { - const string[] traditional_desktops = { "Unity", "XFCE", "MATE", "LXDE", "Cinnamon", "X-Cinnamon" }; + const string[] traditional_desktops = { "Unity", "XFCE", "MATE", "LXDE", "Cinnamon", "X-Cinnamon", "i3" }; foreach (var name in traditional_desktops) if (is_desktop (name)) return true; return false; - } + } private string state_filename { @@ -2114,25 +1731,6 @@ public class UserInterface : Gtk.ApplicationWindow window_height = 400; window_is_maximized = state_get_boolean (f, "window", "is-maximized"); window_is_fullscreen = state_get_boolean (f, "window", "is-fullscreen"); - default_page_width = state_get_integer (f, "last-page", "width", 595); - default_page_height = state_get_integer (f, "last-page", "height", 842); - default_page_dpi = state_get_integer (f, "last-page", "dpi", 72); - switch (state_get_string (f, "last-page", "scan-direction", "top-to-bottom")) - { - default: - case "top-to-bottom": - default_page_scan_direction = ScanDirection.TOP_TO_BOTTOM; - break; - case "bottom-to-top": - default_page_scan_direction = ScanDirection.BOTTOM_TO_TOP; - break; - case "left-to-right": - default_page_scan_direction = ScanDirection.LEFT_TO_RIGHT; - break; - case "right-to-left": - default_page_scan_direction = ScanDirection.RIGHT_TO_LEFT; - break; - } } private int state_get_integer (KeyFile f, string group_name, string key, int default = 0) @@ -2159,18 +1757,6 @@ public class UserInterface : Gtk.ApplicationWindow } } - private string state_get_string (KeyFile f, string group_name, string key, string default = "") - { - try - { - return f.get_string (group_name, key); - } - catch - { - return default; - } - } - private void save_state (bool force = false) { if (!force) @@ -2192,25 +1778,7 @@ public class UserInterface : Gtk.ApplicationWindow f.set_integer ("window", "width", window_width); f.set_integer ("window", "height", window_height); f.set_boolean ("window", "is-maximized", window_is_maximized); - f.set_boolean ("window", "is-fullscreen", window_is_fullscreen); - f.set_integer ("last-page", "width", default_page_width); - f.set_integer ("last-page", "height", default_page_height); - f.set_integer ("last-page", "dpi", default_page_dpi); - switch (default_page_scan_direction) - { - case ScanDirection.TOP_TO_BOTTOM: - f.set_value ("last-page", "scan-direction", "top-to-bottom"); - break; - case ScanDirection.BOTTOM_TO_TOP: - f.set_value ("last-page", "scan-direction", "bottom-to-top"); - break; - case ScanDirection.LEFT_TO_RIGHT: - f.set_value ("last-page", "scan-direction", "left-to-right"); - break; - case ScanDirection.RIGHT_TO_LEFT: - f.set_value ("last-page", "scan-direction", "right-to-left"); - break; - } + f.set_boolean ("window", "is-fullscreen", window_is_fullscreen); try { FileUtils.set_contents (state_filename, f.to_data ()); @@ -2221,178 +1789,52 @@ public class UserInterface : Gtk.ApplicationWindow } } - private void book_saving_cb (int page_number) - { - /* Prevent GUI from freezing */ - while (Gtk.events_pending ()) - Gtk.main_iteration (); - - var total = (int) book.n_pages; - var fraction = (page_number + 1.0) / total; - var complete = fraction == 1.0; - if (complete) - Timeout.add (500, () => { - progress_dialog.visible = false; - return false; - }); - var message = _("Saving page %d out of %d").printf (page_number + 1, total); - - progress_dialog.fraction = fraction; - progress_dialog.message = message; - } - - public void show_progress_dialog () - { - progress_dialog.visible = true; - } - - public void hide_progress_dialog () - { - progress_dialog.visible = false; - } - - public void show_error (string error_title, string error_text, bool change_scanner_hint) - { - have_error = true; - this.error_title = error_title; - this.error_text = error_text; - error_change_scanner_hint = change_scanner_hint; - update_info_bar (); - } - public void start () { visible = true; } } -private class ProgressBarDialog : Gtk.Window +private class CancellableProgressBar : Gtk.HBox { private Gtk.ProgressBar bar; + private Gtk.Button? button; - public double fraction - { - get { return bar.fraction; } - set { bar.fraction = value; } - } - - public string message - { - get { return bar.text; } - set { bar.text = value; } - } - - public ProgressBarDialog (Gtk.ApplicationWindow parent, string title) + public CancellableProgressBar (string? text, Cancellable? cancellable) { bar = new Gtk.ProgressBar (); - var hbox = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 5); - var vbox = new Gtk.Box (Gtk.Orientation.VERTICAL, 5); - hbox.hexpand = true; - - bar.text = ""; - bar.show_text = true; - bar.set_size_request (225, 25); - set_size_request (250, 50); - - vbox.pack_start (bar, true, false, 0); - hbox.pack_start (vbox, true, false, 0); - add (hbox); - this.title = title; - - transient_for = parent; - set_position (Gtk.WindowPosition.CENTER_ON_PARENT); - modal = true; - resizable = false; - - hbox.visible = true; - vbox.visible = true; bar.visible = true; - } -} - -// FIXME: Duplicated from simple-scan.vala -private string? get_temporary_filename (string prefix, string extension) -{ - /* NOTE: I'm not sure if this is a 100% safe strategy to use g_file_open_tmp(), close and - * use the filename but it appears to work in practise */ - - var filename = "%sXXXXXX.%s".printf (prefix, extension); - string path; - try - { - var fd = FileUtils.open_tmp (filename, out path); - Posix.close (fd); - } - catch (Error e) - { - warning ("Error saving email attachment: %s", e.message); - return null; - } - - return path; -} - -private class PageIcon : Gtk.DrawingArea -{ - private string text; - private double r; - private double g; - private double b; - private const int MINIMUM_WIDTH = 20; - - public PageIcon (string text, double r = 1.0, double g = 1.0, double b = 1.0) - { - this.text = text; - this.r = r; - this.g = g; - this.b = b; - } - - public override void get_preferred_width (out int minimum_width, out int natural_width) - { - minimum_width = natural_width = MINIMUM_WIDTH; - } - - public override void get_preferred_height (out int minimum_height, out int natural_height) - { - minimum_height = natural_height = (int) Math.round (MINIMUM_WIDTH * Math.SQRT2); - } + bar.set_text (text); + bar.set_show_text (true); + pack_start (bar); - 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); + if (cancellable != null) + { + button = new Gtk.Button.with_label (/* Text of button for cancelling save */ + _("Cancel")); + button.visible = true; + button.clicked.connect (() => + { + set_visible (false); + cancellable.cancel (); + }); + pack_start (button); + } } - public override void get_preferred_width_for_height (int height, out int minimum_width, out int natural_width) + public void set_fraction (double fraction) { - minimum_width = natural_width = (int) (height / Math.SQRT2); + bar.set_fraction (fraction); } - public override bool draw (Cairo.Context c) + public void destroy_with_delay (uint delay) { - var w = get_allocated_width (); - var h = get_allocated_height (); - if (w * Math.SQRT2 > h) - w = (int) Math.round (h / Math.SQRT2); - else - h = (int) Math.round (w * Math.SQRT2); - - c.translate ((get_allocated_width () - w) / 2, (get_allocated_height () - h) / 2); - - c.rectangle (0.5, 0.5, w - 1, h - 1); - - c.set_source_rgb (r, g, b); - c.fill_preserve (); - - c.set_line_width (1.0); - c.set_source_rgb (0.0, 0.0, 0.0); - c.stroke (); + button.set_sensitive (false); - Cairo.TextExtents extents; - c.text_extents (text, out extents); - c.translate ((w - extents.width) * 0.5 - 0.5, (h + extents.height) * 0.5 - 0.5); - c.show_text (text); - - return true; + Timeout.add (delay, () => + { + this.destroy (); + return false; + }); } } diff --git a/src/authorize-dialog.ui b/src/authorize-dialog.ui new file mode 100644 index 0000000..c099563 --- /dev/null +++ b/src/authorize-dialog.ui @@ -0,0 +1,141 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.20.0 --> +<interface> + <requires lib="gtk+" version="3.10"/> + <template class="AuthorizeDialog" parent="GtkDialog"> + <property name="can_focus">False</property> + <property name="border_width">12</property> + <property name="resizable">False</property> + <property name="modal">True</property> + <property name="type_hint">normal</property> + <property name="urgency_hint">True</property> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="dialog-action_area1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="authorize_button"> + <property name="label" translatable="yes" comments="Button to submit authorization dialog">_Authorize</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="vbox5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel" id="authorize_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" comments="This label is set dynamically and is not translated">To connect to ? you need to authorize</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkGrid" id="grid2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">6</property> + <property name="column_spacing">6</property> + <child> + <object class="GtkEntry" id="username_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + <property name="invisible_char">●</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="password_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + <property name="visibility">False</property> + <property name="invisible_char">●</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="username_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes" comments="Label beside username entry">_Username for resource:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">username_entry</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="password_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes" comments="Label beside password entry">_Password:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">password_entry</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="0">authorize_button</action-widget> + </action-widgets> + </template> +</interface> diff --git a/src/authorize-dialog.vala b/src/authorize-dialog.vala new file mode 100644 index 0000000..a6e5ab0 --- /dev/null +++ b/src/authorize-dialog.vala @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2009-2017 Canonical Ltd. + * Author: Robert Ancell <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. + */ + +[GtkTemplate (ui = "/org/gnome/SimpleScan/authorize-dialog.ui")] +private class AuthorizeDialog : Gtk.Dialog +{ + [GtkChild] + private Gtk.Label authorize_label; + [GtkChild] + private Gtk.Entry username_entry; + [GtkChild] + private Gtk.Entry password_entry; + + public AuthorizeDialog (string title) + { + authorize_label.set_text (title); + } + + public string get_username () + { + return username_entry.text; + } + + public string get_password () + { + return password_entry.text; + } +} diff --git a/src/book-view.vala b/src/book-view.vala index 182edc7..9a9c9bb 100644 --- a/src/book-view.vala +++ b/src/book-view.vala @@ -23,20 +23,6 @@ public class BookView : Gtk.Box private bool laying_out; private bool show_selected_page; - /* Page to show when book empty */ - private PageView? default_page_view = null; - public Page default_page - { - set - { - if (value == null) - default_page_view = null; - else - default_page_view = new PageView (value); - need_layout = true; - } - } - /* Currently selected page */ private PageView? selected_page_view = null; public Page? selected_page @@ -310,16 +296,8 @@ public class BookView : Gtk.Box private void layout_into (int width, int height, out int book_width, out int book_height) { var pages = new List<PageView> (); - if (book.n_pages == 0) - { - if (default_page_view != null) - pages.append (default_page_view); - } - else - { - for (var i = 0; i < book.n_pages; i++) - pages.append (get_nth_page (i)); - } + for (var i = 0; i < book.n_pages; i++) + pages.append (get_nth_page (i)); /* Get maximum page resolution */ int max_dpi = 0; @@ -461,16 +439,8 @@ public class BookView : Gtk.Box context.clip_extents (out left, out top, out right, out bottom); var pages = new List<PageView> (); - if (book.n_pages == 0) - { - if (default_page_view != null) - pages.append (default_page_view); - } - else - { - for (var i = 0; i < book.n_pages; i++) - pages.append (get_nth_page (i)); - } + for (var i = 0; i < book.n_pages; i++) + pages.append (get_nth_page (i)); /* Render each page */ foreach (var page in pages) diff --git a/src/book.vala b/src/book.vala index a843981..d53b31a 100644 --- a/src/book.vala +++ b/src/book.vala @@ -9,6 +9,8 @@ * license. */ +public delegate void ProgressionCallback (double fraction); + public class Book { private List<Page> pages; @@ -20,7 +22,6 @@ public class Book public signal void reordered (); public signal void cleared (); public signal void changed (); - public signal void saving (int i); public Book () { @@ -135,92 +136,440 @@ public class Book return pages.index (page); } - public File make_indexed_file (string uri, int i) + public async void save_async (string t, int q, File f, ProgressionCallback? p, Cancellable? c) throws Error { - if (n_pages == 1) - return File.new_for_uri (uri); - - /* Insert index before extension */ - var basename = Path.get_basename (uri); - string prefix = uri, suffix = ""; - var extension_index = basename.last_index_of_char ('.'); - if (extension_index >= 0) - { - suffix = basename.slice (extension_index, basename.length); - prefix = uri.slice (0, uri.length - suffix.length); - } - var width = n_pages.to_string().length; - var number_format = "%%0%dd".printf (width); - var filename = prefix + "-" + number_format.printf (i + 1) + suffix; - return File.new_for_uri (filename); + var book_saver = new BookSaver (); + yield book_saver.save_async (this, t, q, f, p, c); } +} - private void save_multi_file (string type, int quality, File file) throws Error +private class BookSaver +{ + private uint n_pages; + private int quality; + private File file; + private unowned ProgressionCallback progression_callback; + private double progression; + private Mutex progression_mutex; + private Cancellable? cancellable; + private AsyncQueue<WriteTask> write_queue; + private ThreadPool<EncodeTask> encoder; + private SourceFunc save_async_callback; + + /* save_async get called in the main thread to start saving. It + * distributes all encode tasks to other threads then yield so + * the ui can continue operating. The method then return once saving + * is completed, cancelled, or failed */ + public async void save_async (Book book, string type, int quality, File file, ProgressionCallback? progression_callback, Cancellable? cancellable) throws Error { + var timer = new Timer (); + + this.n_pages = book.n_pages; + this.quality = quality; + this.file = file; + this.cancellable = cancellable; + this.save_async_callback = save_async.callback; + this.write_queue = new AsyncQueue<WriteTask> (); + this.progression = 0; + this.progression_mutex = Mutex (); + + /* Configure a callback that monitor saving progression */ + if (progression_callback == null) + this.progression_callback = (fraction) => + { + debug ("Save progression: %f%%", fraction*100.0); + }; + else + this.progression_callback = progression_callback; + + /* Configure an encoder */ + ThreadPoolFunc<EncodeTask>? encode_delegate = null; + switch (type) + { + case "jpeg": + encode_delegate = encode_jpeg; + break; + case "png": + encode_delegate = encode_png; + break; +#if HAVE_WEBP + case "webp": + encode_delegate = encode_webp; + break; +#endif + case "pdf": + encode_delegate = encode_pdf; + break; + } + encoder = new ThreadPool<EncodeTask>.with_owned_data (encode_delegate, (int) get_num_processors (), false); + + /* Configure a writer */ + ThreadFunc<Error?>? write_delegate = null; + switch (type) + { + case "jpeg": + case "png": +#if HAVE_WEBP + case "webp": +#endif + write_delegate = write_multifile; + break; + case "pdf": + write_delegate = write_pdf; + break; + } + var writer = new Thread<Error?> (null, write_delegate); + + /* Issue encode tasks */ for (var i = 0; i < n_pages; i++) { - var page = get_page (i); - page.save (type, quality, make_indexed_file (file.get_uri (), i)); - saving (i); + var encode_task = new EncodeTask (); + encode_task.number = i; + encode_task.page = book.get_page(i); + encoder.add ((owned) encode_task); } + + /* Waiting for saving to finish */ + yield; + + /* Any error from any thread ends up here */ + var error = writer.join (); + if (error != null) + throw error; + + timer.stop (); + debug ("Save time: %f seconds", timer.elapsed (null)); } - private uint8[]? compress_zlib (uint8[] data) + /* Those methods are run in the encoder threads pool. It process + * one encode_task issued by save_async and reissue the result with + * a write_task */ + + private void encode_png (owned EncodeTask encode_task) { - var stream = ZLib.DeflateStream (ZLib.Level.BEST_COMPRESSION); - var out_data = new uint8[data.length]; + var page = encode_task.page; + var icc_data = page.get_icc_data_encoded (); + var write_task = new WriteTask (); + var image = page.get_image (true); - stream.next_in = data; - stream.next_out = out_data; - while (stream.avail_in > 0) + string[] keys = { "x-dpi", "y-dpi", "icc-profile", null }; + string[] values = { "%d".printf (page.dpi), "%d".printf (page.dpi), icc_data, null }; + if (icc_data == null) + keys[2] = null; + + try { - if (stream.deflate (ZLib.Flush.FINISH) == ZLib.Status.STREAM_ERROR) - break; + image.save_to_bufferv (out write_task.data, "png", keys, values); } + catch (Error error) + { + write_task.error = error; + } + write_task.number = encode_task.number; + write_queue.push ((owned) write_task); - if (stream.avail_in > 0) - return null; + update_progression (); + } - var n_written = data.length - stream.avail_out; - out_data.resize ((int) n_written); + private void encode_jpeg (owned EncodeTask encode_task) + { + var page = encode_task.page; + var icc_data = page.get_icc_data_encoded (); + var write_task = new WriteTask (); + var image = page.get_image (true); - return out_data; + string[] keys = { "x-dpi", "y-dpi", "quality", "icc-profile", null }; + string[] values = { "%d".printf (page.dpi), "%d".printf (page.dpi), "%d".printf (quality), icc_data, null }; + if (icc_data == null) + keys[3] = null; + + try + { + image.save_to_bufferv (out write_task.data, "jpeg", keys, values); + } + catch (Error error) + { + write_task.error = error; + } + write_task.number = encode_task.number; + write_queue.push ((owned) write_task); + + update_progression (); } - private ByteArray jpeg_data; +#if HAVE_WEBP + private void encode_webp (owned EncodeTask encode_task) + { + var page = encode_task.page; + var icc_data = page.get_icc_data_encoded (); + var write_task = new WriteTask (); + var image = page.get_image (true); + var webp_data = WebP.encode_rgb (image.get_pixels (), + image.get_width (), + image.get_height (), + image.get_rowstride (), + (float) quality); +#if HAVE_COLORD + WebP.MuxError mux_error; + var mux = WebP.Mux.new_mux (); + uint8[] output; + + mux_error = mux.set_image (webp_data, false); + debug ("mux.set_image: %s", mux_error.to_string ()); + + if (icc_data != null) + { + mux_error = mux.set_chunk ("ICCP", icc_data.data, false); + debug ("mux.set_chunk: %s", mux_error.to_string ()); + if (mux_error != WebP.MuxError.OK) + warning ("icc profile data not saved with page %i", encode_task.number); + } + + mux_error = mux.assemble (out output); + debug ("mux.assemble: %s", mux_error.to_string ()); + if (mux_error != WebP.MuxError.OK) + write_task.error = new FileError.FAILED (_("Unable to encode page %i").printf (encode_task.number)); - private uint8[] compress_jpeg (Gdk.Pixbuf image, int quality, int dpi) + write_task.data = (owned) output; +#else + + if (webp_data.length == 0) + write_task.error = new FileError.FAILED (_("Unable to encode page %i").printf (encode_task.number)); + + write_task.data = (owned) webp_data; +#endif + write_task.number = encode_task.number; + write_queue.push ((owned) write_task); + + update_progression (); + } +#endif + + private void encode_pdf (owned EncodeTask encode_task) { - jpeg_data = new ByteArray (); - string[] keys = { "quality", "density-unit", "x-density", "y-density", null }; - string[] values = { "%d".printf (quality), "dots-per-inch", "%d".printf (dpi), "%d".printf (dpi), null }; + var page = encode_task.page; + var image = page.get_image (true); + var width = image.width; + var height = image.height; + unowned uint8[] pixels = image.get_pixels (); + int depth = 8; + string color_space = "DeviceRGB"; + string? filter = null; + uint8[] data; + + if (page.is_color) + { + depth = 8; + color_space = "DeviceRGB"; + var data_length = height * width * 3; + data = new uint8[data_length]; + for (var row = 0; row < height; row++) + { + var in_offset = row * image.rowstride; + var out_offset = row * width * 3; + for (var x = 0; x < width; x++) + { + var in_o = in_offset + x*3; + var out_o = out_offset + x*3; + + data[out_o] = pixels[in_o]; + data[out_o+1] = pixels[in_o+1]; + data[out_o+2] = pixels[in_o+2]; + } + } + } + else if (page.depth == 2) + { + int shift_count = 6; + depth = 2; + color_space = "DeviceGray"; + var data_length = height * ((width * 2 + 7) / 8); + data = new uint8[data_length]; + var offset = 0; + for (var row = 0; row < height; row++) + { + /* Pad to the next line */ + if (shift_count != 6) + { + offset++; + shift_count = 6; + } + + var in_offset = row * image.rowstride; + for (var x = 0; x < width; x++) + { + /* Clear byte */ + if (shift_count == 6) + data[offset] = 0; + + /* Set bits */ + var p = pixels[in_offset + x*3]; + if (p >= 192) + data[offset] |= 3 << shift_count; + else if (p >= 128) + data[offset] |= 2 << shift_count; + else if (p >= 64) + data[offset] |= 1 << shift_count; + + /* Move to the next position */ + if (shift_count == 0) + { + offset++; + shift_count = 6; + } + else + shift_count -= 2; + } + } + } + else if (page.depth == 1) + { + int mask = 0x80; + + depth = 1; + color_space = "DeviceGray"; + var data_length = height * ((width + 7) / 8); + data = new uint8[data_length]; + var offset = 0; + for (var row = 0; row < height; row++) + { + /* Pad to the next line */ + if (mask != 0x80) + { + offset++; + mask = 0x80; + } + + var in_offset = row * image.rowstride; + for (var x = 0; x < width; x++) + { + /* Clear byte */ + if (mask == 0x80) + data[offset] = 0; + + /* Set bit */ + if (pixels[in_offset+x*3] != 0) + data[offset] |= (uint8) mask; + + /* Move to the next bit */ + mask >>= 1; + if (mask == 0) + { + offset++; + mask = 0x80; + } + } + } + } + else + { + depth = 8; + color_space = "DeviceGray"; + var data_length = height * width; + data = new uint8 [data_length]; + for (var row = 0; row < height; row++) + { + var in_offset = row * image.rowstride; + var out_offset = row * width; + for (var x = 0; x < width; x++) + data[out_offset+x] = pixels[in_offset+x*3]; + } + } + + /* Compress data and use zlib compression if it is smaller than JPEG. + * zlib compression is slower in the worst case, so do JPEG first + * and stop zlib if it exceeds the JPEG size */ + var write_task = new WriteTaskPDF (); + uint8[]? jpeg_data = null; try { - image.save_to_callbackv (write_pixbuf_data, "jpeg", keys, values); + jpeg_data = compress_jpeg (image, quality, page.dpi); } - catch (Error e) + catch (Error error) + { + write_task.error = error; + } + var zlib_data = compress_zlib (data, jpeg_data.length); + if (zlib_data != null) { + filter = "FlateDecode"; + data = zlib_data; + } + else + { + filter = "DCTDecode"; + data = jpeg_data; } - var data = (owned) jpeg_data.data; - jpeg_data = null; - return data; + write_task.number = encode_task.number; + write_task.data = data; + write_task.width = width; + write_task.height = height; + write_task.color_space = color_space; + write_task.depth = depth; + write_task.filter = filter; + write_task.dpi = page.dpi; + write_queue.push (write_task); + + update_progression (); } - private bool write_pixbuf_data (uint8[] buf) throws Error + private Error? write_multifile () { - jpeg_data.append (buf); - return true; + for (var i=0; i < n_pages; i++) + { + if (cancellable.is_cancelled ()) + { + finished_saving (); + return null; + } + + var write_task = write_queue.pop (); + if (write_task.error != null) + { + finished_saving (); + return write_task.error; + } + + var indexed_file = make_indexed_file (file.get_uri (), write_task.number, n_pages); + try + { + var stream = indexed_file.replace (null, false, FileCreateFlags.NONE); + stream.write_all (write_task.data, null); + } + catch (Error error) + { + finished_saving (); + return error; + } + } + + update_progression (); + finished_saving (); + return null; } - private void save_pdf (File file, int quality) throws Error + /* Those methods are run in the writer thread. It receive all + * write_tasks sent to it by the encoder threads and write those to + * disk. */ + + private Error? write_pdf () { /* Generate a random ID for this file */ var id = ""; for (var i = 0; i < 4; i++) id += "%08x".printf (Random.next_int ()); - var stream = file.replace (null, false, FileCreateFlags.NONE, null); + FileOutputStream? stream = null; + try + { + stream = file.replace (null, false, FileCreateFlags.NONE, null); + } + catch (Error error) + { + finished_saving (); + return error; + } var writer = new PDFWriter (stream); /* Choose object numbers */ @@ -304,163 +653,41 @@ public class Book writer.write_string (">>\n"); writer.write_string ("endobj\n"); - for (var i = 0; i < n_pages; i++) + /* Process each page in order */ + var tasks_in_standby = new Queue<WriteTaskPDF> (); + for (int i = 0; i < n_pages; i++) { - var page = get_page (i); - var image = page.get_image (true); - var width = image.width; - var height = image.height; - unowned uint8[] pixels = image.get_pixels (); - var page_width = width * 72.0 / page.dpi; - var page_height = height * 72.0 / page.dpi; - - int depth = 8; - string color_space = "DeviceRGB"; - string? filter = null; - char[] width_buffer = new char[double.DTOSTR_BUF_SIZE]; - char[] height_buffer = new char[double.DTOSTR_BUF_SIZE]; - uint8[] data; - if (page.is_color) + if (cancellable.is_cancelled ()) { - depth = 8; - color_space = "DeviceRGB"; - var data_length = height * width * 3; - data = new uint8[data_length]; - for (var row = 0; row < height; row++) - { - var in_offset = row * image.rowstride; - var out_offset = row * width * 3; - for (var x = 0; x < width; x++) - { - var in_o = in_offset + x*3; - var out_o = out_offset + x*3; - - data[out_o] = pixels[in_o]; - data[out_o+1] = pixels[in_o+1]; - data[out_o+2] = pixels[in_o+2]; - } - } - } - else if (page.depth == 2) - { - int shift_count = 6; - depth = 2; - color_space = "DeviceGray"; - var data_length = height * ((width * 2 + 7) / 8); - data = new uint8[data_length]; - var offset = 0; - for (var row = 0; row < height; row++) - { - /* Pad to the next line */ - if (shift_count != 6) - { - offset++; - shift_count = 6; - } - - var in_offset = row * image.rowstride; - for (var x = 0; x < width; x++) - { - /* Clear byte */ - if (shift_count == 6) - data[offset] = 0; - - /* Set bits */ - var p = pixels[in_offset + x*3]; - if (p >= 192) - data[offset] |= 3 << shift_count; - else if (p >= 128) - data[offset] |= 2 << shift_count; - else if (p >= 64) - data[offset] |= 1 << shift_count; - - /* Move to the next position */ - if (shift_count == 0) - { - offset++; - shift_count = 6; - } - else - shift_count -= 2; - } - } + finished_saving (); + return null; } - else if (page.depth == 1) - { - int mask = 0x80; - - depth = 1; - color_space = "DeviceGray"; - var data_length = height * ((width + 7) / 8); - data = new uint8[data_length]; - var offset = 0; - for (var row = 0; row < height; row++) - { - /* Pad to the next line */ - if (mask != 0x80) - { - offset++; - mask = 0x80; - } - var in_offset = row * image.rowstride; - for (var x = 0; x < width; x++) - { - /* Clear byte */ - if (mask == 0x80) - data[offset] = 0; - - /* Set bit */ - if (pixels[in_offset+x*3] != 0) - data[offset] |= (uint8) mask; - - /* Move to the next bit */ - mask >>= 1; - if (mask == 0) - { - offset++; - mask = 0x80; - } - } - } - } + var write_task = tasks_in_standby.peek_head (); + if (write_task != null && write_task.number == i) + tasks_in_standby.pop_head (); else { - depth = 8; - color_space = "DeviceGray"; - var data_length = height * width; - data = new uint8 [data_length]; - for (var row = 0; row < height; row++) + while (true) { - var in_offset = row * image.rowstride; - var out_offset = row * width; - for (var x = 0; x < width; x++) - data[out_offset+x] = pixels[in_offset+x*3]; - } - } - - /* Compress data */ - var compressed_data = compress_zlib (data); - if (compressed_data != null) - { - /* Try if JPEG compression is better */ - if (depth > 1) - { - var jpeg_data = compress_jpeg (image, quality, page.dpi); - if (jpeg_data.length < compressed_data.length) + write_task = (WriteTaskPDF) write_queue.pop (); + if (write_task.error != null) { - filter = "DCTDecode"; - data = jpeg_data; + finished_saving (); + return write_task.error; } - } + if (write_task.number == i) + break; - if (filter == null) - { - filter = "FlateDecode"; - data = compressed_data; + tasks_in_standby.insert_sorted (write_task, (a, b) => {return a.number - b.number;}); } } + var page_width = write_task.width * 72.0 / write_task.dpi; + var page_height = write_task.height * 72.0 / write_task.dpi; + var width_buffer = new char[double.DTOSTR_BUF_SIZE]; + var height_buffer = new char[double.DTOSTR_BUF_SIZE]; + /* Page */ writer.write_string ("\n"); writer.start_object (page_numbers[i]); @@ -481,16 +708,16 @@ public class Book writer.write_string ("<<\n"); writer.write_string ("/Type /XObject\n"); writer.write_string ("/Subtype /Image\n"); - writer.write_string ("/Width %d\n".printf (width)); - writer.write_string ("/Height %d\n".printf (height)); - writer.write_string ("/ColorSpace /%s\n".printf (color_space)); - writer.write_string ("/BitsPerComponent %d\n".printf (depth)); - writer.write_string ("/Length %d\n".printf (data.length)); - if (filter != null) - writer.write_string ("/Filter /%s\n".printf (filter)); + writer.write_string ("/Width %d\n".printf (write_task.width)); + writer.write_string ("/Height %d\n".printf (write_task.height)); + writer.write_string ("/ColorSpace /%s\n".printf (write_task.color_space)); + writer.write_string ("/BitsPerComponent %d\n".printf (write_task.depth)); + writer.write_string ("/Length %d\n".printf (write_task.data.length)); + if (write_task.filter != null) + writer.write_string ("/Filter /%s\n".printf (write_task.filter)); writer.write_string (">>\n"); writer.write_string ("stream\n"); - writer.write (data); + writer.write (write_task.data); writer.write_string ("\n"); writer.write_string ("endstream\n"); writer.write_string ("endobj\n"); @@ -500,7 +727,7 @@ public class Book writer.start_object (struct_tree_root_number); writer.write_string ("%u 0 obj\n".printf (struct_tree_root_number)); writer.write_string ("<<\n"); - writer.write_string ("/Type /StructTreeRoot\n"); + writer.write_string ("/Type /StructTreeRoot\n"); writer.write_string (">>\n"); writer.write_string ("endobj\n"); @@ -517,8 +744,6 @@ public class Book writer.write_string ("\n"); writer.write_string ("endstream\n"); writer.write_string ("endobj\n"); - - saving (i); } /* Info */ @@ -535,10 +760,10 @@ public class Book var xref_offset = writer.offset; writer.write_string ("xref\n"); writer.write_string ("0 %zu\n".printf (writer.object_offsets.length + 1)); - writer.write_string ("%010zu 65535 f \n".printf (next_empty_object (writer, 0))); + writer.write_string ("%010zu 65535 f \n".printf (writer.next_empty_object (0))); for (var i = 0; i < writer.object_offsets.length; i++) if (writer.object_offsets[i] == 0) - writer.write_string ("%010zu 65535 f \n".printf (next_empty_object (writer, i + 1))); + writer.write_string ("%010zu 65535 f \n".printf (writer.next_empty_object (i + 1))); else writer.write_string ("%010zu 00000 n \n".printf (writer.object_offsets[i])); @@ -554,31 +779,102 @@ public class Book writer.write_string ("startxref\n"); writer.write_string ("%zu\n".printf (xref_offset)); writer.write_string ("%%EOF\n"); + + update_progression (); + finished_saving (); + return null; } - static int next_empty_object (PDFWriter writer, int start) + /* update_progression is called once by page by encoder threads and + * once at the end by writer thread. */ + private void update_progression () { - for (var i = start; i < writer.object_offsets.length; i++) - if (writer.object_offsets[i] == 0) - return i + 1; - return 0; + double step = 1.0 / (double)(n_pages+1); + progression_mutex.lock (); + progression += step; + progression_mutex.unlock (); + Idle.add (() => + { + progression_callback (progression); + return false; + }); } - public void save (string type, int quality, File file) throws Error + /* finished_saving is called by the writer thread when it's done, + * meaning there is nothing left to do or saving has been + * cancelled */ + private void finished_saving () { - switch (type) + /* At this point, any remaining encode_task ought to remain unprocessed */ + ThreadPool.free ((owned) encoder, true, true); + + /* Wake-up save_async method in main thread */ + Idle.add ((owned)save_async_callback); + } + + /* Utility methods */ + + private static uint8[]? compress_zlib (uint8[] data, uint max_size) + { + var stream = ZLib.DeflateStream (ZLib.Level.BEST_COMPRESSION); + var out_data = new uint8[max_size]; + + stream.next_in = data; + stream.next_out = out_data; + while (true) { - case "jpeg": - case "png": - save_multi_file (type, quality, file); - break; - case "pdf": - save_pdf (file, quality); - break; + /* Compression complete */ + if (stream.avail_in == 0) + break; + + /* Out of space */ + if (stream.avail_out == 0) + return null; + + if (stream.deflate (ZLib.Flush.FINISH) == ZLib.Status.STREAM_ERROR) + return null; } + + var n_written = out_data.length - stream.avail_out; + out_data.resize ((int) n_written); + + return out_data; + } + + private static uint8[] compress_jpeg (Gdk.Pixbuf image, int quality, int dpi) throws Error + { + uint8[] jpeg_data; + string[] keys = { "quality", "x-dpi", "y-dpi", null }; + string[] values = { "%d".printf (quality), "%d".printf (dpi), "%d".printf (dpi), null }; + + image.save_to_bufferv (out jpeg_data, "jpeg", keys, values); + return jpeg_data; } } +private class EncodeTask +{ + public int number; + public Page page; +} + +private class WriteTask +{ + public int number; + public uint8[] data; + public Error error; +} + +private class WriteTaskPDF : WriteTask +{ + public int width; + public int height; + public string color_space; + public int depth; + public string? filter; + public int dpi; +} + private class PDFWriter { public size_t offset = 0; @@ -621,31 +917,32 @@ private class PDFWriter { object_offsets[index - 1] = (uint)offset; } -} - -public class PsWriter -{ - public Cairo.PsSurface surface; - public FileOutputStream stream; - public PsWriter (FileOutputStream stream) + public int next_empty_object (int start) { - this.stream = stream; - surface = new Cairo.PsSurface.for_stream (write_cairo_data, 0, 0); + for (var i = start; i < object_offsets.length; i++) + if (object_offsets[i] == 0) + return i + 1; + return 0; } +} - private Cairo.Status write_cairo_data (uint8[] data) +public File make_indexed_file (string uri, uint i, uint n_pages) +{ + if (n_pages == 1) + return File.new_for_uri (uri); + + /* Insert index before extension */ + var basename = Path.get_basename (uri); + string prefix = uri, suffix = ""; + var extension_index = basename.last_index_of_char ('.'); + if (extension_index >= 0) { - try - { - stream.write_all (data, null, null); - } - catch (Error e) - { - warning ("Error writing data: %s", e.message); - return Cairo.Status.WRITE_ERROR; - } - - return Cairo.Status.SUCCESS; + suffix = basename.slice (extension_index, basename.length); + prefix = uri.slice (0, uri.length - suffix.length); } + var width = n_pages.to_string().length; + var number_format = "%%0%dd".printf (width); + var filename = prefix + "-" + number_format.printf (i + 1) + suffix; + return File.new_for_uri (filename); } diff --git a/src/help-overlay.ui b/src/help-overlay.ui new file mode 100644 index 0000000..dabec9f --- /dev/null +++ b/src/help-overlay.ui @@ -0,0 +1,128 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <!-- interface-requires gtk+ 3.17 --> + <object class="GtkShortcutsWindow" id="help_overlay"> + <property name="modal">1</property> + <child> + <object class="GtkShortcutsSection"> + <property name="visible">1</property> + <child> + <object class="GtkShortcutsGroup"> + <property name="visible">1</property> + <property name="title" translatable="yes" context="shortcut window">Scanning</property> + <child> + <object class="GtkShortcutsShortcut"> + <property name="visible">1</property> + <property name="accelerator"><ctrl>1</property> + <property name="title" translatable="yes" context="shortcut window">Scan a single page</property> + </object> + </child> + <child> + <object class="GtkShortcutsShortcut"> + <property name="visible">1</property> + <property name="accelerator"><ctrl>f</property> + <property name="title" translatable="yes" context="shortcut window">Scan all pages from document feeder</property> + </object> + </child> + <child> + <object class="GtkShortcutsShortcut"> + <property name="visible">1</property> + <property name="accelerator"><ctrl>m</property> + <property name="title" translatable="yes" context="shortcut window">Scan continuously from a flatbed scanner</property> + </object> + </child> + <child> + <object class="GtkShortcutsShortcut"> + <property name="visible">1</property> + <property name="accelerator">Escape</property> + <property name="title" translatable="yes" context="shortcut window">Stop scan in progress</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkShortcutsGroup"> + <property name="visible">1</property> + <property name="title" translatable="yes" context="shortcut window">Document Modification</property> + <child> + <object class="GtkShortcutsShortcut"> + <property name="visible">1</property> + <property name="accelerator">less</property> + <property name="title" translatable="yes" context="shortcut window">Move page left</property> + </object> + </child> + <child> + <object class="GtkShortcutsShortcut"> + <property name="visible">1</property> + <property name="accelerator">greater</property> + <property name="title" translatable="yes" context="shortcut window">Move page right</property> + </object> + </child> + <child> + <object class="GtkShortcutsShortcut"> + <property name="visible">1</property> + <property name="accelerator">bracketleft</property> + <property name="title" translatable="yes" context="shortcut window">Rotate page to the left (anti-clockwise)</property> + </object> + </child> + <child> + <object class="GtkShortcutsShortcut"> + <property name="visible">1</property> + <property name="accelerator">bracketright</property> + <property name="title" translatable="yes" context="shortcut window">Rotate page to the right (clockwise)</property> + </object> + </child> + <child> + <object class="GtkShortcutsShortcut"> + <property name="visible">1</property> + <property name="accelerator">Delete</property> + <property name="title" translatable="yes" context="shortcut window">Delete page</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkShortcutsGroup"> + <property name="visible">1</property> + <property name="title" translatable="yes" context="shortcut window">Document Management</property> + <child> + <object class="GtkShortcutsShortcut"> + <property name="visible">1</property> + <property name="accelerator"><ctrl>n</property> + <property name="title" translatable="yes" context="shortcut window">Start new document</property> + </object> + </child> + <child> + <object class="GtkShortcutsShortcut"> + <property name="visible">1</property> + <property name="accelerator"><ctrl>s</property> + <property name="title" translatable="yes" context="shortcut window">Save scanned document</property> + </object> + </child> + <child> + <object class="GtkShortcutsShortcut"> + <property name="visible">1</property> + <property name="accelerator"><ctrl>e</property> + <property name="title" translatable="yes" context="shortcut window">Email scanned document</property> + </object> + </child> + <child> + <object class="GtkShortcutsShortcut"> + <property name="visible">1</property> + <property name="accelerator"><ctrl>p</property> + <property name="title" translatable="yes" context="shortcut window">Print scanned document</property> + </object> + </child> + <child> + <object class="GtkShortcutsShortcut"> + <property name="visible">1</property> + <property name="accelerator"><ctrl>c</property> + <property name="title" translatable="yes" context="shortcut window">Copy current page to clipboard</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> +</interface> diff --git a/src/libwebp.vapi b/src/libwebp.vapi new file mode 100644 index 0000000..74bfdd5 --- /dev/null +++ b/src/libwebp.vapi @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2017 Stéphane Fillion + * Authors: Stéphane Fillion <stphanef3724@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. + */ + +namespace WebP +{ + // Returns the size of the compressed data (pointed to by *output), or 0 if + // an error occurred. The compressed data must be released by the caller + // using the call 'free(*output)'. + // These functions compress using the lossy format, and the quality_factor + // can go from 0 (smaller output, lower quality) to 100 (best quality, + // larger output). + [CCode (cheader_filename = "webp/encode.h", cname = "WebPEncodeRGB")] + private size_t _encode_rgb ([CCode (array_length = false)] uint8[] rgb, + int width, + int height, + int stride, + float quality_factor, + [CCode (array_length = false)] out uint8[] output); + [CCode (cname = "vala_encode_rgb")] + public uint8[] encode_rgb (uint8[] rgb, int width, int height, int stride, float quality_factor) + { + uint8[] output; + size_t length; + length = _encode_rgb (rgb, width, height, stride, quality_factor, out output); + output.length = (int) length; + return output; + } + + // These functions are the equivalent of the above, but compressing in a + // lossless manner. Files are usually larger than lossy format, but will + // not suffer any compression loss. + [CCode (cheader_filename = "webp/encode.h", cname = "WebPEncodeLosslessRGB")] + private size_t _encode_lossless_rgb ([CCode (array_length = false)] uint8[] rgb, + int width, + int height, + int stride, + [CCode (array_length = false)] out uint8[] output); + [CCode (cname = "vala_encode_lossless_rgb")] + public uint8[] encode_lossless_rgb (uint8[] rgb, int width, int height, int stride) + { + uint8[] output; + size_t length; + length = _encode_lossless_rgb (rgb, width, height, stride, out output); + output.length = (int) length; + return output; + } +} diff --git a/src/libwebpmux.vapi b/src/libwebpmux.vapi new file mode 100644 index 0000000..f2461a2 --- /dev/null +++ b/src/libwebpmux.vapi @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2017 Stéphane Fillion + * Authors: Stéphane Fillion <stphanef3724@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. + */ + +namespace WebP +{ + // Error codes + [CCode (cheader_filename = "webp/mux.h", cname = "WebPMuxError", cprefix = "WEBP_MUX_", has_type_id = false)] + public enum MuxError + { + OK = 1, + NOT_FOUND = 0, + INVALID_ARGUMENT = -1, + BAD_DATA = -2, + MEMORY_ERROR = -3, + NOT_ENOUGH_DATA = -4 + } + + // Data type used to describe 'raw' data, e.g., chunk data + // (ICC profile, metadata) and WebP compressed image data. + [CCode (cheader_filename = "webp/mux.h", cname = "WebPData", destroy_function = "", has_type_id = false)] + private struct Data + { + [CCode (array_length = false)] unowned uint8[] bytes; + size_t size; + } + + // main opaque object. + [CCode (cheader_filename = "webp/mux.h", cname = "WebPMux", free_function = "WebPMuxDelete")] + [Compact] + public class Mux + { + // Creates an empty mux object. + // Returns: + // A pointer to the newly created empty mux object. + // Or NULL in case of memory error. + [CCode (cname = "WebPMuxNew")] + public static Mux? new_mux (); + + // Sets the (non-animated and non-fragmented) image in the mux object. + // Note: Any existing images (including frames/fragments) will be removed. + // Parameters: + // mux - (in/out) object in which the image is to be set + // bitstream - (in) can be a raw VP8/VP8L bitstream or a single-image + // WebP file (non-animated and non-fragmented) + // copy_data - (in) value 1 indicates given data WILL be copied to the mux + // object and value 0 indicates data will NOT be copied. + // Returns: + // WEBP_MUX_INVALID_ARGUMENT - if mux is NULL or bitstream is NULL. + // WEBP_MUX_MEMORY_ERROR - on memory allocation error. + // WEBP_MUX_OK - on success. + [CCode (cname = "WebPMuxSetImage")] + private MuxError _set_image (Data bitstream, bool copy_data); + [CCode (cname = "vala_set_image")] + public MuxError set_image (uint8[] bitstream, bool copy_data) + { + Data data; + data.bytes = bitstream; + data.size = bitstream.length; + return _set_image (data, copy_data); + } + + // Adds a chunk with id 'fourcc' and data 'chunk_data' in the mux object. + // Any existing chunk(s) with the same id will be removed. + // Parameters: + // mux - (in/out) object to which the chunk is to be added + // fourcc - (in) a character array containing the fourcc of the given chunk; + // e.g., "ICCP", "XMP ", "EXIF" etc. + // chunk_data - (in) the chunk data to be added + // copy_data - (in) value 1 indicates given data WILL be copied to the mux + // object and value 0 indicates data will NOT be copied. + // Returns: + // WEBP_MUX_INVALID_ARGUMENT - if mux, fourcc or chunk_data is NULL + // or if fourcc corresponds to an image chunk. + // WEBP_MUX_MEMORY_ERROR - on memory allocation error. + // WEBP_MUX_OK - on success. + [CCode (cname = "WebPMuxSetChunk")] + private MuxError _set_chunk ([CCode (array_length = false)] uchar[] fourcc, + Data chunk_data, + bool copy_data); + [CCode (cname = "vala_set_chunk")] + public MuxError set_chunk (string fourcc, uint8[] chunk_data, bool copy_data) + requires (fourcc.length == 4) + { + Data data; + data.bytes = chunk_data; + data.size = chunk_data.length; + return _set_chunk ((uchar[]) fourcc, data, copy_data); + } + + // Assembles all chunks in WebP RIFF format and returns in 'assembled_data'. + // This function also validates the mux object. + // Note: The content of 'assembled_data' will be ignored and overwritten. + // Also, the content of 'assembled_data' is allocated using malloc(), and NOT + // owned by the 'mux' object. It MUST be deallocated by the caller by calling + // WebPDataClear(). It's always safe to call WebPDataClear() upon return, + // even in case of error. + // Parameters: + // mux - (in/out) object whose chunks are to be assembled + // assembled_data - (out) assembled WebP data + // Returns: + // WEBP_MUX_BAD_DATA - if mux object is invalid. + // WEBP_MUX_INVALID_ARGUMENT - if mux or assembled_data is NULL. + // WEBP_MUX_MEMORY_ERROR - on memory allocation error. + // WEBP_MUX_OK - on success. + [CCode (cname = "WebPMuxAssemble")] + private MuxError _assemble (out Data assembled_data); + [CCode (cname = "vala_assemble")] + public MuxError assemble (out uint8[] assembled_data) + { + Data data; + MuxError mux_error; + unowned uint8[] out_array; + mux_error = _assemble (out data); + out_array = data.bytes; + out_array.length = (int) data.size; + assembled_data = out_array; + return mux_error; + } + } +} diff --git a/src/meson.build b/src/meson.build index cfda86d..9e40e42 100644 --- a/src/meson.build +++ b/src/meson.build @@ -12,16 +12,23 @@ if packagekit_dep.found () vala_args += [ '-D', 'HAVE_PACKAGEKIT' ] dependencies += packagekit_dep endif +if webp_dep.found () and (not colord_dep.found () or webpmux_dep.found ()) # Webpmux only required if colord + vala_args += [ '-D', 'HAVE_WEBP' ] + dependencies += [ webp_dep, webpmux_dep ] +endif simple_scan = executable ('simple-scan', [ 'config.vapi', + 'app-window.vala', + 'authorize-dialog.vala', 'book.vala', 'book-view.vala', 'page.vala', 'page-view.vala', + 'preferences-dialog.vala', 'simple-scan.vala', 'scanner.vala', - 'ui.vala', + 'screensaver.vala', 'autosave-manager.vala' ] + resources, dependencies: dependencies, vala_args: vala_args, diff --git a/src/page.vala b/src/page.vala index 8936187..582aef8 100644 --- a/src/page.vala +++ b/src/page.vala @@ -624,13 +624,16 @@ public class Page return image; } - private string? get_icc_data_encoded (string icc_profile_filename) + public string? get_icc_data_encoded () { + if (color_profile == null) + return null; + /* Get binary data */ string contents; try { - FileUtils.get_contents (icc_profile_filename, out contents); + FileUtils.get_contents (color_profile, out contents); } catch (Error e) { @@ -641,63 +644,33 @@ public class Page /* Encode into base64 */ return Base64.encode ((uchar[]) contents.to_utf8 ()); } - + public void copy_to_clipboard (Gtk.Window window) - { + { var display = window.get_display (); var clipboard = Gtk.Clipboard.get_for_display (display, Gdk.SELECTION_CLIPBOARD); var image = get_image (true); clipboard.set_image (image); } - public void save (string type, int quality, File file) throws Error + public void save_png (File file) throws Error { var stream = file.replace (null, false, FileCreateFlags.NONE, null); - var writer = new PixbufWriter (stream); var image = get_image (true); string? icc_profile_data = null; if (color_profile != null) - icc_profile_data = get_icc_data_encoded (color_profile); + icc_profile_data = get_icc_data_encoded (); - if (strcmp (type, "jpeg") == 0) - { - string[] keys = { "x-dpi", "y-dpi", "quality", "icc-profile", null }; - string[] values = { "%d".printf (dpi), "%d".printf (dpi), "%d".printf (quality), icc_profile_data, null }; - if (icc_profile_data == null) - keys[3] = null; - writer.save (image, "jpeg", keys, values); - } - else if (strcmp (type, "png") == 0) - { - string[] keys = { "x-dpi", "y-dpi", "icc-profile", null }; - string[] values = { "%d".printf (dpi), "%d".printf (dpi), icc_profile_data, null }; - if (icc_profile_data == null) - keys[2] = null; - writer.save (image, "png", keys, values); - } - else - throw new FileError.INVAL ("Unknown file type: %s".printf (type)); - } -} + string[] keys = { "x-dpi", "y-dpi", "icc-profile", null }; + string[] values = { "%d".printf (dpi), "%d".printf (dpi), icc_profile_data, null }; + if (icc_profile_data == null) + keys[2] = null; -public class PixbufWriter -{ - public FileOutputStream stream; - - public PixbufWriter (FileOutputStream stream) - { - this.stream = stream; - } - - public void save (Gdk.Pixbuf image, string type, string[] option_keys, string[] option_values) throws Error - { - image.save_to_callbackv (write_pixbuf_data, type, option_keys, option_values); - } - - private bool write_pixbuf_data (uint8[] buf) throws Error - { - stream.write_all (buf, null, null); - return true; + image.save_to_callbackv ((buf) => + { + stream.write_all (buf, null, null); + return true; + }, "png", keys, values); } } diff --git a/src/preferences-dialog.ui b/src/preferences-dialog.ui new file mode 100644 index 0000000..2272b77 --- /dev/null +++ b/src/preferences-dialog.ui @@ -0,0 +1,608 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.20.0 --> +<interface> + <requires lib="gtk+" version="3.10"/> + <object class="GtkAdjustment" id="brightness_adjustment"> + <property name="lower">-100</property> + <property name="upper">100</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkAdjustment" id="contrast_adjustment"> + <property name="lower">-100</property> + <property name="upper">100</property> + <property name="step_increment">1</property> + <property name="page_increment">10</property> + </object> + <object class="GtkListStore" id="device_model"> + <columns> + <!-- column-name device_name --> + <column type="gchararray"/> + <!-- column-name label --> + <column type="gchararray"/> + </columns> + </object> + <object class="GtkListStore" id="paper_size_model"> + <columns> + <!-- column-name width --> + <column type="gint"/> + <!-- column-name height --> + <column type="gint"/> + <!-- column-name label --> + <column type="gchararray"/> + </columns> + </object> + <object class="GtkListStore" id="photo_dpi_model"> + <columns> + <!-- column-name dpi --> + <column type="gint"/> + <!-- column-name label --> + <column type="gchararray"/> + </columns> + </object> + <object class="GtkListStore" id="text_dpi_model"> + <columns> + <!-- column-name dpi --> + <column type="gint"/> + <!-- column-name label --> + <column type="gchararray"/> + </columns> + </object> + <template class="PreferencesDialog" parent="GtkDialog"> + <property name="can_focus">False</property> + <property name="title" translatable="yes" comments="Title of preferences dialog">Preferences</property> + <property name="resizable">False</property> + <property name="icon_name">scanner</property> + <property name="type_hint">normal</property> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">2</property> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="dialog-action_area2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">30</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="preferences_close_button"> + <property name="label">gtk-close</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkNotebook"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="show_border">False</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">30</property> + <property name="orientation">vertical</property> + <property name="spacing">30</property> + <child> + <object class="GtkGrid"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">15</property> + <property name="column_spacing">10</property> + <child> + <object class="GtkLabel" id="source_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes" comments="Label beside scan source combo box">_Scanner</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">device_combo</property> + <property name="xalign">1</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="device_combo"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="hexpand">True</property> + <property name="model">device_model</property> + <signal name="changed" handler="device_combo_changed_cb" swapped="no"/> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="page_side_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes" comments="Label beside scan side combo box">Scan Sides</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">scan_side_box</property> + <property name="xalign">1</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="paper_size_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes" comments="Label beside page size combo box">Page Size</property> + <property name="use_underline">True</property> + <property name="xalign">1</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="paper_size_combo"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="hexpand">True</property> + <property name="model">paper_size_model</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkBox" id="scan_side_box"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="homogeneous">True</property> + <child> + <object class="GtkRadioButton" id="front_side_button"> + <property name="label" translatable="yes" comments="Preferences Dialog: Toggle button to select scanning on front side of a page">Front</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="active">True</property> + <property name="draw_indicator">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="back_side_button"> + <property name="label" translatable="yes" comments="Preferences Dialog: Toggle button to select scanning on the back side of a page">Back</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="draw_indicator">False</property> + <property name="group">front_side_button</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="both_side_button"> + <property name="label" translatable="yes" comments="Preferences Dialog: Toggle button to select scanning on both sides of a page">Both</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="draw_indicator">False</property> + <property name="group">front_side_button</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <style> + <class name="linked"/> + </style> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkGrid"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">15</property> + <property name="column_spacing">10</property> + <child> + <object class="GtkLabel" id="page_delay_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes" comments="Label beside page delay scale">Delay</property> + <property name="use_underline">True</property> + <property name="xalign">1</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes" comments="Preferences dialog: Label above settings for scanning multiple pages from a flatbed">Multiple pages from flatbed</property> + <property name="xalign">0</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + <property name="width">3</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="homogeneous">True</property> + <child> + <object class="GtkRadioButton" id="page_delay_3s_button"> + <property name="label" translatable="yes" comments="Preferences Dialog: Toggle button to select scanning on front side of a page">3</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="active">True</property> + <property name="draw_indicator">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="page_delay_5s_button"> + <property name="label" translatable="yes" comments="Preferences Dialog: Toggle button to select scanning on front side of a page">5</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="active">True</property> + <property name="draw_indicator">False</property> + <property name="group">page_delay_3s_button</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="page_delay_7s_button"> + <property name="label" translatable="yes" comments="Preferences Dialog: Toggle button to select scanning on front side of a page">7</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="active">True</property> + <property name="draw_indicator">False</property> + <property name="group">page_delay_3s_button</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="page_delay_10s_button"> + <property name="label" translatable="yes" comments="Preferences Dialog: Toggle button to select scanning on front side of a page">10</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="active">True</property> + <property name="draw_indicator">False</property> + <property name="group">page_delay_3s_button</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="page_delay_15s_button"> + <property name="label" translatable="yes" comments="Preferences Dialog: Toggle button to select scanning on front side of a page">15</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="active">True</property> + <property name="draw_indicator">False</property> + <property name="group">page_delay_3s_button</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">4</property> + </packing> + </child> + <style> + <class name="linked"/> + </style> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes" comments="Label after page delay radio buttons">Seconds</property> + <property name="use_underline">True</property> + <property name="xalign">1</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="tab_expand">True</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes" comments="Preferences Dialog: Tab label for scanning settings">Scanning</property> + </object> + <packing> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">30</property> + <property name="orientation">vertical</property> + <property name="spacing">30</property> + <child> + <object class="GtkGrid"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">14</property> + <property name="column_spacing">10</property> + <child> + <object class="GtkLabel" id="text_dpi_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes" comments="Label beside scan source combo box">_Text Resolution</property> + <property name="use_underline">True</property> + <property name="xalign">1</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="photo_dpi_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes" comments="Label beside scan source combo box">_Photo Resolution</property> + <property name="use_underline">True</property> + <property name="xalign">1</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="text_dpi_combo"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="hexpand">True</property> + <property name="model">text_dpi_model</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="photo_dpi_combo"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="hexpand">True</property> + <property name="model">photo_dpi_model</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkGrid"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">15</property> + <property name="column_spacing">10</property> + <child> + <object class="GtkLabel" id="brightness_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes" comments="Label beside brightness scale">Brightness</property> + <property name="use_underline">True</property> + <property name="xalign">1</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="contrast_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes" comments="Label beside contrast scale">Contrast</property> + <property name="use_underline">True</property> + <property name="xalign">1</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkScale" id="brightness_scale"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + <property name="adjustment">brightness_adjustment</property> + <property name="draw_value">False</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkScale" id="contrast_scale"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + <property name="adjustment">contrast_adjustment</property> + <property name="draw_value">False</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">1</property> + <property name="tab_expand">True</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes" comments="Preferences Dialog: Tab for quality settings">Quality</property> + </object> + <packing> + <property name="position">1</property> + <property name="tab_fill">False</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="1">preferences_close_button</action-widget> + </action-widgets> + </template> + <object class="GtkSizeGroup" id="label_size_group"> + <widgets> + <widget name="source_label"/> + <widget name="page_side_label"/> + <widget name="paper_size_label"/> + <widget name="page_delay_label"/> + <widget name="text_dpi_label"/> + <widget name="photo_dpi_label"/> + <widget name="brightness_label"/> + <widget name="contrast_label"/> + </widgets> + </object> +</interface> diff --git a/src/preferences-dialog.vala b/src/preferences-dialog.vala new file mode 100644 index 0000000..bf213fb --- /dev/null +++ b/src/preferences-dialog.vala @@ -0,0 +1,534 @@ +/* + * Copyright (C) 2009-2017 Canonical Ltd. + * Author: Robert Ancell <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. + */ + +[GtkTemplate (ui = "/org/gnome/SimpleScan/preferences-dialog.ui")] +private class PreferencesDialog : Gtk.Dialog +{ + private Settings settings; + + private bool setting_devices; + private bool user_selected_device; + + [GtkChild] + private Gtk.ComboBox device_combo; + [GtkChild] + private Gtk.ComboBox text_dpi_combo; + [GtkChild] + private Gtk.ComboBox photo_dpi_combo; + [GtkChild] + private Gtk.ComboBox paper_size_combo; + [GtkChild] + private Gtk.Scale brightness_scale; + [GtkChild] + private Gtk.Scale contrast_scale; + [GtkChild] + private Gtk.ListStore device_model; + [GtkChild] + private Gtk.RadioButton page_delay_3s_button; + [GtkChild] + private Gtk.RadioButton page_delay_5s_button; + [GtkChild] + private Gtk.RadioButton page_delay_7s_button; + [GtkChild] + private Gtk.RadioButton page_delay_10s_button; + [GtkChild] + private Gtk.RadioButton page_delay_15s_button; + [GtkChild] + private Gtk.ListStore text_dpi_model; + [GtkChild] + private Gtk.ListStore photo_dpi_model; + [GtkChild] + private Gtk.RadioButton front_side_button; + [GtkChild] + private Gtk.RadioButton back_side_button; + [GtkChild] + private Gtk.RadioButton both_side_button; + [GtkChild] + private Gtk.ListStore paper_size_model; + [GtkChild] + private Gtk.Adjustment brightness_adjustment; + [GtkChild] + private Gtk.Adjustment contrast_adjustment; + [GtkChild] + private Gtk.Button preferences_close_button; + + public PreferencesDialog (Settings settings, bool use_header_bar) + { + Object (use_header_bar: use_header_bar ? 1 : -1); + + if (use_header_bar) + preferences_close_button.visible = false; + + this.settings = settings; + + Gtk.TreeIter iter; + paper_size_model.append (out iter); + paper_size_model.set (iter, 0, 0, 1, 0, 2, + /* Combo box value for automatic paper size */ + _("Automatic"), -1); + paper_size_model.append (out iter); + paper_size_model.set (iter, 0, 1050, 1, 1480, 2, "A6", -1); + paper_size_model.append (out iter); + paper_size_model.set (iter, 0, 1480, 1, 2100, 2, "A5", -1); + paper_size_model.append (out iter); + paper_size_model.set (iter, 0, 2100, 1, 2970, 2, "A4", -1); + paper_size_model.append (out iter); + paper_size_model.set (iter, 0, 2159, 1, 2794, 2, "Letter", -1); + paper_size_model.append (out iter); + paper_size_model.set (iter, 0, 2159, 1, 3556, 2, "Legal", -1); + paper_size_model.append (out iter); + paper_size_model.set (iter, 0, 1016, 1, 1524, 2, "4×6", -1); + + var renderer = new Gtk.CellRendererText (); + device_combo.pack_start (renderer, true); + device_combo.add_attribute (renderer, "text", 1); + + var dpi = settings.get_int ("text-dpi"); + if (dpi <= 0) + dpi = DEFAULT_TEXT_DPI; + set_dpi_combo (text_dpi_combo, DEFAULT_TEXT_DPI, dpi); + text_dpi_combo.changed.connect (() => { settings.set_int ("text-dpi", get_text_dpi ()); }); + dpi = settings.get_int ("photo-dpi"); + if (dpi <= 0) + dpi = DEFAULT_PHOTO_DPI; + set_dpi_combo (photo_dpi_combo, DEFAULT_PHOTO_DPI, dpi); + photo_dpi_combo.changed.connect (() => { settings.set_int ("photo-dpi", get_photo_dpi ()); }); + + set_page_side ((ScanType) settings.get_enum ("page-side")); + front_side_button.toggled.connect ((button) => { if (button.active) settings.set_enum ("page-side", ScanType.ADF_FRONT); }); + back_side_button.toggled.connect ((button) => { if (button.active) settings.set_enum ("page-side", ScanType.ADF_BACK); }); + both_side_button.toggled.connect ((button) => { if (button.active) settings.set_enum ("page-side", ScanType.ADF_BOTH); }); + + renderer = new Gtk.CellRendererText (); + paper_size_combo.pack_start (renderer, true); + paper_size_combo.add_attribute (renderer, "text", 2); + + var lower = brightness_adjustment.lower; + var darker_label = "<small>%s</small>".printf (_("Darker")); + var upper = brightness_adjustment.upper; + var lighter_label = "<small>%s</small>".printf (_("Lighter")); + brightness_scale.add_mark (lower, Gtk.PositionType.BOTTOM, darker_label); + brightness_scale.add_mark (0, Gtk.PositionType.BOTTOM, null); + brightness_scale.add_mark (upper, Gtk.PositionType.BOTTOM, lighter_label); + brightness_adjustment.value = settings.get_int ("brightness"); + brightness_adjustment.value_changed.connect (() => { settings.set_int ("brightness", get_brightness ()); }); + + lower = contrast_adjustment.lower; + var less_label = "<small>%s</small>".printf (_("Less")); + upper = contrast_adjustment.upper; + var more_label = "<small>%s</small>".printf (_("More")); + contrast_scale.add_mark (lower, Gtk.PositionType.BOTTOM, less_label); + contrast_scale.add_mark (0, Gtk.PositionType.BOTTOM, null); + contrast_scale.add_mark (upper, Gtk.PositionType.BOTTOM, more_label); + contrast_adjustment.value = settings.get_int ("contrast"); + contrast_adjustment.value_changed.connect (() => { settings.set_int ("contrast", get_contrast ()); }); + + var paper_width = settings.get_int ("paper-width"); + var paper_height = settings.get_int ("paper-height"); + set_paper_size (paper_width, paper_height); + paper_size_combo.changed.connect (() => + { + int w, h; + get_paper_size (out w, out h); + settings.set_int ("paper-width", w); + settings.set_int ("paper-height", h); + }); + + set_page_delay (settings.get_int ("page-delay")); + page_delay_3s_button.toggled.connect ((button) => { if (button.active) settings.set_int ("page-delay", 3); }); + page_delay_5s_button.toggled.connect ((button) => { if (button.active) settings.set_int ("page-delay", 5); }); + page_delay_7s_button.toggled.connect ((button) => { if (button.active) settings.set_int ("page-delay", 7); }); + page_delay_10s_button.toggled.connect ((button) => { if (button.active) settings.set_int ("page-delay", 10); }); + page_delay_15s_button.toggled.connect ((button) => { if (button.active) settings.set_int ("page-delay", 15); }); + } + + public void set_scan_devices (List<ScanDevice> devices) + { + setting_devices = true; + + /* If the user hasn't chosen a scanner choose the best available one */ + var have_selection = false; + if (user_selected_device) + have_selection = device_combo.active >= 0; + + /* Add new devices */ + int index = 0; + Gtk.TreeIter iter; + foreach (var device in devices) + { + int n_delete = -1; + + /* Find if already exists */ + if (device_model.iter_nth_child (out iter, null, index)) + { + int i = 0; + do + { + string name; + bool matched; + + device_model.get (iter, 0, out name, -1); + matched = name == device.name; + + if (matched) + { + n_delete = i; + break; + } + i++; + } while (device_model.iter_next (ref iter)); + } + + /* If exists, remove elements up to this one */ + if (n_delete >= 0) + { + int i; + + /* Update label */ + device_model.set (iter, 1, device.label, -1); + + for (i = 0; i < n_delete; i++) + { + device_model.iter_nth_child (out iter, null, index); +#if VALA_0_36 + device_model.remove (ref iter); +#else + device_model.remove (iter); +#endif + } + } + else + { + device_model.insert (out iter, index); + device_model.set (iter, 0, device.name, 1, device.label, -1); + } + index++; + } + + /* Remove any remaining devices */ + while (device_model.iter_nth_child (out iter, null, index)) +#if VALA_0_36 + device_model.remove (ref iter); +#else + device_model.remove (iter); +#endif + + /* Select the previously selected device or the first available device */ + if (!have_selection) + { + var device = settings.get_string ("selected-device"); + if (device != null && find_scan_device (device, out iter)) + device_combo.set_active_iter (iter); + else + device_combo.set_active (0); + } + + setting_devices = false; + } + + public string? get_selected_device () + { + Gtk.TreeIter iter; + + if (device_combo.get_active_iter (out iter)) + { + string device; + device_model.get (iter, 0, out device, -1); + return device; + } + + return null; + } + + public string? get_selected_device_label () + { + Gtk.TreeIter iter; + + if (device_combo.get_active_iter (out iter)) + { + string label; + device_model.get (iter, 1, out label, -1); + return label; + } + + return null; + } + + public void set_selected_device (string device) + { + user_selected_device = true; + + Gtk.TreeIter iter; + if (!find_scan_device (device, out iter)) + return; + + device_combo.set_active_iter (iter); + } + + private bool find_scan_device (string device, out Gtk.TreeIter iter) + { + bool have_iter = false; + + if (device_model.get_iter_first (out iter)) + { + do + { + string d; + device_model.get (iter, 0, out d, -1); + if (d == device) + have_iter = true; + } while (!have_iter && device_model.iter_next (ref iter)); + } + + return have_iter; + } + + private void set_page_side (ScanType page_side) + { + switch (page_side) + { + case ScanType.ADF_FRONT: + front_side_button.active = true; + break; + case ScanType.ADF_BACK: + back_side_button.active = true; + break; + default: + case ScanType.ADF_BOTH: + both_side_button.active = true; + break; + } + } + + public ScanType get_page_side () + { + if (front_side_button.active) + return ScanType.ADF_FRONT; + else if (back_side_button.active) + return ScanType.ADF_BACK; + else + return ScanType.ADF_BOTH; + } + + public void set_paper_size (int width, int height) + { + Gtk.TreeIter iter; + bool have_iter; + + for (have_iter = paper_size_model.get_iter_first (out iter); + have_iter; + have_iter = paper_size_model.iter_next (ref iter)) + { + int w, h; + paper_size_model.get (iter, 0, out w, 1, out h, -1); + if (w == width && h == height) + break; + } + + if (!have_iter) + have_iter = paper_size_model.get_iter_first (out iter); + if (have_iter) + paper_size_combo.set_active_iter (iter); + } + + public int get_text_dpi () + { + Gtk.TreeIter iter; + int dpi = DEFAULT_TEXT_DPI; + + if (text_dpi_combo.get_active_iter (out iter)) + text_dpi_model.get (iter, 0, out dpi, -1); + + return dpi; + } + + public int get_photo_dpi () + { + Gtk.TreeIter iter; + int dpi = DEFAULT_PHOTO_DPI; + + if (photo_dpi_combo.get_active_iter (out iter)) + photo_dpi_model.get (iter, 0, out dpi, -1); + + return dpi; + } + + public bool get_paper_size (out int width, out int height) + { + Gtk.TreeIter iter; + + width = height = 0; + if (paper_size_combo.get_active_iter (out iter)) + { + paper_size_model.get (iter, 0, ref width, 1, ref height, -1); + return true; + } + + return false; + } + + public int get_brightness () + { + return (int) brightness_adjustment.value; + } + + public void set_brightness (int brightness) + { + brightness_adjustment.value = brightness; + } + + public int get_contrast () + { + return (int) contrast_adjustment.value; + } + + public void set_contrast (int contrast) + { + contrast_adjustment.value = contrast; + } + + public int get_page_delay () + { + if (page_delay_15s_button.active) + return 15; + else if (page_delay_10s_button.active) + return 10; + else if (page_delay_7s_button.active) + return 7; + else if (page_delay_5s_button.active) + return 5; + else + return 3; + } + + public void set_page_delay (int page_delay) + { + if (page_delay >= 15) + page_delay_15s_button.active = true; + else if (page_delay >= 10) + page_delay_10s_button.active = true; + else if (page_delay >= 7) + page_delay_7s_button.active = true; + else if (page_delay >= 5) + page_delay_5s_button.active = true; + else + page_delay_3s_button.active = true; + } + + private void set_dpi_combo (Gtk.ComboBox combo, int default_dpi, int current_dpi) + { + var renderer = new Gtk.CellRendererText (); + combo.pack_start (renderer, true); + combo.add_attribute (renderer, "text", 1); + + var model = combo.model as Gtk.ListStore; + int[] scan_resolutions = {75, 150, 300, 600, 1200, 2400}; + foreach (var dpi in scan_resolutions) + { + string label; + if (dpi == default_dpi) + /* Preferences dialog: Label for default resolution in resolution list */ + label = _("%d dpi (default)").printf (dpi); + else if (dpi == 75) + /* Preferences dialog: Label for minimum resolution in resolution list */ + label = _("%d dpi (draft)").printf (dpi); + else if (dpi == 1200) + /* Preferences dialog: Label for maximum resolution in resolution list */ + label = _("%d dpi (high resolution)").printf (dpi); + else + /* Preferences dialog: Label for resolution value in resolution list (dpi = dots per inch) */ + label = _("%d dpi").printf (dpi); + + Gtk.TreeIter iter; + model.append (out iter); + model.set (iter, 0, dpi, 1, label, -1); + + if (dpi == current_dpi) + combo.set_active_iter (iter); + } + } + + [GtkCallback] + private void device_combo_changed_cb (Gtk.Widget widget) + { + if (setting_devices) + return; + user_selected_device = true; + if (get_selected_device () != null) + settings.set_string ("selected-device", get_selected_device ()); + } +} + +private class PageIcon : Gtk.DrawingArea +{ + private string text; + private double r; + private double g; + private double b; + private const int MINIMUM_WIDTH = 20; + + public PageIcon (string text, double r = 1.0, double g = 1.0, double b = 1.0) + { + this.text = text; + this.r = r; + this.g = g; + this.b = b; + } + + public override void get_preferred_width (out int minimum_width, out int natural_width) + { + minimum_width = natural_width = MINIMUM_WIDTH; + } + + public override void get_preferred_height (out int minimum_height, out int natural_height) + { + minimum_height = natural_height = (int) Math.round (MINIMUM_WIDTH * Math.SQRT2); + } + + public override void get_preferred_height_for_width (int width, out int minimum_height, out int natural_height) + { + minimum_height = natural_height = (int) (width * Math.SQRT2); + } + + public override void get_preferred_width_for_height (int height, out int minimum_width, out int natural_width) + { + minimum_width = natural_width = (int) (height / Math.SQRT2); + } + + public override bool draw (Cairo.Context c) + { + var w = get_allocated_width (); + var h = get_allocated_height (); + if (w * Math.SQRT2 > h) + w = (int) Math.round (h / Math.SQRT2); + else + h = (int) Math.round (w * Math.SQRT2); + + c.translate ((get_allocated_width () - w) / 2, (get_allocated_height () - h) / 2); + + c.rectangle (0.5, 0.5, w - 1, h - 1); + + c.set_source_rgb (r, g, b); + c.fill_preserve (); + + c.set_line_width (1.0); + c.set_source_rgb (0.0, 0.0, 0.0); + c.stroke (); + + Cairo.TextExtents extents; + c.text_extents (text, out extents); + c.translate ((w - extents.width) * 0.5 - 0.5, (h + extents.height) * 0.5 - 0.5); + c.show_text (text); + + return true; + } +} diff --git a/src/screensaver.vala b/src/screensaver.vala new file mode 100644 index 0000000..ef2dfb8 --- /dev/null +++ b/src/screensaver.vala @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2017 Stéphane Fillion + * Authors: Stéphane Fillion <stphanef3724@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. + */ + +[DBus (name = "org.freedesktop.ScreenSaver")] +public interface FreedesktopScreensaver : Object +{ + public static FreedesktopScreensaver get_proxy () throws IOError + { + return Bus.get_proxy_sync (BusType.SESSION, "org.freedesktop.ScreenSaver", "/org/freedesktop/ScreenSaver"); + } + + [DBus (name = "Inhibit")] + public abstract uint32 inhibit (string application_name, string reason_for_inhibit) throws IOError; + + [DBus (name = "UnInhibit")] + public abstract void uninhibit (uint32 cookie) throws IOError; +} diff --git a/src/simple-scan.gresource.xml b/src/simple-scan.gresource.xml index b6fe6a6..a62619e 100644 --- a/src/simple-scan.gresource.xml +++ b/src/simple-scan.gresource.xml @@ -1,6 +1,11 @@ <?xml version="1.0" encoding="UTF-8"?> <gresources> <gresource prefix="/org/gnome/SimpleScan"> - <file preprocess="xml-stripblanks">simple-scan.ui</file> + <file preprocess="xml-stripblanks">app-window.ui</file> + <file preprocess="xml-stripblanks">preferences-dialog.ui</file> + <file preprocess="xml-stripblanks">authorize-dialog.ui</file> + </gresource> + <gresource prefix="/org/gnome/SimpleScan/gtk"> + <file preprocess="xml-stripblanks">help-overlay.ui</file> </gresource> </gresources> diff --git a/src/simple-scan.vala b/src/simple-scan.vala index 2ab83f0..841e702 100644 --- a/src/simple-scan.vala +++ b/src/simple-scan.vala @@ -23,7 +23,7 @@ public class SimpleScan : Gtk.Application /* Help string for command line --debug flag */ N_("Print debugging messages"), null}, { "fix-pdf", 0, 0, OptionArg.STRING, ref fix_pdf_filename, - N_("Fix PDF files generated with older versions of Simple Scan"), "FILENAME..."}, + N_("Fix PDF files generated with older versions of Simple Scan"), "FILENAME…"}, { null } }; private static Timer log_timer; @@ -32,12 +32,16 @@ public class SimpleScan : Gtk.Application private ScanDevice? default_device = null; private bool have_devices = false; private GUsb.Context usb_context; - private UserInterface ui; + private AppWindow app; private Scanner scanner; private Book book; public SimpleScan (ScanDevice? device = null) { + /* The inhibit () method use this */ + Object (application_id: "org.gnome.SimpleScan"); + register_session = true; + default_device = device; } @@ -45,11 +49,10 @@ public class SimpleScan : Gtk.Application { base.startup (); - ui = new UserInterface (); - book = ui.book; - ui.start_scan.connect (scan_cb); - ui.stop_scan.connect (cancel_cb); - ui.email.connect (email_cb); + app = new AppWindow (); + book = app.book; + app.start_scan.connect (scan_cb); + app.stop_scan.connect (cancel_cb); scanner = Scanner.get_instance (); scanner.update_devices.connect (update_scan_devices_cb); @@ -78,15 +81,15 @@ public class SimpleScan : Gtk.Application List<ScanDevice> device_list = null; device_list.append (default_device); - ui.set_scan_devices (device_list); - ui.selected_device = default_device.name; + app.set_scan_devices (device_list); + app.selected_device = default_device.name; } } public override void activate () { base.activate (); - ui.start (); + app.start (); scanner.start (); } @@ -94,7 +97,7 @@ public class SimpleScan : Gtk.Application { base.shutdown (); book = null; - ui = null; + app = null; usb_context = null; scanner.free (); } @@ -127,7 +130,7 @@ public class SimpleScan : Gtk.Application if (!have_devices) missing_driver = suggest_driver (); - ui.set_scan_devices (devices_copy, missing_driver); + app.set_scan_devices (devices_copy, missing_driver); } /* Taken from /usr/local/Brother/sane/Brsane.ini from brscan driver */ @@ -212,7 +215,7 @@ public class SimpleScan : Gtk.Application private void authorize_cb (Scanner scanner, string resource) { string username, password; - ui.authorize (resource, out username, out password); + app.authorize (resource, out username, out password); scanner.authorize (username, password); } @@ -222,7 +225,7 @@ public class SimpleScan : Gtk.Application var page = book.get_page (-1); if (page != null && !page.has_data) { - ui.selected_page = page; + app.selected_page = page; page.start (); return page; } @@ -262,7 +265,7 @@ public class SimpleScan : Gtk.Application page.set_custom_crop (cw, ch); page.move_crop (cx, cy); } - ui.selected_page = page; + app.selected_page = page; page.start (); return page; @@ -385,118 +388,80 @@ public class SimpleScan : Gtk.Application remove_empty_page (); if (error_code != Sane.Status.CANCELLED) { - ui.show_error (/* Title of error dialog when scan failed */ - _("Failed to scan"), - error_string, - have_devices); + app.show_error_dialog (/* Title of error dialog when scan failed */ + _("Failed to scan"), + error_string); } } - private void scanner_scanning_changed_cb (Scanner scanner) - { - ui.scanning = scanner.is_scanning (); - } - - private void scan_cb (UserInterface ui, string? device, ScanOptions options) - { - debug ("Requesting scan at %d dpi from device '%s'", options.dpi, device); + private uint inhibit_cookie; + private FreedesktopScreensaver? fdss; - if (!scanner.is_scanning ()) - append_page (); - - scanner.scan (device, options); - } - - private void cancel_cb (UserInterface ui) - { - scanner.cancel (); - } - - private string? get_temporary_filename (string prefix, string extension) + private void scanner_scanning_changed_cb (Scanner scanner) { - /* NOTE: I'm not sure if this is a 100% safe strategy to use g_file_open_tmp(), close and - * use the filename but it appears to work in practise */ + var is_scanning = scanner.is_scanning (); - var filename = "%sXXXXXX.%s".printf (prefix, extension); - string path; - try - { - var fd = FileUtils.open_tmp (filename, out path); - Posix.close (fd); - } - catch (Error e) + if (is_scanning) { - warning ("Error saving email attachment: %s", e.message); - return null; - } + /* Attempt to inhibit the screensaver when scanning */ + var reason = _("Scan in progress"); - return path; - } + /* This should work on Gnome, Budgie, Cinnamon, Mate, Unity, ... + * but will not work on KDE, LXDE, XFCE, ... */ + inhibit_cookie = inhibit (app, Gtk.ApplicationInhibitFlags.IDLE, reason); - private void email_cb (UserInterface ui, string profile, int quality) - { - var saved = false; - var command_line = "xdg-email"; - - /* Save text files as PDFs */ - if (profile == "text") - { - /* Open a temporary file */ - var path = get_temporary_filename ("scan", "pdf"); - if (path != null) + if (!is_inhibited (Gtk.ApplicationInhibitFlags.IDLE)) { - var file = File.new_for_path (path); - ui.show_progress_dialog (); + /* If the previous method didn't work, try the one + * provided by Freedesktop. It should work with KDE, + * LXDE, XFCE, and maybe others as well. */ try { - book.save ("pdf", quality, file); + if ((fdss = FreedesktopScreensaver.get_proxy ()) != null) + { + inhibit_cookie = fdss.inhibit ("Simple-Scan", reason); + } } - catch (Error e) - { - ui.hide_progress_dialog (); - warning ("Unable to save email file: %s", e.message); - return; - } - command_line += " --attach %s".printf (path); + catch (IOError error) {} } } else { - for (var i = 0; i < book.n_pages; i++) + /* When finished scanning, uninhibit if inhibit was working */ + if (inhibit_cookie != 0) { - var path = get_temporary_filename ("scan", "jpg"); - if (path == null) - { - saved = false; - break; - } - - var file = File.new_for_path (path); - try - { - book.get_page (i).save ("jpeg", quality, file); - } - catch (Error e) + if (fdss == null) + uninhibit (inhibit_cookie); + else { - warning ("Unable to save email file: %s", e.message); - return; + try + { + fdss.uninhibit (inhibit_cookie); + } + catch (IOError error) {} + fdss = null; } - command_line += " --attach %s".printf (path); - if (!saved) - break; + inhibit_cookie = 0; } } - debug ("Launching email client: %s", command_line); - try - { - Process.spawn_command_line_async (command_line); - } - catch (Error e) - { - warning ("Unable to start email: %s", e.message); - } + app.scanning = is_scanning; + } + + private void scan_cb (AppWindow ui, string? device, ScanOptions options) + { + debug ("Requesting scan at %d dpi from device '%s'", options.dpi, device); + + if (!scanner.is_scanning ()) + append_page (); + + scanner.scan (device, options); + } + + private void cancel_cb (AppWindow ui) + { + scanner.cancel (); } private static void log_cb (string? log_domain, LogLevelFlags log_level, string message) @@ -616,7 +581,7 @@ public class SimpleScan : Gtk.Application Intl.textdomain (GETTEXT_PACKAGE); var c = new OptionContext (/* Arguments and description for --help text */ - _("[DEVICE...] - Scanning utility")); + _("[DEVICE…] — Scanning utility")); c.add_main_entries (options, GETTEXT_PACKAGE); c.add_group (Gtk.get_option_group (true)); try @@ -627,7 +592,7 @@ public class SimpleScan : Gtk.Application { stderr.printf ("%s\n", e.message); stderr.printf (/* Text printed out when an unknown command-line argument provided */ - _("Run '%s --help' to see a full list of available command line options."), args[0]); + _("Run “%s --help” to see a full list of available command line options."), args[0]); stderr.printf ("\n"); return Posix.EXIT_FAILURE; } |