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.vala594
1 files changed, 594 insertions, 0 deletions
diff --git a/src/book-view.vala b/src/book-view.vala
new file mode 100644
index 0000000..9dfd361
--- /dev/null
+++ b/src/book-view.vala
@@ -0,0 +1,594 @@
+/*
+ * 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.VBox
+{
+ /* Book being rendered */
+ private Book book;
+ 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 = null;
+
+ /* Widget being rendered to */
+ private Gtk.Widget drawing_area;
+
+ /* Horizontal scrollbar */
+ private Gtk.HScrollbar 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 BookView (Book book)
+ {
+ this.book = book;
+
+ /* Load existing pages */
+ for (var i = 0; i < book.get_n_pages (); i++)
+ {
+ Page page = book.get_page (i);
+ add_cb (book, page);
+ }
+
+ select_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.set_can_focus (true);
+ drawing_area.set_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.HScrollbar (null);
+ adjustment = scroll.get_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.show ();
+ }
+
+ 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.get_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.get_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 (PageView? page)
+ {
+ /* Deselect existing page if changed */
+ if (selected_page != null && page != selected_page)
+ selected_page.set_selected (false);
+
+ selected_page = page;
+ if (selected_page == null)
+ return;
+
+ /* Select new page if widget has focus */
+ if (!drawing_area.has_focus)
+ selected_page.set_selected (false);
+ else
+ selected_page.set_selected (true);
+ }
+
+ private void set_x_offset (int offset)
+ {
+ adjustment.set_value (offset);
+ }
+
+ private int get_x_offset ()
+ {
+ return (int) adjustment.get_value ();
+ }
+
+ 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.get_x_offset ();
+ var right_edge = page.get_x_offset () + page.get_width ();
+
+ if (left_edge - get_x_offset () < 0)
+ set_x_offset (left_edge);
+ else if (right_edge - get_x_offset () > allocation.width)
+ set_x_offset (right_edge - allocation.width);
+ }
+
+ private void select_page_view (PageView? page)
+ {
+ Page? p = null;
+
+ if (selected_page == page)
+ return;
+
+ set_selected_page (page);
+
+ if (need_layout)
+ show_selected_page = true;
+ else
+ show_page_view (page);
+
+ if (page != null)
+ p = page.get_page ();
+ page_selected (p);
+ }
+
+ private void remove_cb (Book book, Page page)
+ {
+ PageView new_selection = selected_page;
+
+ /* Select previous page or next if removing the selected page */
+ if (page == get_selected ())
+ {
+ new_selection = get_prev_page (selected_page);
+ if (new_selection == selected_page)
+ new_selection = get_next_page (selected_page);
+ selected_page = null;
+ }
+
+ 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 = null;
+ page_selected (null);
+ need_layout = true;
+ redraw ();
+ }
+
+ public Book get_book ()
+ {
+ return book;
+ }
+
+ 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.get_n_pages (); i++)
+ {
+ var page = book.get_page (i);
+ if (page.get_dpi () > max_dpi)
+ max_dpi = page.get_dpi ();
+ }
+
+ /* Get area required to fit all pages */
+ int max_width = 0, max_height = 0;
+ for (var i = 0; i < book.get_n_pages (); i++)
+ {
+ var page = book.get_page (i);
+ var w = page.get_width ();
+ var h = page.get_height ();
+
+ /* Scale to the same DPI */
+ w = (int) ((double)w * max_dpi / page.get_dpi () + 0.5);
+ h = (int) ((double)h * max_dpi / page.get_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.get_n_pages (); i++)
+ {
+ var page = get_nth_page (i);
+ var p = page.get_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.get_width () * max_dpi * width / (p.get_dpi () * max_width));
+ page.set_width (w);
+ }
+ else
+ {
+ /* Set height scaled on DPI and maximum height */
+ int h = (int) ((double)p.get_height () * max_dpi * height / (p.get_dpi () * max_height));
+ page.set_height (h);
+ }
+
+ var h = page.get_height ();
+ if (h > book_height)
+ book_height = h;
+ book_width += page.get_width ();
+ if (i != 0)
+ book_width += spacing;
+ }
+
+ int x_offset = 0;
+ for (var i = 0; i < book.get_n_pages (); i++)
+ {
+ var page = get_nth_page (i);
+
+ /* Layout pages left to right */
+ page.set_x_offset (x_offset);
+ x_offset += page.get_width () + spacing;
+
+ /* Centre page vertically */
+ page.set_y_offset ((height - page.get_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.set_lower (0);
+ adjustment.set_upper (book_width);
+ adjustment.set_page_size (allocation.width);
+
+ /* Keep right-aligned */
+ var max_offset = book_width - allocation.width;
+ if (right_aligned || get_x_offset () > max_offset)
+ set_x_offset(max_offset);
+
+ scroll.show ();
+ }
+ else
+ {
+ scroll.hide ();
+ var offset = (book_width - allocation.width) / 2;
+ adjustment.set_lower (offset);
+ adjustment.set_upper (offset);
+ adjustment.set_page_size (0);
+ set_x_offset (offset);
+ }
+
+ if (show_selected_page)
+ show_page_view (selected_page);
+
+ need_layout = false;
+ show_selected_page = false;
+ laying_out = false;
+ }
+
+ private bool draw_cb (Gtk.Widget widget, Cairo.Context context)
+ {
+ if (book.get_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.get_n_pages (); i++)
+ {
+ var page = get_nth_page (i);
+ var left_edge = page.get_x_offset () - get_x_offset ();
+ var right_edge = page.get_x_offset () + page.get_width () - get_x_offset ();
+
+ /* Page not visible, don't render */
+ if (right_edge < left || left_edge > right)
+ continue;
+
+ context.save ();
+ context.translate (-get_x_offset (), 0);
+ page.render (context);
+ context.restore ();
+
+ if (page.get_selected ())
+ Gtk.paint_focus (drawing_area.get_style (),
+ context,
+ Gtk.StateType.SELECTED,
+ null,
+ null,
+ page.get_x_offset () - get_x_offset (),
+ page.get_y_offset (),
+ page.get_width (),
+ page.get_height ());
+ }
+
+ return false;
+ }
+
+ private PageView? get_page_at (int x, int y, out int x_, out int y_)
+ {
+ for (var i = 0; i < book.get_n_pages (); i++)
+ {
+ var page = get_nth_page (i);
+ var left = page.get_x_offset ();
+ var right = left + page.get_width ();
+ var top = page.get_y_offset ();
+ var bottom = top + page.get_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 + get_x_offset ()), (int) event.y, out x, out y));
+
+ if (selected_page == null)
+ return false;
+
+ /* Modify page */
+ if (event.button == 1)
+ {
+ if (event.type == Gdk.EventType.BUTTON_PRESS)
+ selected_page.button_press (x, y);
+ else if (event.type == Gdk.EventType.BUTTON_RELEASE)
+ selected_page.button_release (x, y);
+ else if (event.type == Gdk.EventType.2BUTTON_PRESS)
+ show_page (get_selected ());
+ }
+
+ /* 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 != null && (event.state & Gdk.ModifierType.BUTTON1_MASK) != 0)
+ {
+ var x = (int) (event.x + get_x_offset () - selected_page.get_x_offset ());
+ var y = (int) (event.y - selected_page.get_y_offset ());
+ selected_page.motion (x, y);
+ cursor = selected_page.get_cursor ();
+ }
+ else
+ {
+ int x, y;
+ var over_page = get_page_at ((int) (event.x + get_x_offset ()), (int) event.y, out x, out y);
+ if (over_page != null)
+ {
+ over_page.motion (x, y);
+ cursor = over_page.get_cursor ();
+ }
+ }
+
+ set_cursor (cursor);
+
+ return false;
+ }
+
+ private bool key_cb (Gtk.Widget widget, Gdk.EventKey event)
+ {
+ switch (event.keyval)
+ {
+ case 0xff50: /* FIXME: GDK_Home */
+ select_page (book.get_page (0));
+ return true;
+ case 0xff51: /* FIXME: GDK_Left */
+ select_page_view (get_prev_page (selected_page));
+ return true;
+ case 0xff53: /* FIXME: GDK_Right */
+ select_page_view (get_next_page (selected_page));
+ return true;
+ case 0xFF57: /* FIXME: GDK_End */
+ select_page (book.get_page ((int) book.get_n_pages () - 1));
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ private bool focus_cb (Gtk.Widget widget, Gdk.EventFocus event)
+ {
+ set_selected_page (selected_page);
+ return false;
+ }
+
+ private void scroll_cb (Gtk.Adjustment adjustment)
+ {
+ if (!laying_out)
+ redraw ();
+ }
+
+ public void redraw ()
+ {
+ drawing_area.queue_draw ();
+ }
+
+ public void select_page (Page? page)
+ {
+ if (get_selected () == page)
+ return;
+
+ if (page != null)
+ select_page_view (page_data.lookup (page));
+ else
+ select_page_view (null);
+ }
+
+ public void select_next_page ()
+ {
+ select_page_view (get_next_page (selected_page));
+ }
+
+ public void select_prev_page ()
+ {
+ select_page_view (get_prev_page (selected_page));
+ }
+
+ public Page? get_selected ()
+ {
+ if (selected_page != null)
+ return selected_page.get_page ();
+ else
+ return null;
+ }
+}