1 /* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /*
3 * Copyright © 2021 Purism SPC
4 *
5 * This file is part of Epiphany.
6 *
7 * Epiphany is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * Epiphany is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with Epiphany. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21 #include "config.h"
22 #include "ephy-fullscreen-box.h"
23
24 #include <handy.h>
25
26 #define FULLSCREEN_HIDE_DELAY 300
27 #define SHOW_HEADERBAR_DISTANCE_PX 5
28
29 struct _EphyFullscreenBox {
30 GtkEventBox parent_instance;
31
32 HdyFlap *flap;
33 GtkEventController *controller;
34 GtkGesture *gesture;
35
36 gboolean fullscreen;
37 gboolean autohide;
38
39 guint timeout_id;
40
41 GtkWidget *last_focus;
42 gdouble last_y;
43 gboolean is_touch;
44 };
45
46 static void ephy_fullscreen_box_buildable_init (GtkBuildableIface *iface);
47
48 G_DEFINE_TYPE_WITH_CODE (EphyFullscreenBox, ephy_fullscreen_box, GTK_TYPE_EVENT_BOX,
49 G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE,
50 ephy_fullscreen_box_buildable_init))
51
52 enum {
53 PROP_0,
54 PROP_FULLSCREEN,
55 PROP_AUTOHIDE,
56 PROP_TITLEBAR,
57 PROP_REVEALED,
58 LAST_PROP
59 };
60
61 static GParamSpec *props[LAST_PROP];
62
63 static void
show_ui(EphyFullscreenBox * self)64 show_ui (EphyFullscreenBox *self)
65 {
66 g_clear_handle_id (&self->timeout_id, g_source_remove);
67
68 hdy_flap_set_reveal_flap (self->flap, TRUE);
69 }
70
71 static void
hide_ui(EphyFullscreenBox * self)72 hide_ui (EphyFullscreenBox *self)
73 {
74 g_clear_handle_id (&self->timeout_id, g_source_remove);
75
76 if (!self->fullscreen)
77 return;
78
79 hdy_flap_set_reveal_flap (self->flap, FALSE);
80 gtk_widget_grab_focus (GTK_WIDGET (self->flap));
81 }
82
83 static gboolean
hide_timeout_cb(EphyFullscreenBox * self)84 hide_timeout_cb (EphyFullscreenBox *self)
85 {
86 self->timeout_id = 0;
87
88 hide_ui (self);
89
90 return G_SOURCE_REMOVE;
91 }
92
93 static void
start_hide_timeout(EphyFullscreenBox * self)94 start_hide_timeout (EphyFullscreenBox *self)
95 {
96 if (!hdy_flap_get_reveal_flap (self->flap))
97 return;
98
99 if (self->timeout_id)
100 return;
101
102 self->timeout_id = g_timeout_add (FULLSCREEN_HIDE_DELAY,
103 (GSourceFunc)hide_timeout_cb,
104 self);
105 }
106
107 static gboolean
is_descendant_of(GtkWidget * widget,GtkWidget * target)108 is_descendant_of (GtkWidget *widget,
109 GtkWidget *target)
110 {
111 GtkWidget *parent;
112
113 if (!widget)
114 return FALSE;
115
116 if (widget == target)
117 return TRUE;
118
119 parent = widget;
120
121 while (parent && parent != target && !GTK_IS_POPOVER (parent))
122 parent = gtk_widget_get_parent (parent);
123
124 if (GTK_IS_POPOVER (parent))
125 return is_descendant_of (gtk_popover_get_relative_to (GTK_POPOVER (parent)), target);
126
127 return parent == target;
128 }
129
130 static double
get_titlebar_area_height(EphyFullscreenBox * self)131 get_titlebar_area_height (EphyFullscreenBox *self)
132 {
133 gdouble height;
134
135 height = gtk_widget_get_allocated_height (hdy_flap_get_flap (self->flap));
136 height *= hdy_flap_get_reveal_progress (self->flap);
137 height = MAX (height, SHOW_HEADERBAR_DISTANCE_PX);
138
139 return height;
140 }
141
142 static void
update(EphyFullscreenBox * self,gboolean hide_immediately)143 update (EphyFullscreenBox *self,
144 gboolean hide_immediately)
145 {
146 if (!self->autohide || !self->fullscreen)
147 return;
148
149 if (!self->is_touch &&
150 self->last_y <= get_titlebar_area_height (self)) {
151 show_ui (self);
152 return;
153 }
154
155 if (self->last_focus && is_descendant_of (self->last_focus,
156 hdy_flap_get_flap (self->flap)))
157 show_ui (self);
158 else if (hide_immediately)
159 hide_ui (self);
160 else
161 start_hide_timeout (self);
162 }
163
164 static void
motion_cb(EphyFullscreenBox * self,double x,double y)165 motion_cb (EphyFullscreenBox *self,
166 double x,
167 double y)
168 {
169 self->is_touch = FALSE;
170 self->last_y = y;
171
172 update (self, TRUE);
173 }
174
175 static void
enter_cb(EphyFullscreenBox * self,double x,double y)176 enter_cb (EphyFullscreenBox *self,
177 double x,
178 double y)
179 {
180 g_autoptr (GdkEvent) event = gtk_get_current_event ();
181
182 if (event->crossing.window != gtk_widget_get_window (GTK_WIDGET (self)) ||
183 event->crossing.detail == GDK_NOTIFY_INFERIOR)
184 return;
185
186 motion_cb (self, x, y);
187 }
188
189 static void
press_cb(EphyFullscreenBox * self,int n_press,double x,double y)190 press_cb (EphyFullscreenBox *self,
191 int n_press,
192 double x,
193 double y)
194 {
195 gtk_gesture_set_state (self->gesture, GTK_EVENT_SEQUENCE_DENIED);
196
197 self->is_touch = TRUE;
198
199 if (y > get_titlebar_area_height (self))
200 update (self, TRUE);
201 }
202
203 static void
set_focus_cb(EphyFullscreenBox * self,GtkWidget * widget)204 set_focus_cb (EphyFullscreenBox *self,
205 GtkWidget *widget)
206 {
207 self->last_focus = widget;
208
209 update (self, TRUE);
210 }
211
212 static void
notify_reveal_cb(EphyFullscreenBox * self)213 notify_reveal_cb (EphyFullscreenBox *self)
214 {
215 g_object_notify_by_pspec (G_OBJECT (self), props[PROP_REVEALED]);
216 }
217
218 static void
ephy_fullscreen_box_hierarchy_changed(GtkWidget * widget,GtkWidget * previous_toplevel)219 ephy_fullscreen_box_hierarchy_changed (GtkWidget *widget,
220 GtkWidget *previous_toplevel)
221 {
222 EphyFullscreenBox *self = EPHY_FULLSCREEN_BOX (widget);
223 GtkWidget *toplevel;
224
225 if (previous_toplevel && GTK_IS_WINDOW (previous_toplevel))
226 g_signal_handlers_disconnect_by_func (previous_toplevel, set_focus_cb, widget);
227
228 toplevel = gtk_widget_get_toplevel (widget);
229
230 if (toplevel && GTK_IS_WINDOW (toplevel)) {
231 g_signal_connect_object (toplevel, "set-focus",
232 G_CALLBACK (set_focus_cb), widget,
233 G_CONNECT_SWAPPED);
234
235 set_focus_cb (self, gtk_window_get_focus (GTK_WINDOW (toplevel)));
236 } else {
237 set_focus_cb (self, NULL);
238 }
239 }
240
241 static void
ephy_fullscreen_box_add(GtkContainer * container,GtkWidget * widget)242 ephy_fullscreen_box_add (GtkContainer *container,
243 GtkWidget *widget)
244 {
245 EphyFullscreenBox *self = EPHY_FULLSCREEN_BOX (container);
246
247 if (!self->flap)
248 GTK_CONTAINER_CLASS (ephy_fullscreen_box_parent_class)->add (container, widget);
249 else
250 gtk_container_add (GTK_CONTAINER (self->flap), widget);
251 }
252
253 static void
ephy_fullscreen_box_remove(GtkContainer * container,GtkWidget * widget)254 ephy_fullscreen_box_remove (GtkContainer *container,
255 GtkWidget *widget)
256 {
257 EphyFullscreenBox *self = EPHY_FULLSCREEN_BOX (container);
258
259 if (widget == GTK_WIDGET (self->flap)) {
260 GTK_CONTAINER_CLASS (ephy_fullscreen_box_parent_class)->remove (container, widget);
261 self->flap = NULL;
262 } else {
263 gtk_container_remove (GTK_CONTAINER (self->flap), widget);
264 }
265 }
266
267 static void
ephy_fullscreen_box_forall(GtkContainer * container,gboolean include_internals,GtkCallback callback,gpointer callback_data)268 ephy_fullscreen_box_forall (GtkContainer *container,
269 gboolean include_internals,
270 GtkCallback callback,
271 gpointer callback_data)
272 {
273 EphyFullscreenBox *self = EPHY_FULLSCREEN_BOX (container);
274
275 if (include_internals) {
276 GTK_CONTAINER_CLASS (ephy_fullscreen_box_parent_class)->forall (container,
277 include_internals,
278 callback,
279 callback_data);
280 } else {
281 gtk_container_foreach (GTK_CONTAINER (self->flap),
282 callback,
283 callback_data);
284 }
285 }
286
287 static void
ephy_fullscreen_box_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)288 ephy_fullscreen_box_get_property (GObject *object,
289 guint prop_id,
290 GValue *value,
291 GParamSpec *pspec)
292 {
293 EphyFullscreenBox *self = EPHY_FULLSCREEN_BOX (object);
294
295 switch (prop_id) {
296 case PROP_FULLSCREEN:
297 g_value_set_boolean (value, ephy_fullscreen_box_get_fullscreen (self));
298 break;
299
300 case PROP_AUTOHIDE:
301 g_value_set_boolean (value, ephy_fullscreen_box_get_autohide (self));
302 break;
303
304 case PROP_TITLEBAR:
305 g_value_set_object (value, ephy_fullscreen_box_get_titlebar (self));
306 break;
307
308 case PROP_REVEALED:
309 g_value_set_boolean (value, hdy_flap_get_reveal_flap (self->flap));
310 break;
311
312 default:
313 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
314 }
315 }
316
317 static void
ephy_fullscreen_box_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)318 ephy_fullscreen_box_set_property (GObject *object,
319 guint prop_id,
320 const GValue *value,
321 GParamSpec *pspec)
322 {
323 EphyFullscreenBox *self = EPHY_FULLSCREEN_BOX (object);
324
325 switch (prop_id) {
326 case PROP_FULLSCREEN:
327 ephy_fullscreen_box_set_fullscreen (self, g_value_get_boolean (value));
328 break;
329
330 case PROP_AUTOHIDE:
331 ephy_fullscreen_box_set_autohide (self, g_value_get_boolean (value));
332 break;
333
334 case PROP_TITLEBAR:
335 ephy_fullscreen_box_set_titlebar (self, g_value_get_object (value));
336 break;
337
338 default:
339 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
340 }
341 }
342
343 static void
ephy_fullscreen_box_dispose(GObject * object)344 ephy_fullscreen_box_dispose (GObject *object)
345 {
346 EphyFullscreenBox *self = EPHY_FULLSCREEN_BOX (object);
347
348 g_clear_object (&self->controller);
349 g_clear_object (&self->gesture);
350
351 G_OBJECT_CLASS (ephy_fullscreen_box_parent_class)->dispose (object);
352 }
353
354 static void
ephy_fullscreen_box_class_init(EphyFullscreenBoxClass * klass)355 ephy_fullscreen_box_class_init (EphyFullscreenBoxClass *klass)
356 {
357 GObjectClass *object_class = G_OBJECT_CLASS (klass);
358 GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
359 GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
360
361 object_class->get_property = ephy_fullscreen_box_get_property;
362 object_class->set_property = ephy_fullscreen_box_set_property;
363 object_class->dispose = ephy_fullscreen_box_dispose;
364
365 widget_class->hierarchy_changed = ephy_fullscreen_box_hierarchy_changed;
366
367 container_class->add = ephy_fullscreen_box_add;
368 container_class->remove = ephy_fullscreen_box_remove;
369 container_class->forall = ephy_fullscreen_box_forall;
370
371 props[PROP_FULLSCREEN] =
372 g_param_spec_boolean ("fullscreen",
373 "Fullscreen",
374 "Fullscreen",
375 FALSE,
376 G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
377
378 props[PROP_AUTOHIDE] =
379 g_param_spec_boolean ("autohide",
380 "Autohide",
381 "Autohide",
382 TRUE,
383 G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
384
385 props[PROP_TITLEBAR] =
386 g_param_spec_object ("titlebar",
387 "Titlebar",
388 "Titlebar",
389 GTK_TYPE_WIDGET,
390 G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
391
392 props[PROP_REVEALED] =
393 g_param_spec_boolean ("revealed",
394 "Revealed",
395 "Revealed",
396 TRUE,
397 G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
398
399 g_object_class_install_properties (object_class, LAST_PROP, props);
400
401 gtk_widget_class_set_css_name (widget_class, "fullscreenbox");
402 }
403
404 static void
ephy_fullscreen_box_init(EphyFullscreenBox * self)405 ephy_fullscreen_box_init (EphyFullscreenBox *self)
406 {
407 HdyFlap *flap;
408
409 self->autohide = TRUE;
410
411 gtk_widget_add_events (GTK_WIDGET (self), GDK_ALL_EVENTS_MASK);
412
413 flap = HDY_FLAP (hdy_flap_new ());
414 gtk_orientable_set_orientation (GTK_ORIENTABLE (flap), GTK_ORIENTATION_VERTICAL);
415 hdy_flap_set_flap_position (flap, GTK_PACK_START);
416 hdy_flap_set_fold_policy (flap, HDY_FLAP_FOLD_POLICY_NEVER);
417 hdy_flap_set_locked (flap, TRUE);
418 hdy_flap_set_modal (flap, FALSE);
419 hdy_flap_set_swipe_to_open (flap, FALSE);
420 hdy_flap_set_swipe_to_close (flap, FALSE);
421 hdy_flap_set_transition_type (flap, HDY_FLAP_TRANSITION_TYPE_OVER);
422 gtk_widget_show (GTK_WIDGET (flap));
423
424 g_signal_connect_object (flap, "notify::reveal-flap",
425 G_CALLBACK (notify_reveal_cb), self, G_CONNECT_SWAPPED);
426
427 gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (flap));
428 self->flap = flap;
429
430 self->controller = gtk_event_controller_motion_new (GTK_WIDGET (self));
431 gtk_event_controller_set_propagation_phase (self->controller, GTK_PHASE_CAPTURE);
432 g_signal_connect_object (self->controller, "enter",
433 G_CALLBACK (enter_cb), self, G_CONNECT_SWAPPED);
434 g_signal_connect_object (self->controller, "motion",
435 G_CALLBACK (motion_cb), self, G_CONNECT_SWAPPED);
436
437 self->gesture = gtk_gesture_multi_press_new (GTK_WIDGET (self));
438 gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (self->gesture),
439 GTK_PHASE_CAPTURE);
440 gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (self->gesture), TRUE);
441 g_signal_connect_object (self->gesture, "pressed",
442 G_CALLBACK (press_cb), self, G_CONNECT_SWAPPED);
443 }
444
445 static void
ephy_fullscreen_box_buildable_add_child(GtkBuildable * buildable,GtkBuilder * builder,GObject * child,const gchar * type)446 ephy_fullscreen_box_buildable_add_child (GtkBuildable *buildable,
447 GtkBuilder *builder,
448 GObject *child,
449 const gchar *type)
450 {
451 EphyFullscreenBox *self = EPHY_FULLSCREEN_BOX (buildable);
452
453 if (!self->flap) {
454 gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (child));
455 return;
456 }
457
458 if (!g_strcmp0 (type, "titlebar"))
459 ephy_fullscreen_box_set_titlebar (self, GTK_WIDGET (child));
460 else
461 gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (child));
462 }
463
464 static void
ephy_fullscreen_box_buildable_init(GtkBuildableIface * iface)465 ephy_fullscreen_box_buildable_init (GtkBuildableIface *iface)
466 {
467 iface->add_child = ephy_fullscreen_box_buildable_add_child;
468 }
469
470 EphyFullscreenBox *
ephy_fullscreen_box_new(void)471 ephy_fullscreen_box_new (void)
472 {
473 return g_object_new (EPHY_TYPE_FULLSCREEN_BOX, NULL);
474 }
475
476 gboolean
ephy_fullscreen_box_get_fullscreen(EphyFullscreenBox * self)477 ephy_fullscreen_box_get_fullscreen (EphyFullscreenBox *self)
478 {
479 g_return_val_if_fail (EPHY_IS_FULLSCREEN_BOX (self), FALSE);
480
481 return self->fullscreen;
482 }
483
484 void
ephy_fullscreen_box_set_fullscreen(EphyFullscreenBox * self,gboolean fullscreen)485 ephy_fullscreen_box_set_fullscreen (EphyFullscreenBox *self,
486 gboolean fullscreen)
487 {
488 g_return_if_fail (EPHY_IS_FULLSCREEN_BOX (self));
489
490 fullscreen = !!fullscreen;
491
492 if (fullscreen == self->fullscreen)
493 return;
494
495 self->fullscreen = fullscreen;
496
497 if (!self->autohide)
498 return;
499
500 if (fullscreen) {
501 hdy_flap_set_fold_policy (self->flap, HDY_FLAP_FOLD_POLICY_ALWAYS);
502 update (self, FALSE);
503 } else {
504 hdy_flap_set_fold_policy (self->flap, HDY_FLAP_FOLD_POLICY_NEVER);
505 show_ui (self);
506 }
507
508 g_object_notify_by_pspec (G_OBJECT (self), props[PROP_FULLSCREEN]);
509 }
510
511 gboolean
ephy_fullscreen_box_get_autohide(EphyFullscreenBox * self)512 ephy_fullscreen_box_get_autohide (EphyFullscreenBox *self)
513 {
514 g_return_val_if_fail (EPHY_IS_FULLSCREEN_BOX (self), FALSE);
515
516 return self->autohide;
517 }
518
519 void
ephy_fullscreen_box_set_autohide(EphyFullscreenBox * self,gboolean autohide)520 ephy_fullscreen_box_set_autohide (EphyFullscreenBox *self,
521 gboolean autohide)
522 {
523 g_return_if_fail (EPHY_IS_FULLSCREEN_BOX (self));
524
525 autohide = !!autohide;
526
527 if (autohide == self->autohide)
528 return;
529
530 self->autohide = autohide;
531
532 if (!self->fullscreen)
533 return;
534
535 if (autohide)
536 hide_ui (self);
537 else
538 show_ui (self);
539
540 g_object_notify_by_pspec (G_OBJECT (self), props[PROP_AUTOHIDE]);
541 }
542
543 GtkWidget *
ephy_fullscreen_box_get_titlebar(EphyFullscreenBox * self)544 ephy_fullscreen_box_get_titlebar (EphyFullscreenBox *self)
545 {
546 g_return_val_if_fail (EPHY_IS_FULLSCREEN_BOX (self), NULL);
547
548 return hdy_flap_get_flap (self->flap);
549 }
550
551 void
ephy_fullscreen_box_set_titlebar(EphyFullscreenBox * self,GtkWidget * titlebar)552 ephy_fullscreen_box_set_titlebar (EphyFullscreenBox *self,
553 GtkWidget *titlebar)
554 {
555 g_return_if_fail (EPHY_IS_FULLSCREEN_BOX (self));
556 g_return_if_fail (GTK_IS_WIDGET (titlebar) || titlebar == NULL);
557
558 if (hdy_flap_get_flap (self->flap) == titlebar)
559 return;
560
561 hdy_flap_set_flap (self->flap, titlebar);
562
563 g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLEBAR]);
564 }
565