[simple-scan/relayout: 1/2] WIP: Relayout



commit 3a403049abb50c018b26b6116776a0e631910a64
Author: Robert Ancell <robert ancell canonical com>
Date:   Fri Aug 24 15:34:40 2018 +1200

    WIP: Relayout

 src/app-window.ui       |  96 +++++++-
 src/app-window.vala     |  92 ++++----
 src/book-view.vala      | 612 -----------------------------------------------
 src/meson.build         |   4 +-
 src/page-view.vala      | 616 ++++++------------------------------------------
 src/page.vala           |   6 +-
 src/thumbnail-view.vala | 143 +++++++++++
 7 files changed, 354 insertions(+), 1215 deletions(-)
---
diff --git a/src/app-window.ui b/src/app-window.ui
index 44a0203..2b5a2d3 100644
--- a/src/app-window.ui
+++ b/src/app-window.ui
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.20.0 -->
+<!-- Generated with glade 3.22.1 -->
 <interface>
   <requires lib="gtk+" version="3.12"/>
   <object class="GtkMenu" id="page_menu">
@@ -249,10 +249,9 @@
                     </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"/>
+                        <signal name="activate-link" handler="status_label_activate_link_cb" swapped="no"/>
                         <style>
                           <class name="dim-label"/>
                         </style>
@@ -266,18 +265,99 @@
               </packing>
             </child>
             <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="GtkActionBar" id="action_bar">
+                  <object class="GtkBox">
                     <property name="visible">True</property>
                     <property name="can_focus">False</property>
+                    <property name="orientation">horizontal</property>
+                    <property name="vexpand">True</property>
+                    <child>
+                      <object class="GtkRevealer" id="thumbnail_revealer">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="transition_type">slide-right</property>
+                        <child>
+                          <object class="GtkBox">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="orientation">vertical</property>
+                            <child>
+                              <object class="GtkScrolledWindow">
+                                <property name="visible">True</property>
+                                <property name="can_focus">True</property>
+                                <property name="hscrollbar_policy">never</property>
+                                <property name="shadow_type">in</property>
+                                <property name="width_request">150</property> <!-- Why is min_content_width 
not working?! -->
+                                <child>
+                                  <object class="GtkBox" id="thumbnail_box">
+                                    <property name="visible">True</property>
+                                    <property name="can_focus">False</property>
+                                    <property name="orientation">vertical</property>
+                                    <property name="valign">start</property>
+                                    <property name="border_width">12</property>
+                                    <property name="spacing">12</property>
+                                  </object>
+                                </child>
+                              </object>
+                              <packing>
+                                <property name="expand">True</property>
+                                <property name="fill">True</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkButton">
+                                <property name="visible">True</property>
+                                <property name="can_focus">True</property>
+                                <property name="receives_default">False</property>
+                                <property name="label" translatable="yes">Delete All</property>
+                                <property name="margin_top">6</property>
+                                <property name="margin_bottom">6</property>
+                                <property name="margin_left">6</property>
+                                <property name="margin_right">6</property>
+                                <signal name="clicked" handler="new_button_clicked_cb" swapped="no"/>
+                              </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">True</property>
+                              </packing>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkBox" id="page_box">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="orientation">vertical</property>
+                        <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">True</property>
+                        <property name="fill">True</property>
+                      </packing>
+                    </child>
                   </object>
-                  <packing>
-                    <property name="pack_type">end</property>
-                  </packing>
                 </child>
               </object>
               <packing>
diff --git a/src/app-window.vala b/src/app-window.vala
index d673725..688ac89 100644
--- a/src/app-window.vala
+++ b/src/app-window.vala
@@ -45,7 +45,7 @@ public class AppWindow : Gtk.ApplicationWindow
     [GtkChild]
     private Gtk.Label status_secondary_label;
     [GtkChild]
-    private Gtk.Box main_vbox;
+    private Gtk.Box page_box;
     [GtkChild]
     private Gtk.RadioMenuItem custom_crop_menuitem;
     [GtkChild]
@@ -101,6 +101,11 @@ public class AppWindow : Gtk.ApplicationWindow
     [GtkChild]
     private Gtk.MenuButton menu_button;
 
+    [GtkChild]
+    private Gtk.Revealer thumbnail_revealer;
+    [GtkChild]
+    private Gtk.Box thumbnail_box;
+
     private bool have_devices = false;
     private string? missing_driver = null;
 
@@ -114,17 +119,22 @@ public class AppWindow : Gtk.ApplicationWindow
     {
         get
         {
-            return book_view.selected_page;
+            return page_view.page;
         }
         set
         {
-            book_view.selected_page = value;
+            page_view.page = value;
+            foreach (var child in thumbnail_box.get_children ())
+            {
+                var thumbnail = (ThumbnailView) child;
+                thumbnail.selected = thumbnail.page == value;
+            }
         }
     }
 
     private AutosaveManager autosave_manager;
 
-    private BookView book_view;
+    private PageView page_view;
     private bool updating_page_menu;
 
     private string document_hint = "photo";
@@ -200,7 +210,7 @@ public class AppWindow : Gtk.ApplicationWindow
         else
         {
             stack.set_visible_child_name ("document");
-            book_view.selected_page = book.get_page (0);
+            selected_page = book.get_page (0);
             book_needs_saving = true;
             book_changed_cb (book);
         }
@@ -731,7 +741,7 @@ public class AppWindow : Gtk.ApplicationWindow
 
     private void update_page_menu ()
     {
-        var page = book_view.selected_page;
+        var page = page_view.page;
         if (page == null)
         {
             page_move_left_menuitem.sensitive = false;
@@ -745,7 +755,7 @@ public class AppWindow : Gtk.ApplicationWindow
         }
     }
 
-    private void page_selected_cb (BookView view, Page? page)
+    /*private void page_selected_cb (BookView view, Page? page)
     {
         if (page == null)
             return;
@@ -783,9 +793,9 @@ public class AppWindow : Gtk.ApplicationWindow
         crop_button.active = page.has_crop;
 
         updating_page_menu = false;
-    }
+    }*/
 
-    private void show_page_cb (BookView view, Page page)
+    /*private void show_page_cb (BookView view, Page page)
     {
         File file;
         try
@@ -796,7 +806,7 @@ public class AppWindow : Gtk.ApplicationWindow
         }
         catch (Error e)
         {
-            show_error_dialog (/* Error message display when unable to save image for preview */
+            show_error_dialog (/* Error message display when unable to save image for preview * /
                                _("Unable to save image for preview"),
                                e.message);
             return;
@@ -808,23 +818,23 @@ public class AppWindow : Gtk.ApplicationWindow
         }
         catch (Error e)
         {
-            show_error_dialog (/* Error message display when unable to preview image */
+            show_error_dialog (/* Error message display when unable to preview image * /
                                _("Unable to open image preview application"),
                                e.message);
         }
-    }
+    }*/
 
-    private void show_page_menu_cb (BookView view, Gdk.Event event)
+    /*private void show_page_menu_cb (BookView view, Gdk.Event event)
     {
         page_menu.popup_at_pointer (event);
-    }
+    }*/
 
     [GtkCallback]
     private void rotate_left_button_clicked_cb (Gtk.Widget widget)
     {
         if (updating_page_menu)
             return;
-        var page = book_view.selected_page;
+        var page = page_view.page;
         if (page != null)
             page.rotate_left ();
     }
@@ -834,7 +844,7 @@ public class AppWindow : Gtk.ApplicationWindow
     {
         if (updating_page_menu)
             return;
-        var page = book_view.selected_page;
+        var page = page_view.page;
         if (page != null)
             page.rotate_right ();
     }
@@ -846,7 +856,7 @@ public class AppWindow : Gtk.ApplicationWindow
         if (updating_page_menu)
             return;
 
-        var page = book_view.selected_page;
+        var page = page_view.page;
         if (page == null)
         {
             warning ("Trying to set crop but no selected page");
@@ -934,7 +944,7 @@ public class AppWindow : Gtk.ApplicationWindow
     [GtkCallback]
     private void crop_rotate_menuitem_activate_cb (Gtk.Widget widget)
     {
-        var page = book_view.selected_page;
+        var page = page_view.page;
         if (page == null)
             return;
         page.rotate_crop ();
@@ -943,7 +953,7 @@ public class AppWindow : Gtk.ApplicationWindow
     [GtkCallback]
     private void page_move_left_menuitem_activate_cb (Gtk.Widget widget)
     {
-        var page = book_view.selected_page;
+        var page = page_view.page;
         var index = book.get_page_index (page);
         if (index > 0)
             book.move_page (page, index - 1);
@@ -952,7 +962,7 @@ public class AppWindow : Gtk.ApplicationWindow
     [GtkCallback]
     private void page_move_right_menuitem_activate_cb (Gtk.Widget widget)
     {
-        var page = book_view.selected_page;
+        var page = page_view.page;
         var index = book.get_page_index (page);
         if (index < book.n_pages - 1)
             book.move_page (page, book.get_page_index (page) + 1);
@@ -961,7 +971,7 @@ public class AppWindow : Gtk.ApplicationWindow
     [GtkCallback]
     private void page_delete_menuitem_activate_cb (Gtk.Widget widget)
     {
-        book_view.book.delete_page (book_view.selected_page);
+        book.delete_page (page_view.page);
     }
 
     private void reorder_document ()
@@ -1145,7 +1155,7 @@ public class AppWindow : Gtk.ApplicationWindow
     [GtkCallback]
     private void copy_to_clipboard_button_clicked_cb (Gtk.Widget widget)
     {
-        var page = book_view.selected_page;
+        var page = page_view.page;
         if (page != null)
             page.copy_to_clipboard (this);
     }
@@ -1484,16 +1494,30 @@ public class AppWindow : Gtk.ApplicationWindow
 
     private void page_added_cb (Book book, Page page)
     {
+        var view = new ThumbnailView ();
+        view.page = page;
+        view.visible = true;
+        view.selected = page == selected_page;
+        view.clicked.connect (select_page_cb);
+        thumbnail_box.pack_start (view);
+        thumbnail_revealer.reveal_child = book.n_pages > 1;
         update_page_menu ();
     }
 
+    private void select_page_cb (ThumbnailView view)
+    {
+        selected_page = view.page;
+    }
+
     private void reordered_cb (Book book)
     {
+        // FIXME: Move thumbnail
         update_page_menu ();
     }
 
     private void page_removed_cb (Book book, Page page)
     {
+        // FIXME: Remove thumbnail
         update_page_menu ();
     }
 
@@ -1545,14 +1569,6 @@ public class AppWindow : Gtk.ApplicationWindow
 
         app.add_window (this);
 
-        /* Populate ActionBar (not supported in Glade) */
-        /* https://bugzilla.gnome.org/show_bug.cgi?id=769966 */
-        var button = new Gtk.Button.with_label (/* Label on new document button */
-                                               _("Start Again…"));
-        button.visible = true;
-        button.clicked.connect (new_document_cb);
-        action_bar.pack_start (button);
-
         var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 10);
         box.visible = true;
         action_bar.set_center_widget (box);
@@ -1562,7 +1578,7 @@ public class AppWindow : Gtk.ApplicationWindow
         rotate_box.visible = true;
         box.add (rotate_box);
 
-        button = new Gtk.Button.from_icon_name ("object-rotate-left-symbolic");
+        var button = new Gtk.Button.from_icon_name ("object-rotate-left-symbolic");
         button.visible = true;
         button.image.margin_start = 18;
         button.image.margin_end = 18;
@@ -1607,21 +1623,17 @@ public class AppWindow : Gtk.ApplicationWindow
         delete_button.image.margin_end = 18;
         /* Tooltip for delete button */
         delete_button.tooltip_text = _("Delete the selected page");
-        delete_button.clicked.connect (() => { book_view.book.delete_page (book_view.selected_page); });
+        delete_button.clicked.connect (() => { book.delete_page (page_view.page); });
         box.add (delete_button);
 
         var document_type = settings.get_string ("document-type");
         if (document_type != null)
             set_document_hint (document_type);
 
-        book_view = new BookView (book);
-        book_view.border_width = 18;
-        book_view.vexpand = true;
-        main_vbox.add (book_view);
-        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;
+        page_view = new PageView ();
+        page_view.margin = 18;
+        page_box.add (page_view);
+        page_view.visible = true;
 
         preferences_dialog.transient_for = this;
 
diff --git a/src/meson.build b/src/meson.build
index 9e40e42..9e0326e 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -21,15 +21,15 @@ simple_scan = executable ('simple-scan',
                           [ 'config.vapi',
                             'app-window.vala',
                             'authorize-dialog.vala',
+                            'autosave-manager.vala',
                             'book.vala',
-                            'book-view.vala',
                             'page.vala',
                             'page-view.vala',
                             'preferences-dialog.vala',
                             'simple-scan.vala',
                             'scanner.vala',
                             'screensaver.vala',
-                            'autosave-manager.vala' ] + resources,
+                            'thumbnail-view.vala' ] + resources,
                           dependencies: dependencies,
                           vala_args: vala_args,
                           c_args: [ '-DVERSION="@0@"'.format (meson.project_version ()),
diff --git a/src/page-view.vala b/src/page-view.vala
index 03fecfb..ad83716 100644
--- a/src/page-view.vala
+++ b/src/page-view.vala
@@ -23,47 +23,39 @@ public enum CropLocation
     BOTTOM_RIGHT
 }
 
-public class PageView
+public class PageView : Gtk.DrawingArea
 {
     /* Page being rendered */
-    public Page page { get; private set; }
-
-    /* Image to render at current resolution */
-    private Gdk.Pixbuf? image = null;
-
-    /* Border around image */
-    private bool selected_ = false;
-    public bool selected
-    {
-        get { return selected_; }
-        set    
-        {
-            if ((this.selected && selected) || (!this.selected && !selected))
-                return;
-            this.selected = selected;
-            changed ();
+    private Page page_ = null;
+    public Page page {
+        get {
+            return page_;
+        }
+        set {
+            if (page_ != null) {
+                page_.pixels_changed.disconnect (page_pixels_changed_cb);
+                page_.size_changed.disconnect (page_size_changed_cb);
+                page_.crop_changed.disconnect (page_overlay_changed_cb);
+                page_.scan_line_changed.disconnect (page_overlay_changed_cb);
+                page_.scan_direction_changed.disconnect (scan_direction_changed_cb);
+            }
+            page_ = value;
+            page_.pixels_changed.connect (page_pixels_changed_cb);
+            page_.size_changed.connect (page_size_changed_cb);
+            page_.crop_changed.connect (page_overlay_changed_cb);
+            page_.scan_line_changed.connect (page_overlay_changed_cb);
+            page_.scan_direction_changed.connect (scan_direction_changed_cb);
         }
     }
 
     private int border_width = 1;
 
-    /* True if image needs to be regenerated */
-    private bool update_image = true;
-
     /* Direction of currently scanned image */
     private ScanDirection scan_direction;
 
     /* Next scan line to render */
     private int scan_line;
 
-    /* Dimensions of image to generate */
-    private int width_;
-    private int height_;
-
-    /* Location to place this page */
-    public int x_offset { get; set; }
-    public int y_offset { get; set; }
-
     private CropLocation crop_location;
     private double selected_crop_px;
     private double selected_crop_py;
@@ -79,485 +71,52 @@ public class PageView
     private int animate_segment;
     private uint animate_timeout;
 
-    public signal void size_changed ();
-    public signal void changed ();
-
-    public PageView (Page page)
-    {
-        this.page = page;
-        page.pixels_changed.connect (page_pixels_changed_cb);
-        page.size_changed.connect (page_size_changed_cb);
-        page.crop_changed.connect (page_overlay_changed_cb);
-        page.scan_line_changed.connect (page_overlay_changed_cb);
-        page.scan_direction_changed.connect (scan_direction_changed_cb);
-    }
-
-    ~PageView ()
-    {
-        page.pixels_changed.disconnect (page_pixels_changed_cb);
-        page.size_changed.disconnect (page_size_changed_cb);
-        page.crop_changed.disconnect (page_overlay_changed_cb);
-        page.scan_line_changed.disconnect (page_overlay_changed_cb);
-        page.scan_direction_changed.disconnect (scan_direction_changed_cb);
-    }
-
-    private uchar get_sample (uchar[] pixels, int offset, int x, int depth, int sample)
+    public PageView ()
     {
-        // FIXME
-        return 0xFF;
+        //add_events (Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK);
     }
 
-    private void get_pixel (Page page, int x, int y, uchar[] pixel)
+    private void get_page_size (out int width, out int height)
     {
-        switch (page.scan_direction)
-        {
-        case ScanDirection.TOP_TO_BOTTOM:
-            break;
-        case ScanDirection.BOTTOM_TO_TOP:
-            x = page.scan_width - x - 1;
-            y = page.scan_height - y - 1;
-            break;
-        case ScanDirection.LEFT_TO_RIGHT:
-            var t = x;
-            x = page.scan_width - y - 1;
-            y = t;
-            break;
-        case ScanDirection.RIGHT_TO_LEFT:
-            var t = x;
-            x = y;
-            y = page.scan_height - t - 1;
-            break;
-        }
-
-        var depth = page.depth;
-        var n_channels = page.n_channels;
-        unowned uchar[] pixels = page.get_pixels ();
-        var offset = page.rowstride * y;
-
-        /* Optimise for 8 bit images */
-        if (depth == 8 && n_channels == 3)
-        {
-            var o = offset + x * n_channels;
-            pixel[0] = pixels[o];
-            pixel[1] = pixels[o+1];
-            pixel[2] = pixels[o+2];
-            return;
-        }
-        else if (depth == 8 && n_channels == 1)
-        {
-            pixel[0] = pixel[1] = pixel[2] = pixels[offset + x];
-            return;
-        }
-
-        /* Optimise for bitmaps */
-        else if (depth == 1 && n_channels == 1)
-        {
-            var o = offset + (x / 8);
-            pixel[0] = pixel[1] = pixel[2] = (pixels[o] & (0x80 >> (x % 8))) != 0 ? 0x00 : 0xFF;
-            return;
-        }
-
-        /* Optimise for 2 bit images */
-        else if (depth == 2 && n_channels == 1)
-        {
-            int block_shift[4] = { 6, 4, 2, 0 };
-
-            var o = offset + (x / 4);
-            var sample = (pixels[o] >> block_shift[x % 4]) & 0x3;
-            sample = sample * 255 / 3;
-
-            pixel[0] = pixel[1] = pixel[2] = (uchar) sample;
-            return;
-        }
+        var w = get_allocated_width () - border_width * 2;
+        var h = get_allocated_height () - border_width * 2;
 
-        /* Use slow method */
-        pixel[0] = get_sample (pixels, offset, x, depth, x * n_channels);
-        pixel[1] = get_sample (pixels, offset, x, depth, x * n_channels + 1);
-        pixel[2] = get_sample (pixels, offset, x, depth, x * n_channels + 2);
-    }
-
-    private void set_pixel (Page page, double l, double r, double t, double b, uchar[] output, int offset)
-    {
-        /* Decimation:
-         *
-         * Target pixel is defined by (t,l)-(b,r)
-         * It touches 16 pixels in original image
-         * It completely covers 4 pixels in original image (T,L)-(B,R)
-         * Add covered pixels and add weighted partially covered pixels.
-         * Divide by total area.
-         *
-         *      l  L           R   r
-         *   +-----+-----+-----+-----+
-         *   |     |     |     |     |
-         * t |  +--+-----+-----+---+ |
-         * T +--+--+-----+-----+---+-+
-         *   |  |  |     |     |   | |
-         *   |  |  |     |     |   | |
-         *   +--+--+-----+-----+---+-+
-         *   |  |  |     |     |   | |
-         *   |  |  |     |     |   | |
-         * B +--+--+-----+-----+---+-+
-         *   |  |  |     |     |   | |
-         * b |  +--+-----+-----+---+ |
-         *   +-----+-----+-----+-----+
-         *
-         *
-         * Interpolation:
-         *
-         *             l    r
-         *   +-----+-----+-----+-----+
-         *   |     |     |     |     |
-         *   |     |     |     |     |
-         *   +-----+-----+-----+-----+
-         * t |     |   +-+--+  |     |
-         *   |     |   | |  |  |     |
-         *   +-----+---+-+--+--+-----+
-         * b |     |   +-+--+  |     |
-         *   |     |     |     |     |
-         *   +-----+-----+-----+-----+
-         *   |     |     |     |     |
-         *   |     |     |     |     |
-         *   +-----+-----+-----+-----+
-         *
-         * Same again, just no completely covered pixels.
-         */
-
-        var L = (int) l;
-        if (L != l)
-            L++;
-        var R = (int) r;
-        var T = (int) t;
-        if (T != t)
-            T++;
-        var B = (int) b;
-
-        var red = 0.0;
-        var green = 0.0;
-        var blue = 0.0;
-
-        /* Target can fit inside one source pixel
-         * +-----+
-         * |     |
-         * | +--+|      +-----+-----+      +-----+      +-----+      +-----+
-         * +-+--++  or  |   +-++    |  or  | +-+ |  or  | +--+|  or  |  +--+
-         * | +--+|      |   +-++    |      | +-+ |      | |  ||      |  |  |
-         * |     |      +-----+-----+      +-----+      +-+--++      +--+--+
-         * +-----+
-         */
-        if ((r - l <= 1.0 && (int)r == (int)l) || (b - t <= 1.0 && (int)b == (int)t))
-        {
-            /* Inside */
-            if ((int)l == (int)r || (int)t == (int)b)
-            {
-                uchar p[3];
-                get_pixel (page, (int)l, (int)t, p);
-                output[offset] = p[0];
-                output[offset+1] = p[1];
-                output[offset+2] = p[2];
-                return;
-            }
-
-            /* Stradling horizontal edge */
-            if (L > R)
-            {
-                uchar p[3];
-                get_pixel (page, R, T-1, p);
-                red   += p[0] * (r-l)*(T-t);
-                green += p[1] * (r-l)*(T-t);
-                blue  += p[2] * (r-l)*(T-t);
-                for (var y = T; y < B; y++)
-                {
-                    get_pixel (page, R, y, p);
-                    red   += p[0] * (r-l);
-                    green += p[1] * (r-l);
-                    blue  += p[2] * (r-l);
-                }
-                get_pixel (page, R, B, p);
-                red   += p[0] * (r-l)*(b-B);
-                green += p[1] * (r-l)*(b-B);
-                blue  += p[2] * (r-l)*(b-B);
-            }
-            /* Stradling vertical edge */
-            else
-            {
-                uchar p[3];
-                get_pixel (page, L - 1, B, p);
-                red   += p[0] * (b-t)*(L-l);
-                green += p[1] * (b-t)*(L-l);
-                blue  += p[2] * (b-t)*(L-l);
-                for (var x = L; x < R; x++) {
-                    get_pixel (page, x, B, p);
-                    red   += p[0] * (b-t);
-                    green += p[1] * (b-t);
-                    blue  += p[2] * (b-t);
-                }
-                get_pixel (page, R, B, p);
-                red   += p[0] * (b-t)*(r-R);
-                green += p[1] * (b-t)*(r-R);
-                blue  += p[2] * (b-t)*(r-R);
-            }
-
-            var scale = 1.0 / ((r - l) * (b - t));
-            output[offset] = (uchar)(red * scale + 0.5);
-            output[offset+1] = (uchar)(green * scale + 0.5);
-            output[offset+2] = (uchar)(blue * scale + 0.5);
-            return;
-        }
-
-        /* Add the middle pixels */
-        for (var x = L; x < R; x++)
-        {
-            for (var y = T; y < B; y++)
-            {
-                uchar p[3];
-                get_pixel (page, x, y, p);
-                red   += p[0];
-                green += p[1];
-                blue  += p[2];
-            }
-        }
-
-        /* Add the weighted top and bottom pixels */
-        for (var x = L; x < R; x++)
-        {
-            if (t != T)
-            {
-                uchar p[3];
-                get_pixel (page, x, T - 1, p);
-                red   += p[0] * (T - t);
-                green += p[1] * (T - t);
-                blue  += p[2] * (T - t);
-            }
-
-            if (b != B)
-            {
-                uchar p[3];
-                get_pixel (page, x, B, p);
-                red   += p[0] * (b - B);
-                green += p[1] * (b - B);
-                blue  += p[2] * (b - B);
-            }
-        }
-
-        /* Add the left and right pixels */
-        for (var y = T; y < B; y++)
-        {
-            if (l != L)
-            {
-                uchar p[3];
-                get_pixel (page, L - 1, y, p);
-                red   += p[0] * (L - l);
-                green += p[1] * (L - l);
-                blue  += p[2] * (L - l);
-            }
-
-            if (r != R)
-            {
-                uchar p[3];
-                get_pixel (page, R, y, p);
-                red   += p[0] * (r - R);
-                green += p[1] * (r - R);
-                blue  += p[2] * (r - R);
-            }
-        }
-
-        /* Add the corner pixels */
-        if (l != L && t != T)
-        {
-            uchar p[3];
-            get_pixel (page, L - 1, T - 1, p);
-            red   += p[0] * (L - l)*(T - t);
-            green += p[1] * (L - l)*(T - t);
-            blue  += p[2] * (L - l)*(T - t);
-        }
-        if (r != R && t != T)
-        {
-            uchar p[3];
-            get_pixel (page, R, T - 1, p);
-            red   += p[0] * (r - R)*(T - t);
-            green += p[1] * (r - R)*(T - t);
-            blue  += p[2] * (r - R)*(T - t);
-        }
-        if (r != R && b != B)
-        {
-            uchar p[3];
-            get_pixel (page, R, B, p);
-            red   += p[0] * (r - R)*(b - B);
-            green += p[1] * (r - R)*(b - B);
-            blue  += p[2] * (r - R)*(b - B);
-        }
-        if (l != L && b != B)
-        {
-            uchar p[3];
-            get_pixel (page, L - 1, B, p);
-            red   += p[0] * (L - l)*(b - B);
-            green += p[1] * (L - l)*(b - B);
-            blue  += p[2] * (L - l)*(b - B);
-        }
-
-        /* Scale pixel values and clamp in range [0, 255] */
-        var scale = 1.0 / ((r - l) * (b - t));
-        output[offset] = (uchar)(red * scale + 0.5);
-        output[offset+1] = (uchar)(green * scale + 0.5);
-        output[offset+2] = (uchar)(blue * scale + 0.5);
-    }
-
-    private void update_preview (Page page, ref Gdk.Pixbuf? output_image, int output_width, int 
output_height,
-                                 ScanDirection scan_direction, int old_scan_line, int scan_line)
-    {
-        var input_width = page.width;
-        var input_height = page.height;
-
-        /* Create new image if one does not exist or has changed size */
-        int L, R, T, B;
-        if (output_image == null ||
-            output_image.width != output_width ||
-            output_image.height != output_height)
-        {
-            output_image = new Gdk.Pixbuf (Gdk.Colorspace.RGB,
-                                           false,
-                                           8,
-                                           output_width,
-                                           output_height);
-
-            /* Update entire image */
-            L = 0;
-            R = output_width - 1;
-            T = 0;
-            B = output_height - 1;
-        }
-        /* Otherwise only update changed area */
+        /* Fit page inside available space */
+        if (w * page.height > h * page.width)
+            w = h * page.width / page.height;
         else
-        {
-            switch (scan_direction)
-            {
-            case ScanDirection.TOP_TO_BOTTOM:
-                L = 0;
-                R = output_width - 1;
-                T = (int)((double)old_scan_line * output_height / input_height);
-                B = (int)((double)scan_line * output_height / input_height + 0.5);
-                break;
-            case ScanDirection.LEFT_TO_RIGHT:
-                L = (int)((double)old_scan_line * output_width / input_width);
-                R = (int)((double)scan_line * output_width / input_width + 0.5);
-                T = 0;
-                B = output_height - 1;
-                break;
-            case ScanDirection.BOTTOM_TO_TOP:
-                L = 0;
-                R = output_width - 1;
-                T = (int)((double)(input_height - scan_line) * output_height / input_height);
-                B = (int)((double)(input_height - old_scan_line) * output_height / input_height + 0.5);
-                break;
-            case ScanDirection.RIGHT_TO_LEFT:
-                L = (int)((double)(input_width - scan_line) * output_width / input_width);
-                R = (int)((double)(input_width - old_scan_line) * output_width / input_width + 0.5);
-                T = 0;
-                B = output_height - 1;
-                break;
-            default:
-                L = R = B = T = 0;
-                break;
-            }
-        }
-
-        /* FIXME: There's an off by one error in there somewhere... */
-        if (R >= output_width)
-            R = output_width - 1;
-        if (B >= output_height)
-            B = output_height - 1;
-
-        return_if_fail (L >= 0);
-        return_if_fail (R < output_width);
-        return_if_fail (T >= 0);
-        return_if_fail (B < output_height);
-        return_if_fail (output_image != null);
-
-        unowned uchar[] output = output_image.get_pixels ();
-        var output_rowstride = output_image.rowstride;
-        var output_n_channels = output_image.n_channels;
-
-        if (!page.has_data)
-        {
-            for (var x = L; x <= R; x++)
-                for (var y = T; y <= B; y++)
-                {
-                    var o = output_rowstride * y + x * output_n_channels;
-                    output[o] = output[o+1] = output[o+2] = 0xFF;
-                }
-            return;
-        }
-
-        /* Update changed area */
-        for (var x = L; x <= R; x++)
-        {
-            var l = (double)x * input_width / output_width;
-            var r = (double)(x + 1) * input_width / output_width;
-
-            for (var y = T; y <= B; y++)
-            {
-                var t = (double)y * input_height / output_height;
-                var b = (double)(y + 1) * input_height / output_height;
-
-                set_pixel (page,
-                           l, r, t, b,
-                           output, output_rowstride * y + x * output_n_channels);
-            }
-        }
-    }
-
-    private int get_preview_width ()
-    {
-        return width_ - border_width * 2;
-    }
-
-    private int get_preview_height ()
-    {
-        return height_ - border_width * 2;
-    }
-
-    private void update_page_view ()
-    {
-        if (!update_image)
-            return;
-
-        var old_scan_line = scan_line;
-        var scan_line = page.scan_line;
+            h = w * page.height / page.width;
 
-        /* Delete old image if scan direction changed */
-        var left_steps = scan_direction - page.scan_direction;
-        if (left_steps != 0 && image != null)
-            image = null;
-        scan_direction = page.scan_direction;
-
-        update_preview (page,
-                        ref image,
-                        get_preview_width (),
-                        get_preview_height (),
-                        page.scan_direction, old_scan_line, scan_line);
-
-        update_image = false;
-        this.scan_line = scan_line;
+        width = w;
+        height = h;
     }
 
     private int page_to_screen_x (int x)
     {
-        return (int) ((double)x * get_preview_width () / page.width + 0.5);
+        int w, h;
+        get_page_size (out w, out h);
+        return (int) ((double)x * w / page.width + 0.5);
     }
 
     private int page_to_screen_y (int y)
     {
-        return (int) ((double)y * get_preview_height () / page.height + 0.5);
+        int w, h;
+        get_page_size (out w, out h);
+        return (int) ((double)y * h / page.height + 0.5);
     }
 
     private int screen_to_page_x (int x)
     {
-        return (int) ((double)x * page.width / get_preview_width () + 0.5);
+        int w, h;
+        get_page_size (out w, out h);
+        return (int) ((double)x * page.width / w + 0.5);
     }
 
     private int screen_to_page_y (int y)
     {
-        return (int) ((double)y * page.height / get_preview_height () + 0.5);
+        int w, h;
+        get_page_size (out w, out h);
+        return (int) ((double)y * page.height / h + 0.5);
     }
 
     private CropLocation get_crop_location (int x, int y)
@@ -787,13 +346,13 @@ public class PageView
     {
         /* Complete crop */
         crop_location = CropLocation.NONE;
-        changed ();
+        queue_draw ();
     }
 
     private bool animation_cb ()
     {
         animate_segment = (animate_segment + 1) % animate_n_segments;
-        changed ();
+        queue_draw ();
         return true;
     }
 
@@ -820,30 +379,35 @@ public class PageView
         }
     }
 
-    public void render (Cairo.Context context)
+    public override bool draw (Cairo.Context context)
     {
         update_animation ();
-        update_page_view ();
 
-        var w = get_preview_width ();
-        var h = get_preview_height ();
+        int w, h;
+        get_page_size (out w, out h);
+
+        var x_offset = (get_allocated_width () - w - border_width * 2) / 2;
+        var y_offset = (get_allocated_height () - h - border_width * 2) / 2;
 
         context.set_line_width (1);
-        context.translate (x_offset, y_offset);
 
         /* Draw page border */
-        context.set_source_rgb (0, 0, 0);
+        context.set_source_rgba (0, 0, 0, 0.25);
         context.set_line_width (border_width);
-        context.rectangle ((double)border_width / 2,
-                           (double)border_width / 2,
-                           width_ - border_width,
-                           height_ - border_width);
+        context.rectangle (x_offset + (double)border_width / 2,
+                           y_offset + (double)border_width / 2,
+                           w + border_width,
+                           h + border_width);
         context.stroke ();
 
         /* Draw image */
-        context.translate (border_width, border_width);
+        var image = page.get_image (); // FIXME: Cache and mipmap
+        context.translate (x_offset + border_width, y_offset + border_width);
+        context.save ();
+        context.scale ((double) w / image.width, (double) h / image.height);
         Gdk.cairo_set_source_pixbuf (context, image, 0, 0);
         context.paint ();
+        context.restore ();
 
         /* Draw throbber */
         if (page.is_scanning && !page.has_data)
@@ -947,75 +511,27 @@ public class PageView
             context.set_source_rgb (0.0, 0.0, 0.0);
             context.stroke ();
         }
-    }
-
-    public int width
-    {
-        get { return width_; }
-        set
-        {
-            // FIXME: Automatically update when get updated image
-            var h = (int) ((double) value * page.height / page.width);
-            if (width_ == value && height_ == h)
-                return;
-
-            width_ = value;
-            height_ = h;
 
-            /* Regenerate image */
-            update_image = true;
-
-            size_changed ();
-            changed ();
-        }
-    }
-
-    public int height
-    {
-        get { return height_; }
-        set
-        {
-            // FIXME: Automatically update when get updated image
-            var w = (int) ((double) value * page.width / page.height);
-            if (width_ == w && height_ == value)
-                return;
-
-            width_ = w;
-            height_ = value;
-
-            /* Regenerate image */
-            update_image = true;
-
-            size_changed ();
-            changed ();
-        }
+        return true;
     }
 
     private void page_pixels_changed_cb (Page p)
     {
-        /* Regenerate image */
-        update_image = true;
-        changed ();
+        queue_draw ();
     }
 
     private void page_size_changed_cb (Page p)
     {
-        /* Regenerate image */
-        update_image = true;
-        size_changed ();
-        changed ();
+        queue_draw ();
     }
 
     private void page_overlay_changed_cb (Page p)
     {
-        changed ();
+        queue_draw ();
     }
 
     private void scan_direction_changed_cb (Page p)
     {
-        /* Regenerate image */
-        update_image = true;
-        size_changed ();
-        changed ();
+        queue_draw ();
     }
 }
diff --git a/src/page.vala b/src/page.vala
index 13de4d2..35b904a 100644
--- a/src/page.vala
+++ b/src/page.vala
@@ -587,7 +587,7 @@ public class Page
         pixel[offset+2] = get_sample (pixels, line_offset, x, depth, n_channels, 2);
     }
 
-    public Gdk.Pixbuf get_image (bool apply_crop)
+    public Gdk.Pixbuf get_image (bool apply_crop = true)
     {
         int l, r, t, b;
         if (apply_crop && has_crop)
@@ -651,14 +651,14 @@ public class Page
     {
         var display = window.get_display ();
         var clipboard = Gtk.Clipboard.get_for_display (display, Gdk.SELECTION_CLIPBOARD);
-        var image = get_image (true);
+        var image = get_image ();
         clipboard.set_image (image);
     }
 
     public void save_png (File file) throws Error
     {
         var stream = file.replace (null, false, FileCreateFlags.NONE, null);
-        var image = get_image (true);
+        var image = get_image ();
 
         string? icc_profile_data = null;
         if (color_profile != null)
diff --git a/src/thumbnail-view.vala b/src/thumbnail-view.vala
new file mode 100644
index 0000000..d6eadcd
--- /dev/null
+++ b/src/thumbnail-view.vala
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2018 Canonical Ltd.
+ * Author: Robert Ancell <robert ancell canonical 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.
+ */
+
+public class ThumbnailView : Gtk.DrawingArea
+{
+    /* Page being rendered */
+    private Page page_ = null;
+    public Page page {
+        get {
+            return page_;
+        }
+        set {
+            if (page_ != null) {
+                page_.pixels_changed.disconnect (page_changed_cb);
+                page_.size_changed.disconnect (page_changed_cb);
+                page_.scan_line_changed.disconnect (page_changed_cb);
+                page_.scan_direction_changed.disconnect (page_changed_cb);
+            }
+            page_ = value;
+            page_.pixels_changed.connect (page_changed_cb);
+            page_.size_changed.connect (page_changed_cb);
+            page_.scan_line_changed.connect (page_changed_cb);
+            page_.scan_direction_changed.connect (page_changed_cb);
+        }
+    }
+
+    /* Border around image */
+    private bool selected_ = false;
+    public bool selected
+    {
+        get { return selected_; }
+        set
+        {
+            if (selected_ == value)
+                return;
+            selected_ = value;
+            queue_draw ();
+        }
+    }
+
+    private int selected_border_width = 4;
+    private int unselected_border_width = 1;
+
+    public signal void clicked ();
+
+    public ThumbnailView ()
+    {
+        add_events (Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK);
+    }
+
+    private void get_page_size (out int width, out int height)
+    {
+        var w = get_allocated_width () - selected_border_width * 2;
+        var h = get_allocated_height () - selected_border_width * 2;
+
+        /* Fit page inside available space */
+        if (w * page.height > h * page.width)
+            w = h * page.width / page.height;
+        else
+            h = w * page.height / page.width;
+
+        width = w;
+        height = h;
+    }
+
+    public override void get_preferred_height_for_width (int width, out int minimum_height, out int 
natural_height)
+    {
+        if (page == null) {
+            minimum_height = natural_height = 1 + selected_border_width * 2;
+            return;
+        }
+        minimum_height = 1 + selected_border_width * 2;
+        natural_height = int.max (width - selected_border_width * 2, 0) * page.height / page.width + 
selected_border_width * 2;
+    }
+
+    public override Gtk.SizeRequestMode get_request_mode ()
+    {
+        return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH;
+    }
+
+    public override bool draw (Cairo.Context context)
+    {
+        int w, h;
+        get_page_size (out w, out h);
+
+        var x_offset = (get_allocated_width () - w - selected_border_width * 2) / 2;
+        var y_offset = (get_allocated_height () - h - selected_border_width * 2) / 2;
+
+        /* Draw page border */
+        context.set_source_rgba (0.0, 0.0, 0.0, 0.25);
+        var border_delta = selected_border_width - unselected_border_width;
+        if (selected)
+        {
+            context.arc (x_offset + selected_border_width, y_offset + selected_border_width,
+                         selected_border_width, -Math.PI, -Math.PI / 2);
+            context.arc (x_offset + w + selected_border_width, y_offset + selected_border_width,
+                         selected_border_width, -Math.PI / 2, 0);
+            context.arc (x_offset + w + selected_border_width, y_offset + h + selected_border_width,
+                         selected_border_width, 0, Math.PI / 2);
+            context.arc (x_offset + selected_border_width, y_offset + h + selected_border_width,
+                         selected_border_width, Math.PI / 2, Math.PI);
+            context.close_path ();
+        }
+        else
+            context.rectangle (x_offset + border_delta, y_offset + border_delta,
+                               w + unselected_border_width * 2,
+                               h + unselected_border_width * 2);
+        context.set_line_width (selected_border_width);
+        context.fill ();
+
+        /* Draw page data */
+        var image = page.get_image (); // FIXME: Cache and mipmap
+        context.translate (x_offset + selected_border_width, y_offset + selected_border_width);
+        context.scale ((double) w / image.width, (double) h / image.height);
+        Gdk.cairo_set_source_pixbuf (context, image, 0, 0);
+        context.paint ();
+
+        return true;
+    }
+
+    public override bool button_release_event (Gdk.EventButton event)
+    {
+        if (event.button == 1) {
+            clicked ();
+            return true;
+        }
+
+        return false;
+    }
+
+    private void page_changed_cb (Page p)
+    {
+        queue_draw ();
+    }
+}



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]