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