summaryrefslogtreecommitdiff
path: root/src/book-view.vala
diff options
context:
space:
mode:
Diffstat (limited to 'src/book-view.vala')
-rw-r--r--src/book-view.vala610
1 files changed, 610 insertions, 0 deletions
diff --git a/src/book-view.vala b/src/book-view.vala
new file mode 100644
index 0000000..7e337f1
--- /dev/null
+++ b/src/book-view.vala
@@ -0,0 +1,610 @@
+/*
+ * Copyright (C) 2009-2011 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.
+ */
+
+// FIXME: When scrolling, copy existing render sideways?
+// FIXME: Only render pages that change and only the part that changed
+
+public class BookView : Gtk.Box
+{
+ /* Book being rendered */
+ public Book book { get; private set; }
+ private HashTable<Page, PageView> page_data;
+
+ /* True if the view needs to be laid out again */
+ private bool need_layout;
+ private bool laying_out;
+ private bool show_selected_page;
+
+ /* Currently selected page */
+ private PageView? selected_page_view = null;
+ public Page? selected_page
+ {
+ get
+ {
+ if (selected_page_view != null)
+ return selected_page_view.page;
+ else
+ return null;
+ }
+ set
+ {
+ if (selected_page == value)
+ return;
+
+ if (value != null)
+ select_page_view (page_data.lookup (value));
+ else
+ select_page_view (null);
+ }
+ }
+
+ /* Widget being rendered to */
+ private Gtk.Widget drawing_area;
+
+ /* Horizontal scrollbar */
+ private Gtk.Scrollbar scroll;
+ private Gtk.Adjustment adjustment;
+
+ private Gdk.CursorType cursor;
+
+ public signal void page_selected (Page? page);
+ public signal void show_page (Page page);
+ public signal void show_menu ();
+
+ public int x_offset
+ {
+ get
+ {
+ return (int) adjustment.get_value ();
+ }
+ set
+ {
+ adjustment.value = value;
+ }
+ }
+
+ public BookView (Book book)
+ {
+ GLib.Object (orientation: Gtk.Orientation.VERTICAL);
+ this.book = book;
+
+ /* Load existing pages */
+ for (var i = 0; i < book.n_pages; i++)
+ {
+ Page page = book.get_page (i);
+ add_cb (book, page);
+ }
+
+ selected_page = book.get_page (0);
+
+ /* Watch for new pages */
+ book.page_added.connect (add_cb);
+ book.page_removed.connect (remove_cb);
+ book.reordered.connect (reorder_cb);
+ book.cleared.connect (clear_cb);
+
+ need_layout = true;
+ page_data = new HashTable<Page, PageView> (direct_hash, direct_equal);
+ cursor = Gdk.CursorType.ARROW;
+
+ drawing_area = new Gtk.DrawingArea ();
+ drawing_area.set_size_request (200, 100);
+ drawing_area.can_focus = true;
+ drawing_area.events = Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventMask.FOCUS_CHANGE_MASK | Gdk.EventMask.STRUCTURE_MASK | Gdk.EventMask.SCROLL_MASK;
+ pack_start (drawing_area, true, true, 0);
+
+ scroll = new Gtk.Scrollbar (Gtk.Orientation.HORIZONTAL, null);
+ adjustment = scroll.adjustment;
+ pack_start (scroll, false, true, 0);
+
+ drawing_area.configure_event.connect (configure_cb);
+ drawing_area.draw.connect (draw_cb);
+ drawing_area.motion_notify_event.connect (motion_cb);
+ drawing_area.key_press_event.connect (key_cb);
+ drawing_area.button_press_event.connect (button_cb);
+ drawing_area.button_release_event.connect (button_cb);
+ drawing_area.focus_in_event.connect_after (focus_cb);
+ drawing_area.focus_out_event.connect_after (focus_cb);
+ adjustment.value_changed.connect (scroll_cb);
+
+ drawing_area.visible = true;
+ }
+
+ ~BookView ()
+ {
+ book.page_added.disconnect (add_cb);
+ book.page_removed.disconnect (remove_cb);
+ book.reordered.disconnect (reorder_cb);
+ book.cleared.disconnect (clear_cb);
+ drawing_area.configure_event.disconnect (configure_cb);
+ drawing_area.draw.disconnect (draw_cb);
+ drawing_area.motion_notify_event.disconnect (motion_cb);
+ drawing_area.key_press_event.disconnect (key_cb);
+ drawing_area.button_press_event.disconnect (button_cb);
+ drawing_area.button_release_event.disconnect (button_cb);
+ drawing_area.focus_in_event.disconnect (focus_cb);
+ drawing_area.focus_out_event.disconnect (focus_cb);
+ adjustment.value_changed.disconnect (scroll_cb);
+ }
+
+ private PageView get_nth_page (int n)
+ {
+ Page page = book.get_page (n);
+ return page_data.lookup (page);
+ }
+
+ private PageView get_next_page (PageView page)
+ {
+ for (var i = 0; ; i++)
+ {
+ var p = book.get_page (i);
+ if (p == null)
+ break;
+ if (p == page.page)
+ {
+ p = book.get_page (i + 1);
+ if (p != null)
+ return page_data.lookup (p);
+ }
+ }
+
+ return page;
+ }
+
+ private PageView get_prev_page (PageView page)
+ {
+ var prev_page = page;
+ for (var i = 0; ; i++)
+ {
+ var p = book.get_page (i);
+ if (p == null)
+ break;
+ if (p == page.page)
+ return prev_page;
+ prev_page = page_data.lookup (p);
+ }
+
+ return page;
+ }
+
+ private void page_view_changed_cb (PageView page)
+ {
+ redraw ();
+ }
+
+ private void page_view_size_changed_cb (PageView page)
+ {
+ need_layout = true;
+ redraw ();
+ }
+
+ private void add_cb (Book book, Page page)
+ {
+ var page_view = new PageView (page);
+ page_view.changed.connect (page_view_changed_cb);
+ page_view.size_changed.connect (page_view_size_changed_cb);
+ page_data.insert (page, page_view);
+ need_layout = true;
+ redraw ();
+ }
+
+ private void set_selected_page_view (PageView? page)
+ {
+ /* Deselect existing page if changed */
+ if (selected_page_view != null && page != selected_page_view)
+ selected_page_view.selected = true;
+
+ selected_page_view = page;
+ if (selected_page_view == null)
+ return;
+
+ /* Select new page if widget has focus */
+ if (!drawing_area.has_focus)
+ selected_page_view.selected = false;
+ else
+ selected_page_view.selected = true;
+ }
+
+ private void show_page_view (PageView? page)
+ {
+ if (page == null || !scroll.get_visible ())
+ return;
+
+ Gtk.Allocation allocation;
+ drawing_area.get_allocation (out allocation);
+ var left_edge = page.x_offset;
+ var right_edge = page.x_offset + page.width;
+
+ if (left_edge - x_offset < 0)
+ x_offset = left_edge;
+ else if (right_edge - x_offset > allocation.width)
+ x_offset = right_edge - allocation.width;
+ }
+
+ private void select_page_view (PageView? page)
+ {
+ Page? p = null;
+
+ if (selected_page_view == page)
+ return;
+
+ set_selected_page_view (page);
+
+ if (need_layout)
+ show_selected_page = true;
+ else
+ show_page_view (page);
+
+ if (page != null)
+ p = page.page;
+ page_selected (p);
+ }
+
+ private void remove_cb (Book book, Page page)
+ {
+ PageView new_selection = selected_page_view;
+
+ /* Select previous page or next if removing the selected page */
+ if (page == selected_page)
+ {
+ new_selection = get_prev_page (selected_page_view);
+ if (new_selection == selected_page_view)
+ new_selection = get_next_page (selected_page_view);
+ selected_page_view = null;
+ }
+
+ var page_view = page_data.lookup (page);
+ page_view.changed.disconnect (page_view_changed_cb);
+ page_view.size_changed.disconnect (page_view_size_changed_cb);
+ page_data.remove (page);
+
+ select_page_view (new_selection);
+
+ need_layout = true;
+ redraw ();
+ }
+
+ private void reorder_cb (Book book)
+ {
+ need_layout = true;
+ redraw ();
+ }
+
+ private void clear_cb (Book book)
+ {
+ page_data.remove_all ();
+ selected_page_view = null;
+ page_selected (null);
+ need_layout = true;
+ redraw ();
+ }
+
+ private bool configure_cb (Gtk.Widget widget, Gdk.EventConfigure event)
+ {
+ need_layout = true;
+ return false;
+ }
+
+ private void layout_into (int width, int height, out int book_width, out int book_height)
+ {
+ /* Get maximum page resolution */
+ int max_dpi = 0;
+ for (var i = 0; i < book.n_pages; i++)
+ {
+ var page = book.get_page (i);
+ if (page.dpi > max_dpi)
+ max_dpi = page.dpi;
+ }
+
+ /* Get area required to fit all pages */
+ int max_width = 0, max_height = 0;
+ for (var i = 0; i < book.n_pages; i++)
+ {
+ var page = book.get_page (i);
+ var w = page.width;
+ var h = page.height;
+
+ /* Scale to the same DPI */
+ w = (int) ((double)w * max_dpi / page.dpi + 0.5);
+ h = (int) ((double)h * max_dpi / page.dpi + 0.5);
+
+ if (w > max_width)
+ max_width = w;
+ if (h > max_height)
+ max_height = h;
+ }
+
+ var aspect = (double)width / height;
+ var max_aspect = (double)max_width / max_height;
+
+ /* Get total dimensions of all pages */
+ int spacing = 12;
+ book_width = 0;
+ book_height = 0;
+ for (var i = 0; i < book.n_pages; i++)
+ {
+ var page = get_nth_page (i);
+ var p = page.page;
+
+ /* NOTE: Using double to avoid overflow for large images */
+ if (max_aspect > aspect)
+ {
+ /* Set width scaled on DPI and maximum width */
+ int w = (int) ((double)p.width * max_dpi * width / (p.dpi * max_width));
+ page.width = w;
+ }
+ else
+ {
+ /* Set height scaled on DPI and maximum height */
+ int h = (int) ((double)p.height * max_dpi * height / (p.dpi * max_height));
+ page.height = h;
+ }
+
+ var h = page.height;
+ if (h > book_height)
+ book_height = h;
+ book_width += page.width;
+ if (i != 0)
+ book_width += spacing;
+ }
+
+ int x_offset = 0;
+ for (var i = 0; i < book.n_pages; i++)
+ {
+ var page = get_nth_page (i);
+
+ /* Layout pages left to right */
+ page.x_offset = x_offset;
+ x_offset += page.width + spacing;
+
+ /* Centre page vertically */
+ page.y_offset = (height - page.height) / 2;
+ }
+ }
+
+ private void layout ()
+ {
+ if (!need_layout)
+ return;
+
+ laying_out = true;
+
+ Gtk.Allocation allocation;
+ drawing_area.get_allocation(out allocation);
+ Gtk.Allocation box_allocation;
+ get_allocation(out box_allocation);
+
+ /* If scroll is right aligned then keep that after layout */
+ bool right_aligned = true;
+ if (adjustment.get_value () < adjustment.get_upper () - adjustment.get_page_size ())
+ right_aligned = false;
+
+ /* Try and fit without scrollbar */
+ var width = (int) allocation.width;
+ var height = (int) (box_allocation.height - get_border_width () * 2);
+ int book_width, book_height;
+ layout_into (width, height, out book_width, out book_height);
+
+ /* Relayout with scrollbar */
+ if (book_width > allocation.width)
+ {
+ /* Re-layout leaving space for scrollbar */
+ height = allocation.height;
+ layout_into (width, height, out book_width, out book_height);
+
+ /* Set scrollbar limits */
+ adjustment.lower = 0;
+ adjustment.upper = book_width;
+ adjustment.page_size = allocation.width;
+
+ /* Keep right-aligned */
+ var max_offset = book_width - allocation.width;
+ if (right_aligned || x_offset > max_offset)
+ x_offset = max_offset;
+
+ scroll.visible = true;
+ }
+ else
+ {
+ scroll.visible = false;
+ var offset = (book_width - allocation.width) / 2;
+ adjustment.lower = offset;
+ adjustment.upper = offset;
+ adjustment.page_size = 0;
+ x_offset = offset;
+ }
+
+ if (show_selected_page)
+ show_page_view (selected_page_view);
+
+ need_layout = false;
+ show_selected_page = false;
+ laying_out = false;
+ }
+
+ private bool draw_cb (Gtk.Widget widget, Cairo.Context context)
+ {
+ if (book.n_pages == 0)
+ return false;
+
+ layout ();
+
+ double left, top, right, bottom;
+ context.clip_extents (out left, out top, out right, out bottom);
+
+ /* Render each page */
+ for (var i = 0; i < book.n_pages; i++)
+ {
+ var page = get_nth_page (i);
+ var left_edge = page.x_offset - x_offset;
+ var right_edge = page.x_offset + page.width - x_offset;
+
+ /* Page not visible, don't render */
+ if (right_edge < left || left_edge > right)
+ continue;
+
+ context.save ();
+ context.translate (-x_offset, 0);
+ page.render (context);
+ context.restore ();
+
+ if (page.selected)
+ drawing_area.get_style_context ().render_focus (context,
+ page.x_offset - x_offset,
+ page.y_offset,
+ page.width,
+ page.height);
+ }
+
+ return false;
+ }
+
+ private PageView? get_page_at (int x, int y, out int x_, out int y_)
+ {
+ x_ = y_ = 0;
+ for (var i = 0; i < book.n_pages; i++)
+ {
+ var page = get_nth_page (i);
+ var left = page.x_offset;
+ var right = left + page.width;
+ var top = page.y_offset;
+ var bottom = top + page.height;
+ if (x >= left && x <= right && y >= top && y <= bottom)
+ {
+ x_ = x - left;
+ y_ = y - top;
+ return page;
+ }
+ }
+
+ return null;
+ }
+
+ private bool button_cb (Gtk.Widget widget, Gdk.EventButton event)
+ {
+ layout ();
+
+ drawing_area.grab_focus ();
+
+ int x = 0, y = 0;
+ if (event.type == Gdk.EventType.BUTTON_PRESS)
+ select_page_view (get_page_at ((int) (event.x + x_offset), (int) event.y, out x, out y));
+
+ if (selected_page_view == null)
+ return false;
+
+ /* Modify page */
+ if (event.button == 1)
+ {
+ if (event.type == Gdk.EventType.BUTTON_PRESS)
+ selected_page_view.button_press (x, y);
+ else if (event.type == Gdk.EventType.BUTTON_RELEASE)
+ selected_page_view.button_release (x, y);
+ else if (event.type == Gdk.EventType.2BUTTON_PRESS)
+ show_page (selected_page);
+ }
+
+ /* Show pop-up menu on right click */
+ if (event.button == 3)
+ show_menu ();
+
+ return false;
+ }
+
+ private void set_cursor (Gdk.CursorType cursor)
+ {
+ Gdk.Cursor c;
+
+ if (this.cursor == cursor)
+ return;
+ this.cursor = cursor;
+
+ c = new Gdk.Cursor (cursor);
+ drawing_area.get_window ().set_cursor (c);
+ }
+
+ private bool motion_cb (Gtk.Widget widget, Gdk.EventMotion event)
+ {
+ Gdk.CursorType cursor = Gdk.CursorType.ARROW;
+
+ /* Dragging */
+ if (selected_page_view != null && (event.state & Gdk.ModifierType.BUTTON1_MASK) != 0)
+ {
+ var x = (int) (event.x + x_offset - selected_page_view.x_offset);
+ var y = (int) (event.y - selected_page_view.y_offset);
+ selected_page_view.motion (x, y);
+ cursor = selected_page_view.cursor;
+ }
+ else
+ {
+ int x, y;
+ var over_page = get_page_at ((int) (event.x + x_offset), (int) event.y, out x, out y);
+ if (over_page != null)
+ {
+ over_page.motion (x, y);
+ cursor = over_page.cursor;
+ }
+ }
+
+ set_cursor (cursor);
+
+ return false;
+ }
+
+ private bool key_cb (Gtk.Widget widget, Gdk.EventKey event)
+ {
+ switch (event.keyval)
+ {
+ case 0xff50: /* FIXME: GDK_Home */
+ selected_page = book.get_page (0);
+ return true;
+ case 0xff51: /* FIXME: GDK_Left */
+ select_page_view (get_prev_page (selected_page_view));
+ return true;
+ case 0xff53: /* FIXME: GDK_Right */
+ select_page_view (get_next_page (selected_page_view));
+ return true;
+ case 0xFF57: /* FIXME: GDK_End */
+ selected_page = book.get_page ((int) book.n_pages - 1);
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ private bool focus_cb (Gtk.Widget widget, Gdk.EventFocus event)
+ {
+ set_selected_page_view (selected_page_view);
+ return false;
+ }
+
+ private void scroll_cb (Gtk.Adjustment adjustment)
+ {
+ if (!laying_out)
+ redraw ();
+ }
+
+ public void redraw ()
+ {
+ drawing_area.queue_draw ();
+ }
+
+ public void select_next_page ()
+ {
+ select_page_view (get_next_page (selected_page_view));
+ }
+
+ public void select_prev_page ()
+ {
+ select_page_view (get_prev_page (selected_page_view));
+ }
+}