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