1 /*
2 * Copyright (c) 2016-2021 gnome-mpv
3 *
4 * This file is part of Celluloid.
5 *
6 * Celluloid is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * Celluloid is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with Celluloid. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20 #include "celluloid-video-area.h"
21 #include "celluloid-control-box.h"
22 #include "celluloid-marshal.h"
23 #include "celluloid-common.h"
24 #include "celluloid-def.h"
25
26 #include <gtk/gtk.h>
27 #include <gdk/gdk.h>
28 #include <glib-object.h>
29 #include <math.h>
30
31 #ifdef GDK_WINDOWING_X11
32 #include <gdk/x11/gdkx.h>
33 #endif
34
35 struct _CelluloidVideoArea
36 {
37 GtkBox parent_instance;
38 GtkWidget *overlay;
39 GtkWidget *stack;
40 GtkWidget *draw_area;
41 GtkWidget *gl_area;
42 GtkWidget *control_box;
43 GtkWidget *header_bar;
44 GtkWidget *control_box_revealer;
45 GtkWidget *header_bar_revealer;
46 GtkEventController *area_motion_controller;
47 guint32 last_motion_time;
48 gdouble last_motion_x;
49 gdouble last_motion_y;
50 guint timeout_tag;
51 gboolean fullscreen;
52 gboolean fs_control_hover;
53 };
54
55 struct _CelluloidVideoAreaClass
56 {
57 GtkBoxClass parent_class;
58 };
59
60 static
61 void destroy_handler(GtkWidget *widget, gpointer data);
62
63 static
64 void set_cursor_visible(CelluloidVideoArea *area, gboolean visible);
65
66 static
67 gboolean timeout_handler(gpointer data);
68
69 static
70 void motion_handler( GtkEventControllerMotion *controller,
71 gdouble x,
72 gdouble y,
73 gpointer data );
74
75 static
76 void enter_handler( GtkEventControllerMotion *controller,
77 gdouble x,
78 gdouble y,
79 gpointer data );
80
81 static
82 void leave_handler(GtkEventControllerMotion *controller, gpointer data);
83
G_DEFINE_TYPE(CelluloidVideoArea,celluloid_video_area,GTK_TYPE_BOX)84 G_DEFINE_TYPE(CelluloidVideoArea, celluloid_video_area, GTK_TYPE_BOX)
85
86 static void
87 destroy_handler(GtkWidget *widget, gpointer data)
88 {
89 g_source_clear(&CELLULOID_VIDEO_AREA(widget)->timeout_tag);
90 }
91
92 static void
set_cursor_visible(CelluloidVideoArea * area,gboolean visible)93 set_cursor_visible(CelluloidVideoArea *area, gboolean visible)
94 {
95 GdkSurface *surface = gtk_widget_get_surface(area);
96 GdkCursor *cursor = NULL;
97
98 const gboolean hovering =
99 gtk_event_controller_motion_contains_pointer
100 (GTK_EVENT_CONTROLLER_MOTION(area->area_motion_controller));
101
102 if(visible || !hovering)
103 {
104 cursor = gdk_cursor_new_from_name("default", NULL);
105 }
106 else
107 {
108 cursor = gdk_cursor_new_from_name("none", NULL);
109 }
110
111 gdk_surface_set_cursor(surface, cursor);
112 g_object_unref(cursor);
113 }
114
115 static gboolean
timeout_handler(gpointer data)116 timeout_handler(gpointer data)
117 {
118 CelluloidVideoArea *area = data;
119 CelluloidControlBox *control_box = CELLULOID_CONTROL_BOX(area->control_box);
120 CelluloidHeaderBar *header_bar = CELLULOID_HEADER_BAR(area->header_bar);
121 gboolean open_button_active = FALSE;
122 gboolean menu_button_active = FALSE;
123
124 g_object_get( header_bar,
125 "open-button-active", &open_button_active,
126 "menu-button-active", &menu_button_active,
127 NULL );
128
129 if(control_box
130 && !area->fs_control_hover
131 && !celluloid_control_box_get_volume_popup_visible(control_box)
132 && !open_button_active
133 && !menu_button_active)
134 {
135 GSettings *settings;
136 gboolean always_autohide;
137
138 settings = g_settings_new(CONFIG_ROOT);
139 always_autohide = g_settings_get_boolean
140 (settings, "always-autohide-cursor");
141
142 gtk_revealer_set_reveal_child
143 (GTK_REVEALER(area->control_box_revealer), FALSE);
144 gtk_revealer_set_reveal_child
145 (GTK_REVEALER(area->header_bar_revealer), FALSE);
146
147 set_cursor_visible(area, !(always_autohide || area->fullscreen));
148 area->timeout_tag = 0;
149
150 g_object_unref(settings);
151 }
152 else if(!control_box)
153 {
154 area->timeout_tag = 0;
155 }
156
157 /* Try again later if timeout_tag has not been cleared. This means that
158 * either one of the popups is visible or the cursor is hovering over
159 * the control box, preventing it from being hidden.
160 */
161 return (area->timeout_tag != 0);
162 }
163
164 static void
motion_handler(GtkEventControllerMotion * controller,gdouble x,gdouble y,gpointer data)165 motion_handler( GtkEventControllerMotion *controller,
166 gdouble x,
167 gdouble y,
168 gpointer data )
169 {
170 GSettings *settings = g_settings_new(CONFIG_ROOT);
171 GtkWidget *widget = GTK_WIDGET(data);
172 CelluloidVideoArea *area = CELLULOID_VIDEO_AREA(data);
173 const gint height = gtk_widget_get_allocated_height(widget);
174
175 const gdouble unhide_speed =
176 g_settings_get_double(settings, "controls-unhide-cursor-speed");
177 const gdouble dead_zone =
178 g_settings_get_double(settings, "controls-dead-zone-size");
179
180 const guint32 time = gtk_event_controller_get_current_event_time
181 (GTK_EVENT_CONTROLLER(controller));
182 const gdouble dist = sqrt( pow(x - area->last_motion_x, 2) +
183 pow(y - area->last_motion_y, 2) );
184 const gdouble speed = dist / (time - area->last_motion_time);
185
186 area->last_motion_time = time;
187 area->last_motion_x = x;
188 area->last_motion_y = y;
189
190 if(speed >= unhide_speed)
191 {
192 GdkCursor *cursor = gdk_cursor_new_from_name("default", NULL);
193 GdkSurface *surface = gtk_widget_get_surface(widget);
194
195 gdk_surface_set_cursor(surface, cursor);
196
197 if( area->control_box &&
198 ABS((2 * y - height) / height) > dead_zone )
199 {
200 gtk_revealer_set_reveal_child
201 ( GTK_REVEALER(area->control_box_revealer),
202 TRUE );
203 gtk_revealer_set_reveal_child
204 ( GTK_REVEALER(area->header_bar_revealer),
205 area->fullscreen );
206 }
207
208 g_source_clear(&area->timeout_tag);
209 area->timeout_tag = g_timeout_add_seconds
210 ( FS_CONTROL_HIDE_DELAY,
211 timeout_handler,
212 area );
213 }
214
215 g_object_unref(settings);
216 }
217
218 static gboolean
render_handler(GtkGLArea * gl_area,GdkGLContext * context,gpointer data)219 render_handler(GtkGLArea *gl_area, GdkGLContext *context, gpointer data)
220 {
221 g_signal_emit_by_name(data, "render");
222
223 return TRUE;
224 }
225
226 static void
resize_handler(GtkWidget * widget,gint width,gint height,gpointer data)227 resize_handler(GtkWidget *widget, gint width, gint height, gpointer data)
228 {
229 g_signal_emit_by_name(data, "resize", width, height);
230 }
231
232 static void
popover_notify_handler(GObject * gobject,GParamSpec * pspec,gpointer data)233 popover_notify_handler(GObject *gobject, GParamSpec *pspec, gpointer data)
234 {
235 gboolean value = FALSE;
236
237 g_object_get(gobject, pspec->name, &value, NULL);
238
239 if(value)
240 {
241 set_cursor_visible(CELLULOID_VIDEO_AREA(data), TRUE);
242 }
243 }
244
245 static void
reveal_notify_handler(GObject * gobject,GParamSpec * pspec,gpointer data)246 reveal_notify_handler(GObject *gobject, GParamSpec *pspec, gpointer data)
247 {
248 /* Due to a GTK+ bug, the header bar isn't hidden completely if hidden
249 * by a GtkRevealer. Workaround this by manually hiding the revealer
250 * along with the header bar when the revealer completes its transition
251 * to hidden state.
252 */
253 CelluloidVideoArea *area = data;
254 gboolean reveal_child = TRUE;
255 gboolean child_revealed = TRUE;
256
257 g_object_get( gobject,
258 "reveal-child", &reveal_child,
259 "child-revealed", &child_revealed,
260 NULL );
261
262 gtk_widget_set_visible( GTK_WIDGET(area->header_bar_revealer),
263 area->fullscreen &&
264 (reveal_child || child_revealed) );
265 }
266
267 static void
enter_handler(GtkEventControllerMotion * controller,gdouble x,gdouble y,gpointer data)268 enter_handler( GtkEventControllerMotion *controller,
269 gdouble x,
270 gdouble y,
271 gpointer data )
272 {
273 CELLULOID_VIDEO_AREA(data)->fs_control_hover = TRUE;
274 }
275
276 static void
leave_handler(GtkEventControllerMotion * controller,gpointer data)277 leave_handler(GtkEventControllerMotion *controller, gpointer data)
278 {
279 CELLULOID_VIDEO_AREA(data)->fs_control_hover = FALSE;
280 }
281
282 static void
celluloid_video_area_class_init(CelluloidVideoAreaClass * klass)283 celluloid_video_area_class_init(CelluloidVideoAreaClass *klass)
284 {
285 GtkWidgetClass *wgt_class = GTK_WIDGET_CLASS(klass);
286
287 gtk_widget_class_set_css_name(wgt_class, "celluloid-video-area");
288
289 g_signal_new( "render",
290 G_TYPE_FROM_CLASS(klass),
291 G_SIGNAL_RUN_FIRST,
292 0,
293 NULL,
294 NULL,
295 g_cclosure_marshal_VOID__VOID,
296 G_TYPE_NONE,
297 0 );
298 g_signal_new( "resize",
299 G_TYPE_FROM_CLASS(klass),
300 G_SIGNAL_RUN_FIRST,
301 0,
302 NULL,
303 NULL,
304 g_cclosure_gen_marshal_VOID__INT_INT,
305 G_TYPE_NONE,
306 2,
307 G_TYPE_INT,
308 G_TYPE_INT );
309 }
310
311 static void
celluloid_video_area_init(CelluloidVideoArea * area)312 celluloid_video_area_init(CelluloidVideoArea *area)
313 {
314 area->overlay = gtk_overlay_new();
315 area->stack = gtk_stack_new();
316 area->draw_area = gtk_drawing_area_new();
317 area->gl_area = gtk_gl_area_new();
318 area->control_box = celluloid_control_box_new();
319 area->header_bar = celluloid_header_bar_new();
320 area->control_box_revealer = gtk_revealer_new();
321 area->header_bar_revealer = gtk_revealer_new();
322 area->area_motion_controller = gtk_event_controller_motion_new();
323 area->last_motion_time = 0;
324 area->last_motion_x = -1;
325 area->last_motion_y = -1;
326 area->timeout_tag = 0;
327 area->fullscreen = FALSE;
328 area->fs_control_hover = FALSE;
329
330 gtk_widget_set_valign(area->control_box_revealer, GTK_ALIGN_END);
331 gtk_revealer_set_transition_type
332 (GTK_REVEALER(area->control_box_revealer),
333 GTK_REVEALER_TRANSITION_TYPE_SLIDE_UP);
334 gtk_revealer_set_reveal_child
335 (GTK_REVEALER(area->control_box_revealer), FALSE);
336
337 gtk_widget_set_valign(area->header_bar_revealer, GTK_ALIGN_START);
338 gtk_revealer_set_reveal_child
339 (GTK_REVEALER(area->header_bar_revealer), FALSE);
340
341 gtk_widget_show(area->control_box);
342 gtk_widget_hide(area->control_box_revealer);
343
344 gtk_widget_show(area->header_bar);
345 gtk_widget_hide(area->header_bar_revealer);
346
347 GtkEventController *area_motion_controller =
348 area->area_motion_controller;
349 gtk_widget_add_controller
350 (GTK_WIDGET(area), area_motion_controller);
351
352 g_signal_connect( area_motion_controller,
353 "motion",
354 G_CALLBACK(motion_handler),
355 area );
356
357 GtkEventController *control_box_motion_controller =
358 gtk_event_controller_motion_new();
359 gtk_widget_add_controller
360 (GTK_WIDGET(area->control_box), control_box_motion_controller);
361
362 g_signal_connect( control_box_motion_controller,
363 "enter",
364 G_CALLBACK(enter_handler),
365 area );
366 g_signal_connect( control_box_motion_controller,
367 "leave",
368 G_CALLBACK(leave_handler),
369 area );
370
371 GtkEventController *header_bar_motion_controller =
372 gtk_event_controller_motion_new();
373 gtk_widget_add_controller
374 (GTK_WIDGET(area->header_bar), header_bar_motion_controller);
375
376 g_signal_connect( header_bar_motion_controller,
377 "enter",
378 G_CALLBACK(enter_handler),
379 area );
380 g_signal_connect( header_bar_motion_controller,
381 "leave",
382 G_CALLBACK(leave_handler),
383 area );
384
385 g_signal_connect( area,
386 "destroy",
387 G_CALLBACK(destroy_handler),
388 area );
389 g_signal_connect( area->gl_area,
390 "render",
391 G_CALLBACK(render_handler),
392 area );
393 g_signal_connect( area->gl_area,
394 "resize",
395 G_CALLBACK(resize_handler),
396 area );
397 g_signal_connect( area->draw_area,
398 "resize",
399 G_CALLBACK(resize_handler),
400 area );
401 g_signal_connect( area->control_box,
402 "notify::volume-popup-visible",
403 G_CALLBACK(popover_notify_handler),
404 area );
405 g_signal_connect( area->header_bar,
406 "notify::open-button-active",
407 G_CALLBACK(popover_notify_handler),
408 area );
409 g_signal_connect( area->header_bar,
410 "notify::menu-button-active",
411 G_CALLBACK(popover_notify_handler),
412 area );
413 g_signal_connect( area->header_bar_revealer,
414 "notify::reveal-child",
415 G_CALLBACK(reveal_notify_handler),
416 area );
417 g_signal_connect( area->header_bar_revealer,
418 "notify::child-revealed",
419 G_CALLBACK(reveal_notify_handler),
420 area );
421
422 gtk_stack_add_named(GTK_STACK(area->stack), area->draw_area, "draw");
423 gtk_stack_add_named(GTK_STACK(area->stack), area->gl_area, "gl");
424 gtk_stack_set_visible_child(GTK_STACK(area->stack), area->draw_area);
425
426 gtk_widget_set_hexpand(area->stack, TRUE);
427
428 gtk_revealer_set_child( GTK_REVEALER(area->header_bar_revealer),
429 area->header_bar );
430 gtk_revealer_set_child( GTK_REVEALER(area->control_box_revealer),
431 area->control_box );
432
433 gtk_overlay_add_overlay
434 (GTK_OVERLAY(area->overlay), area->control_box_revealer);
435 gtk_overlay_add_overlay
436 (GTK_OVERLAY(area->overlay), area->header_bar_revealer);
437
438 gtk_overlay_set_child(GTK_OVERLAY(area->overlay), area->stack);
439 gtk_box_append(GTK_BOX(area), area->overlay);
440 }
441
442 GtkWidget *
celluloid_video_area_new()443 celluloid_video_area_new()
444 {
445 return GTK_WIDGET(g_object_new(celluloid_video_area_get_type(), NULL));
446 }
447
448 void
celluloid_video_area_update_track_list(CelluloidVideoArea * area,const GPtrArray * track_list)449 celluloid_video_area_update_track_list( CelluloidVideoArea *area,
450 const GPtrArray *track_list )
451 {
452 celluloid_header_bar_update_track_list
453 (CELLULOID_HEADER_BAR(area->header_bar), track_list);
454 }
455
456 void
celluloid_video_area_update_disc_list(CelluloidVideoArea * area,const GPtrArray * disc_list)457 celluloid_video_area_update_disc_list( CelluloidVideoArea *area,
458 const GPtrArray *disc_list )
459 {
460 celluloid_header_bar_update_disc_list
461 (CELLULOID_HEADER_BAR(area->header_bar), disc_list);
462 }
463
464 void
celluloid_video_area_set_fullscreen_state(CelluloidVideoArea * area,gboolean fullscreen)465 celluloid_video_area_set_fullscreen_state( CelluloidVideoArea *area,
466 gboolean fullscreen )
467 {
468 if(area->fullscreen != fullscreen)
469 {
470 area->fullscreen = fullscreen;
471 area->fs_control_hover = FALSE;
472
473 gtk_widget_hide(area->header_bar_revealer);
474 set_cursor_visible(area, !fullscreen);
475
476 gtk_revealer_set_reveal_child
477 (GTK_REVEALER(area->control_box_revealer), FALSE);
478 gtk_revealer_set_reveal_child
479 (GTK_REVEALER(area->header_bar_revealer), FALSE);
480
481 celluloid_header_bar_set_fullscreen_state
482 (CELLULOID_HEADER_BAR(area->header_bar), fullscreen);
483
484 if(area->control_box)
485 {
486 celluloid_control_box_set_fullscreen_state
487 (CELLULOID_CONTROL_BOX(area->control_box), fullscreen);
488 }
489 }
490 }
491
492 void
celluloid_video_area_set_control_box_visible(CelluloidVideoArea * area,gboolean visible)493 celluloid_video_area_set_control_box_visible( CelluloidVideoArea *area,
494 gboolean visible )
495 {
496 gtk_widget_set_visible(area->control_box_revealer, visible);
497 }
498
499 gboolean
celluloid_video_area_get_control_box_visible(CelluloidVideoArea * area)500 celluloid_video_area_get_control_box_visible(CelluloidVideoArea *area)
501 {
502 return gtk_widget_get_visible(area->control_box_revealer);
503 }
504
505 void
celluloid_video_area_set_use_opengl(CelluloidVideoArea * area,gboolean use_opengl)506 celluloid_video_area_set_use_opengl( CelluloidVideoArea *area,
507 gboolean use_opengl )
508 {
509 gtk_stack_set_visible_child
510 ( GTK_STACK(area->stack),
511 use_opengl?area->gl_area:area->draw_area );
512 }
513
514 void
celluloid_video_area_queue_render(CelluloidVideoArea * area)515 celluloid_video_area_queue_render(CelluloidVideoArea *area)
516 {
517 gtk_gl_area_queue_render(GTK_GL_AREA(area->gl_area));
518 }
519
520 GtkDrawingArea *
celluloid_video_area_get_draw_area(CelluloidVideoArea * area)521 celluloid_video_area_get_draw_area(CelluloidVideoArea *area)
522 {
523 return GTK_DRAWING_AREA(area->draw_area);
524 }
525
526 GtkGLArea *
celluloid_video_area_get_gl_area(CelluloidVideoArea * area)527 celluloid_video_area_get_gl_area(CelluloidVideoArea *area)
528 {
529 return GTK_GL_AREA(area->gl_area);
530 }
531
532 CelluloidHeaderBar *
celluloid_video_area_get_header_bar(CelluloidVideoArea * area)533 celluloid_video_area_get_header_bar(CelluloidVideoArea *area)
534 {
535 return CELLULOID_HEADER_BAR(area->header_bar);
536 }
537
538 CelluloidControlBox *
celluloid_video_area_get_control_box(CelluloidVideoArea * area)539 celluloid_video_area_get_control_box(CelluloidVideoArea *area)
540 {
541 return CELLULOID_CONTROL_BOX(area->control_box);
542 }
543
544 gint64
celluloid_video_area_get_xid(CelluloidVideoArea * area)545 celluloid_video_area_get_xid(CelluloidVideoArea *area)
546 {
547 #ifdef GDK_WINDOWING_X11
548 if(GDK_IS_X11_DISPLAY(gdk_display_get_default()))
549 {
550 GtkWidget *parent = gtk_widget_get_parent(GTK_WIDGET(area));
551 GdkSurface *surface = NULL;
552
553 if(parent && !gtk_widget_get_realized(area->draw_area))
554 {
555 gtk_widget_realize(area->draw_area);
556 }
557
558 surface = gtk_widget_get_surface(area);
559
560 if(!surface)
561 {
562 g_critical("Failed to get XID of video area");
563 }
564
565 return surface?(gint64)gdk_x11_surface_get_xid(surface):-1;
566 }
567 #endif
568
569 return -1;
570 }
571