1/** 2 * Presentater window 3 * 4 * This file is part of pdfpc. 5 * 6 * Copyright (C) 2010-2011 Jakob Westhoff <jakob@westhoffswelt.de> 7 * Copyright 2012 David Vilar 8 * Copyright 2012, 2015 Robert Schroll 9 * Copyright 2015 Andreas Bilke 10 * 11 * This program is free software; you can redistribute it and/or modify 12 * it under the terms of the GNU General Public License as published by 13 * the Free Software Foundation; either version 3 of the License, or 14 * (at your option) any later version. 15 * 16 * This program is distributed in the hope that it will be useful, 17 * but WITHOUT ANY WARRANTY; without even the implied warranty of 18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 * GNU General Public License for more details. 20 * 21 * You should have received a copy of the GNU General Public License along 22 * with this program; if not, write to the Free Software Foundation, Inc., 23 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 24 */ 25 26namespace pdfpc.Window { 27 /** 28 * An overview of all the slides in the form of a table 29 */ 30 public class Overview: Gtk.ScrolledWindow { 31 32 /* 33 * The store of all the slides. 34 */ 35 protected Gtk.ListStore slides; 36 /* 37 * The view of the above. 38 */ 39 protected Gtk.IconView slides_view; 40 41 /** 42 * Metadata of the slides 43 */ 44 protected Metadata.Pdf metadata { 45 get { 46 return this.controller.metadata; 47 } 48 } 49 50 /** 51 * How many (user) slides we have. 52 */ 53 protected int n_slides = 0; 54 55 /** 56 * The target height and width of the scaled images, a bit smaller than 57 * the button dimensions to allow some margin 58 */ 59 protected int target_width; 60 protected int target_height; 61 62 /** 63 * The presentation controller 64 */ 65 protected PresentationController controller; 66 67 /** 68 * The maximal size of the slides_view. 69 */ 70 protected int max_width = -1; 71 protected int max_height = -1; 72 73 /** 74 * The currently selected slide. 75 */ 76 private int _current_slide = 0; 77 public int current_slide { 78 get { return _current_slide; } 79 set { var path = new Gtk.TreePath.from_indices(value); 80 this.slides_view.select_path(path); 81 // _current_slide set in on_selection_changed, below 82 this.slides_view.set_cursor(path, null, false); 83 } 84 } 85 86 /** 87 * The used cell renderer, for later usage 88 */ 89 private CellRendererHighlight cell_renderer; 90 91 /* 92 * When the section changes, we need to update the current slide number. 93 * Also, make sure we don't end up with no selection. 94 */ 95 public void on_selection_changed() { 96 var ltp = this.slides_view.get_selected_items(); 97 if (ltp != null) { 98 var tp = ltp.data; 99 if (tp.get_indices() != null) { // Seg fault if we save tp.get_indices locally 100 this._current_slide = tp.get_indices()[0]; 101 return; 102 } 103 } 104 // If there's no selection, reset the old one 105 this.current_slide = this._current_slide; 106 } 107 108 /** 109 * Constructor 110 */ 111 public Overview(PresentationController controller) { 112 this.controller = controller; 113 114 this.get_style_context().add_class("overviewWindow"); 115 116 this.slides = new Gtk.ListStore(1, typeof(int)); 117 118 this.slides_view = new Gtk.IconView.with_model(this.slides); 119 this.slides_view.selection_mode = Gtk.SelectionMode.SINGLE; 120 this.slides_view.halign = Gtk.Align.CENTER; 121 122 this.cell_renderer = new CellRendererHighlight(); 123 this.cell_renderer.metadata = metadata; 124 125 Gtk.StyleContext style_context = this.get_style_context(); 126 Pango.FontDescription font_description; 127 style_context.get(style_context.get_state(), "font", out font_description, null); 128 this.cell_renderer.font_description = font_description; 129 130 this.slides_view.pack_start(cell_renderer, true); 131 this.slides_view.add_attribute(cell_renderer, "slide_id", 0); 132 this.slides_view.set_item_padding(0); 133 this.add(this.slides_view); 134 135 this.slides_view.motion_notify_event.connect(this.on_mouse_move); 136 this.slides_view.button_release_event.connect(this.on_mouse_release); 137 this.slides_view.key_press_event.connect(this.on_key_press); 138 this.slides_view.selection_changed.connect(this.on_selection_changed); 139 this.key_press_event.connect((event) => this.slides_view.key_press_event(event)); 140 } 141 142 public void set_available_space(int width, int height) { 143 this.max_width = width; 144 this.max_height = height; 145 this.prepare_layout(); 146 } 147 148 /** 149 * Get keyboard focus. This requires that the window has focus. 150 */ 151 public void ensure_focus() { 152 Gtk.Window top = this.get_toplevel() as Gtk.Window; 153 if (top != null && !top.has_toplevel_focus) { 154 top.present(); 155 } 156 this.slides_view.grab_focus(); 157 } 158 159 /** 160 * Figure out the sizes for the icons, and create entries in slides 161 * for all the slides. 162 */ 163 protected void prepare_layout() { 164 if (!this.metadata.is_ready) { 165 return; 166 } 167 if (this.max_width == -1) { 168 return; 169 } 170 171 double aspect_ratio = this.metadata.get_page_width() / this.metadata.get_page_height(); 172 173 this.slides_view.set_margin(0); 174 175 var margin = this.slides_view.get_margin(); 176 var padding = this.slides_view.get_item_padding() + 1; // Additional mystery pixel 177 var row_spacing = this.slides_view.get_row_spacing(); 178 var col_spacing = this.slides_view.get_column_spacing(); 179 180 var eff_max_width = this.max_width - 2 * margin; 181 var eff_max_height = this.max_height - 2 * margin; 182 int cols = eff_max_width / (Options.min_overview_width + 2 * padding + col_spacing); 183 int widthx, widthy, min_width, rows; 184 int tc = 0; 185 186 // Search for the layout with the widest icons. We do this by considering 187 // layouts with different numbers of columns, and figuring the maximum 188 // width for the icon so that all the icons fit both horizontally and 189 // vertically. We start with the largest number of columns that fit the 190 // icons at the minimum allowed width, and we decrease the number of columns 191 // until we cannot fit the icons vertically at the minimal allowed size. 192 // Note that there may be NO solution, in which case target_width == 0. 193 this.target_width = 0; 194 while (cols > 0) { 195 widthx = eff_max_width / cols - 2*padding - 2*col_spacing; 196 rows = (int)Math.ceil((float)this.n_slides / cols); 197 widthy = (int)Math.floor((eff_max_height / rows - 2*padding - 2*row_spacing) 198 * aspect_ratio); // floor so that later round 199 // doesn't increase height 200 if (widthy < Options.min_overview_width) { 201 break; 202 } 203 204 min_width = widthx < widthy ? widthx : widthy; 205 if (min_width >= this.target_width) { // If two layouts give the same width 206 this.target_width = min_width; // (which happens when they're limited 207 tc = cols; // by height), prefer the one with fewer 208 } // columns for a more filled block. 209 cols -= 1; 210 } 211 if (this.target_width < Options.min_overview_width) { 212 this.target_width = Options.min_overview_width; 213 this.slides_view.columns = (eff_max_width - 20) // Guess for scrollbar width 214 / (Options.min_overview_width + 2 * padding + col_spacing); 215 } else { 216 this.slides_view.columns = tc; 217 } 218 this.set_policy(Gtk.PolicyType.ALWAYS, Gtk.PolicyType.ALWAYS); 219 this.target_height = (int)Math.round(this.target_width / aspect_ratio); 220 rows = (int)Math.ceil((float)this.n_slides / this.slides_view.columns); 221 int full_height = rows*(this.target_height + 2*padding + 2*row_spacing) + 2*margin; 222 if (full_height > this.max_height) { 223 full_height = this.max_height; 224 } 225 226 this.cell_renderer.slide_width = this.target_width; 227 this.cell_renderer.slide_height = this.target_height; 228 229 this.slides.clear(); 230 Gtk.TreeIter iter; 231 for (int i = 0; i < this.n_slides; i++) { 232 this.slides.append(out iter); 233 this.slides.set_value(iter, 0, i); 234 } 235 } 236 237 /** 238 * Set the number of slides. If it is different to what we know, it 239 * triggers a rebuilding of the widget. 240 */ 241 public void set_n_slides(int n) { 242 if (n != this.n_slides) { 243 var currently_selected = this.current_slide; 244 this.n_slides = n; 245 this.prepare_layout(); 246 if (currently_selected >= this.n_slides) { 247 currently_selected = this.n_slides - 1; 248 } 249 this.current_slide = currently_selected; 250 } 251 } 252 253 /** 254 * Remove the current slide from the overview, and set the total number 255 * of slides to the new value. Perpare to regenerate the structure the 256 * next time the overview is hidden. 257 */ 258 public void remove_current(int newn) { 259 this.n_slides = newn; 260 Gtk.TreeIter iter; 261 this.slides.get_iter_from_string(out iter, @"$(this.current_slide)"); 262#if VALA_0_36 263 // Updated bindings in Vala 0.36: "iter" param of ListStore.remove() marked as ref 264 this.slides.remove(ref iter); 265#else 266 this.slides.remove(iter); 267#endif 268 if (this.current_slide >= this.n_slides) { 269 this.current_slide = this.n_slides - 1; 270 } 271 } 272 273 /** 274 * We handle some "navigation" key presses ourselves. Others are left to 275 * the standard IconView controls, the rest are passed back to the 276 * PresentationController. 277 */ 278 public bool on_key_press(Gtk.Widget source, Gdk.EventKey key) { 279 bool handled = false; 280 switch (key.keyval) { 281 case Gdk.Key.Left: 282 case Gdk.Key.Page_Up: 283 if (this.current_slide > 0) { 284 this.current_slide -= 1; 285 } 286 handled = true; 287 break; 288 case Gdk.Key.Right: 289 case Gdk.Key.Page_Down: 290 if (this.current_slide < this.n_slides - 1) { 291 this.current_slide += 1; 292 } 293 handled = true; 294 break; 295 case Gdk.Key.Return: 296 bool gotoFirst = (key.state & Gdk.ModifierType.SHIFT_MASK) != 0; 297 this.controller.goto_user_page(this.current_slide, !gotoFirst); 298 handled = true; 299 break; 300 case Gdk.Key.Escape: 301 this.controller.controllables_hide_overview(); 302 handled = true; 303 break; 304 } 305 306 return handled; 307 } 308 309 /* 310 * Update the selection when the mouse moves over a new slides. 311 */ 312 public bool on_mouse_move(Gtk.Widget source, Gdk.EventMotion event) { 313 Gtk.TreePath path; 314 path = this.slides_view.get_path_at_pos((int)event.x, (int)event.y); 315 if (path != null && path.get_indices()[0] != this.current_slide) { 316 this.current_slide = path.get_indices()[0]; 317 } 318 return false; 319 } 320 321 /* 322 * Go to selected slide when the mouse button is released. On a simple 323 * click, the button_press event will have set the current slide. On 324 * a drag, the current slide will have been updated by the motion. 325 */ 326 public bool on_mouse_release(Gdk.EventButton event) { 327 if (event.button == 1) { 328 bool gotoFirst = (event.state & Gdk.ModifierType.SHIFT_MASK) != 0; 329 this.controller.goto_user_page(this.current_slide, !gotoFirst); 330 } 331 return false; 332 } 333 } 334 335 /* 336 * Render a surface that is slightly shaded, unless it is the selected one. 337 */ 338 class CellRendererHighlight : Gtk.CellRenderer { 339 protected int slide_id { get; set; } 340 341 public Pango.FontDescription font_description { get; set; } 342 public Metadata.Pdf metadata { get; set; } 343 public int slide_width { get; set; } 344 public int slide_height { get; set; } 345 346 public override void get_size(Gtk.Widget widget, Gdk.Rectangle? cell_area, 347 out int x_offset, out int y_offset, 348 out int width, out int height) { 349 x_offset = 0; 350 y_offset = 0; 351 width = this.slide_width; 352 height = this.slide_height; 353 } 354 355 public override void render(Cairo.Context cr, Gtk.Widget widget, 356 Gdk.Rectangle background_area, Gdk.Rectangle cell_area, 357 Gtk.CellRendererState flags) { 358 var renderer = this.metadata.renderer; 359 Cairo.ImageSurface slide_to_fill = null; 360 361 slide_to_fill = null; 362 try { 363 var real_slide_id = 364 metadata.user_slide_to_real_slide(this.slide_id, true); 365 slide_to_fill = renderer.render(real_slide_id, 366 false, slide_width, slide_height, 367 true, true); 368 } catch (Renderer.RenderError e) { 369 ; 370 } 371 // nothing to show 372 if (slide_to_fill == null) { 373 cr.set_source_rgba(0.5, 0.5, 0.5, 1); 374 cr.rectangle(cell_area.x, cell_area.y, cell_area.width, cell_area.height); 375 cr.fill(); 376 } else { 377 double scale_factor = (double)slide_width/slide_to_fill.get_width(); 378 cr.scale(scale_factor, scale_factor); 379 cr.set_source_surface(slide_to_fill, (double)cell_area.x/scale_factor, (double)cell_area.y/scale_factor); 380 cr.paint(); 381 cr.scale(1.0/scale_factor, 1.0/scale_factor); 382 } 383 384 if ((flags & Gtk.CellRendererState.SELECTED) == 0) { 385 cr.rectangle(cell_area.x, cell_area.y, cell_area.width, cell_area.height); 386 cr.set_source_rgba(0, 0, 0, 0.4); 387 cr.fill(); 388 } 389 390 // draw slide number 391 var layout = Pango.cairo_create_layout(cr); 392 layout.set_font_description(this.font_description); 393 layout.set_text(@"$(slide_id + 1)", -1); 394 layout.set_width(cell_area.width); 395 layout.set_alignment(Pango.Alignment.CENTER); 396 397 Pango.Rectangle logical_extent; 398 layout.get_pixel_extents(null, out logical_extent); 399 cr.move_to(cell_area.x + (cell_area.width / 2), cell_area.y + (cell_area.height / 2) - (logical_extent.height / 2)); 400 401 if ((flags & Gtk.CellRendererState.SELECTED) == 0) { 402 cr.set_source_rgba(0.7, 0.7, 0.7, 0.7); 403 } else { 404 cr.set_source_rgba(0.7, 0.7, 0.7, 0.2); 405 } 406 407 Pango.cairo_show_layout(cr, layout); 408 } 409 } 410} 411