1/** 2 * Fullscreen Window 3 * 4 * This file is part of pdfpc. 5 * 6 * Copyright (C) 2010-2011 Jakob Westhoff <jakob@westhoffswelt.de> 7 * Copyright 2011, 2012 David Vilar 8 * Copyright 2012, 2015 Robert Schroll 9 * Copyright 2014,2016 Andy Barry 10 * Copyright 2015,2017 Andreas Bilke 11 * 12 * This program is free software; you can redistribute it and/or modify 13 * it under the terms of the GNU General Public License as published by 14 * the Free Software Foundation; either version 3 of the License, or 15 * (at your option) any later version. 16 * 17 * This program is distributed in the hope that it will be useful, 18 * but WITHOUT ANY WARRANTY; without even the implied warranty of 19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 * GNU General Public License for more details. 21 * 22 * You should have received a copy of the GNU General Public License along 23 * with this program; if not, write to the Free Software Foundation, Inc., 24 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 25 */ 26 27namespace pdfpc.Window { 28 /** 29 * Window extension implementing all the needed functionality, to be 30 * displayed fullscreen. 31 * 32 * Methods to specify the monitor to be displayed on in a multi-head setup 33 * are provided as well. 34 */ 35 public class Fullscreen : Gtk.Window { 36 /** 37 * The registered PresentationController 38 */ 39 public PresentationController controller { 40 get; protected set; 41 } 42 43 /** 44 * Metadata of the slides 45 */ 46 protected Metadata.Pdf metadata { 47 get { 48 return this.controller.metadata; 49 } 50 } 51 52 /** 53 * Whether the instance is presenter 54 */ 55 public bool is_presenter { 56 get; protected set; 57 } 58 59 /** 60 * The geometry of this window 61 */ 62 protected int window_w; 63 protected int window_h; 64 65 /** 66 * Currently selected windowed (!=fullscreen) mode 67 */ 68 protected bool windowed; 69 70 /** 71 * Timer id which monitors mouse motion to hide the cursor after 5 72 * seconds of inactivity 73 */ 74 protected uint hide_cursor_timeout = 0; 75 76 /** 77 * Overlay layout. Holds all drawing layers (like the pdf, 78 * the pointer mode etc) 79 */ 80 protected Gtk.Overlay overlay_layout; 81 82 /** 83 * Drawing area for pointer mode 84 */ 85 public Gtk.DrawingArea pointer_drawing_surface { get; protected set; } 86 87 /** 88 * Drawing area for pen mode 89 */ 90 public Gtk.DrawingArea pen_drawing_surface { get; protected set; } 91 92 /** 93 * Video area for playback. All videos are added to this surface. 94 */ 95 public View.Video video_surface { get; protected set; } 96 97 /** 98 * The GDK scale factor. Used for better slide rendering 99 */ 100 public int gdk_scale { 101 get; protected set; 102 } 103 104 /** 105 * The screen we want this window to be shown 106 */ 107 protected Gdk.Screen screen_to_use; 108 109 /** 110 * The monitor number we want to show the window 111 */ 112 protected int monitor_num_to_use; 113 /** 114 * ... and the actual monitor object 115 */ 116 public Gdk.Monitor monitor { 117 get; protected set; 118 } 119 120 protected virtual void resize_gui() {} 121 122 public Fullscreen(PresentationController controller, bool is_presenter, 123 int monitor_num, bool windowed, int width = -1, int height = -1) { 124 this.controller = controller; 125 this.is_presenter = is_presenter; 126 this.windowed = windowed; 127 128 this.title = "pdfpc - %s (%s)".printf( 129 is_presenter ? "presenter" : "presentation", 130 metadata.get_title()); 131 132 this.destroy.connect((source) => controller.quit()); 133 134 var display = Gdk.Display.get_default(); 135 if (monitor_num >= 0) { 136 // Start in the given monitor 137 this.monitor = display.get_monitor(monitor_num); 138 this.monitor_num_to_use = monitor_num; 139 140 this.screen_to_use = display.get_default_screen(); 141 } else { 142 // Start in the monitor the cursor is in 143 var device = display.get_default_seat().get_pointer(); 144 int pointerx, pointery; 145 device.get_position(out this.screen_to_use, 146 out pointerx, out pointery); 147 148 this.monitor = display.get_monitor_at_point(pointerx, pointery); 149 // Shouldn't be used, just a safety precaution 150 this.monitor_num_to_use = 0; 151 } 152 153 this.gdk_scale = this.monitor.get_scale_factor(); 154 155 this.overlay_layout = new Gtk.Overlay(); 156 157 this.pointer_drawing_surface = new Gtk.DrawingArea(); 158 this.pen_drawing_surface = new Gtk.DrawingArea(); 159 this.video_surface = new View.Video(); 160 161 this.overlay_layout.add_overlay(this.video_surface); 162 this.overlay_layout.add_overlay(this.pen_drawing_surface); 163 this.overlay_layout.add_overlay(this.pointer_drawing_surface); 164 165 this.video_surface.realize.connect(() => { 166 this.set_widget_event_pass_through(this.video_surface, true); 167 }); 168 this.pen_drawing_surface.realize.connect(() => { 169 this.enable_pen(false); 170 this.pen_drawing_surface.get_window().set_pass_through(true); 171 this.set_widget_event_pass_through(this.pen_drawing_surface, 172 true); 173 }); 174 this.pointer_drawing_surface.realize.connect(() => { 175 this.enable_pointer(false); 176 this.pointer_drawing_surface.get_window().set_pass_through(true); 177 this.set_widget_event_pass_through(this.pointer_drawing_surface, 178 true); 179 }); 180 181 // By default, we go fullscreen 182 var monitor_geometry = this.monitor.get_geometry(); 183 this.window_w = monitor_geometry.width; 184 this.window_h = monitor_geometry.height; 185 if (Pdfpc.is_Wayland_backend() && Options.wayland_workaround) { 186 // See issue 214. Wayland is doing some double scaling therefore 187 // we are lying about the actual screen size 188 this.window_w /= this.gdk_scale; 189 this.window_h /= this.gdk_scale; 190 } 191 192 if (!this.windowed) { 193 if (Options.move_on_mapped) { 194 // Some WM's ignore move requests made prior to 195 // mapping the window 196 this.map_event.connect(() => { 197 this.do_fullscreen(); 198 return true; 199 }); 200 } else { 201 this.do_fullscreen(); 202 } 203 } else { 204 if (width > 0 && height > 0) { 205 this.window_w = width; 206 this.window_h = height; 207 } else { 208 this.window_w /= 2; 209 this.window_h /= 2; 210 } 211 } 212 213 this.set_default_size(this.window_w, this.window_h); 214 215 this.add_events(Gdk.EventMask.POINTER_MOTION_MASK); 216 this.motion_notify_event.connect(this.on_mouse_move); 217 218 // Start the 5 seconds timeout after which the mouse cursor is 219 // hidden 220 this.restart_hide_cursor_timer(); 221 222 // Watch for window geometry changes; keep the local copy updated 223 this.configure_event.connect((ev) => { 224 if (ev.width != this.window_w || 225 ev.height != this.window_h) { 226 this.window_w = ev.width; 227 this.window_h = ev.height; 228 229 // Resize any GUI elements if needed 230 this.resize_gui(); 231 } 232 return false; 233 }); 234 235 this.pointer_drawing_surface.draw.connect(this.draw_pointer); 236 this.pen_drawing_surface.draw.connect(this.draw_pen); 237 238 this.key_press_event.connect(this.controller.key_press); 239 this.button_press_event.connect(this.controller.button_press); 240 this.scroll_event.connect(this.controller.scroll); 241 } 242 243 protected void do_fullscreen() { 244 // This should not happen, just in case... 245 if (this.monitor == null) { 246 return; 247 } 248 // Wayland has no concept of global coordinates, so move() does not 249 // work there. The window is "somewhere", but we do not care, 250 // since the next call should fix it. For X11 and KWin/Plasma this 251 // does the right thing. 252 Gdk.Rectangle monitor_geometry = this.monitor.get_geometry(); 253 this.move(monitor_geometry.x, monitor_geometry.y); 254 255 // Specially for Wayland; just fullscreen() would do otherwise... 256 this.fullscreen_on_monitor(this.screen_to_use, 257 this.monitor_num_to_use); 258 } 259 260 public void toggle_windowed() { 261 this.windowed = !this.windowed; 262 if (!this.windowed) { 263 var window = this.get_window(); 264 if (window != null) { 265 this.do_fullscreen(); 266 } 267 } else { 268 this.unfullscreen(); 269 } 270 } 271 272 public void connect_monitor(Gdk.Monitor? monitor) { 273 // This will likely become beefier in the future 274 this.monitor = monitor; 275 } 276 277 public bool is_monitor_connected() { 278 return this.monitor != null ? true:false; 279 } 280 281 protected bool draw_pointer(Cairo.Context context) { 282 Gtk.Allocation a; 283 this.pointer_drawing_surface.get_allocation(out a); 284 PresentationController c = this.controller; 285 286 // Draw the highlighted area, but ignore very short drags 287 // made unintentionally by mouse clicks 288 if (!c.current_pointer.is_spotlight && 289 c.highlight.width > 0.01 && c.highlight.height > 0.01) { 290 context.rectangle(0, 0, a.width, a.height); 291 context.new_sub_path(); 292 context.rectangle((int)(c.highlight.x*a.width), 293 (int)(c.highlight.y*a.height), 294 (int)(c.highlight.width*a.width), 295 (int)(c.highlight.height*a.height)); 296 297 context.set_fill_rule(Cairo.FillRule.EVEN_ODD); 298 context.set_source_rgba(0,0,0,0.5); 299 context.fill_preserve(); 300 301 context.new_path(); 302 } 303 // Draw the pointer when not dragging 304 if (c.drag_x == -1 && 305 (!c.pointer_hidden || c.current_pointer.is_spotlight)) { 306 int x = (int)(a.width*c.pointer_x); 307 int y = (int)(a.height*c.pointer_y); 308 int r = (int)(a.height*0.001*c.current_pointer.size); 309 310 Gdk.RGBA rgba = c.current_pointer.get_rgba(); 311 context.set_source_rgba(rgba.red, 312 rgba.green, 313 rgba.blue, 314 rgba.alpha); 315 if (c.current_pointer.is_spotlight) { 316 context.rectangle(0, 0, a.width, a.height); 317 context.new_sub_path(); 318 context.set_fill_rule(Cairo.FillRule.EVEN_ODD); 319 } 320 context.arc(x, y, r, 0, 2*Math.PI); 321 context.fill(); 322 } 323 324 return true; 325 } 326 327 public void enable_pointer(bool onoff) { 328 if (onoff) { 329 this.pointer_drawing_surface.show(); 330 } else { 331 this.pointer_drawing_surface.hide(); 332 } 333 } 334 335 protected bool draw_pen(Cairo.Context context) { 336 Gtk.Allocation a; 337 this.pen_drawing_surface.get_allocation(out a); 338 PresentationController c = this.controller; 339 340 if (c.pen_drawing != null) { 341 Cairo.Surface? drawing_surface = c.pen_drawing.render_to_surface(); 342 int x = (int)(a.width*c.pen_last_x); 343 int y = (int)(a.height*c.pen_last_y); 344 int base_width = c.pen_drawing.width; 345 int base_height = c.pen_drawing.height; 346 Cairo.Matrix old_xform = context.get_matrix(); 347 context.scale( 348 (double) a.width / base_width, 349 (double) a.height / base_height 350 ); 351 context.set_source_surface(drawing_surface, 0, 0); 352 context.paint(); 353 context.set_matrix(old_xform); 354 if (this.is_presenter && c.in_drawing_mode()) { 355 double width_adjustment = (double) a.width / base_width; 356 context.set_operator(Cairo.Operator.OVER); 357 context.set_line_width(2.0); 358 context.set_source_rgba( 359 c.current_pen_drawing_tool.red, 360 c.current_pen_drawing_tool.green, 361 c.current_pen_drawing_tool.blue, 362 1.0 363 ); 364 double arc_radius = c.current_pen_drawing_tool.width * width_adjustment / 2.0; 365 if (arc_radius < 1.0) { 366 arc_radius = 1.0; 367 } 368 context.arc(x, y, arc_radius, 0, 2*Math.PI); 369 context.stroke(); 370 } 371 } 372 373 return true; 374 } 375 376 public void enable_pen(bool onoff) { 377 if (onoff) { 378 this.pen_drawing_surface.show(); 379 } else { 380 this.pen_drawing_surface.hide(); 381 } 382 } 383 384 /** 385 * Called every time the mouse cursor is moved 386 */ 387 public bool on_mouse_move(Gtk.Widget source, Gdk.EventMotion event) { 388 // Restore the mouse cursor to its default value 389 this.get_window().set_cursor(null); 390 391 this.restart_hide_cursor_timer(); 392 393 return false; 394 } 395 396 /** 397 * Restart the 5 seconds timeout before hiding the mouse cursor 398 */ 399 protected void restart_hide_cursor_timer(){ 400 if (this.hide_cursor_timeout != 0) { 401 Source.remove(this.hide_cursor_timeout); 402 } 403 404 this.hide_cursor_timeout = Timeout.add_seconds(5, 405 this.on_hide_cursor_timeout); 406 } 407 408 /** 409 * Timeout method called if the mouse pointer has not been moved for 5 410 * seconds 411 */ 412 protected bool on_hide_cursor_timeout() { 413 this.hide_cursor_timeout = 0; 414 415 // Window might be null in case it has not been mapped 416 if (this.get_window() != null) { 417 var cursor = 418 new Gdk.Cursor.for_display(Gdk.Display.get_default(), 419 Gdk.CursorType.BLANK_CURSOR); 420 this.get_window().set_cursor(cursor); 421 422 // After the timeout disabled the cursor do not run it again 423 return false; 424 } else { 425 // The window was not available. Possibly it was not mapped 426 // yet. We simply try it again if the mouse isn't moved for 427 // another five seconds. 428 return true; 429 } 430 } 431 432 /** 433 * Set the widget passthrough. 434 * 435 * If set to true, the widget will not receive events and they will be 436 * forwarded to the underlying widgets within the Gtk.Overlay 437 */ 438 protected void set_widget_event_pass_through(Gtk.Widget w, 439 bool pass_through) { 440 this.overlay_layout.set_overlay_pass_through(w, pass_through); 441 } 442 } 443} 444