diff options
Diffstat (limited to 'src/book-view.vala')
-rw-r--r-- | src/book-view.vala | 594 |
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; + } +} |