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