/* * Copyright (C) 2009-2015 Canonical Ltd. * Author: Robert Ancell * * 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_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 (Gdk.Event event); 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 (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; drawing_area.vexpand = true; add (drawing_area); scroll = new Gtk.Scrollbar (Gtk.Orientation.HORIZONTAL, null); adjustment = scroll.adjustment; add (scroll); 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; redraw(); 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) { var pages = new List (); for (var i = 0; i < book.n_pages; i++) pages.append (get_nth_page (i)); /* Get maximum page resolution */ int max_dpi = 0; foreach (var page in pages) { var p = page.page; if (p.dpi > max_dpi) max_dpi = p.dpi; } /* Get area required to fit all pages */ int max_width = 0, max_height = 0; foreach (var page in pages) { var p = page.page; var w = p.width; var h = p.height; /* Scale to the same DPI */ w = (int) ((double)w * max_dpi / p.dpi + 0.5); h = (int) ((double)h * max_dpi / p.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; foreach (var page in pages) { 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 + spacing; } if (pages != null) book_width -= spacing; int x_offset = 0; foreach (var page in pages) { /* 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) { layout (); double left, top, right, bottom; context.clip_extents (out left, out top, out right, out bottom); var pages = new List (); for (var i = 0; i < book.n_pages; i++) pages.append (get_nth_page (i)); var ruler_color = get_style_context ().get_color (get_state_flags ()); Gdk.RGBA ruler_color_selected = {}; ruler_color_selected.parse("#3584e4"); /* Gnome Blue 3 */ /* Render each page */ foreach (var page in pages) { 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, page == selected_page_view ? ruler_color_selected : ruler_color); 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 (event); return false; } private void set_cursor (Gdk.CursorType cursor) { Gdk.Cursor c; if (this.cursor == cursor) return; this.cursor = cursor; c = new Gdk.Cursor.for_display (get_display (), 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)); } }