1 /*
2  * Copyright (C) 2013 Red Hat, Inc.
3  * Copyright (C) 2019 Purism SPC
4  *
5  * Author: Alexander Larsson <alexl@redhat.com>
6  * Author: Adrien Plazas <adrien.plazas@puri.sm>
7  *
8  * SPDX-License-Identifier: LGPL-2.1+
9  */
10 
11 /*
12  * Forked from the GTK+ 3.24.2 GtkStack widget initially written by Alexander
13  * Larsson, and heavily modified for libadwaita by Adrien Plazas on behalf of
14  * Purism SPC 2019.
15  */
16 
17 #include "config.h"
18 
19 #include "adw-squeezer.h"
20 
21 #include "gtkprogresstrackerprivate.h"
22 #include "adw-animation-private.h"
23 
24 /**
25  * SECTION:adwsqueezer
26  * @short_description: A best fit container.
27  * @Title: AdwSqueezer
28  *
29  * The AdwSqueezer widget is a container which only shows the first of its
30  * children that fits in the available size. It is convenient to offer different
31  * widgets to represent the same data with different levels of detail, making
32  * the widget seem to squeeze itself to fit in the available space.
33  *
34  * Transitions between children can be animated as fades. This can be controlled
35  * with adw_squeezer_set_transition_type().
36  *
37  * # CSS nodes
38  *
39  * #AdwSqueezer has a single CSS node with name squeezer.
40  *
41  * Since: 1.0
42  */
43 
44 /**
45  * AdwSqueezerTransitionType:
46  * @ADW_SQUEEZER_TRANSITION_TYPE_NONE: No transition
47  * @ADW_SQUEEZER_TRANSITION_TYPE_CROSSFADE: A cross-fade
48  *
49  * These enumeration values describe the possible transitions between children
50  * in a #AdwSqueezer widget.
51  *
52  * Since: 1.0
53  */
54 
55 struct _AdwSqueezerPage {
56   GObject parent_instance;
57 
58   GtkWidget *widget;
59   GtkWidget *last_focus;
60   gboolean enabled;
61 };
62 
63 G_DEFINE_TYPE (AdwSqueezerPage, adw_squeezer_page, G_TYPE_OBJECT)
64 
65 enum {
66   PAGE_PROP_0,
67   PAGE_PROP_CHILD,
68   PAGE_PROP_ENABLED,
69   LAST_PAGE_PROP
70 };
71 
72 static GParamSpec *page_props[LAST_PAGE_PROP];
73 
74 struct _AdwSqueezer
75 {
76   GtkWidget parent_instance;
77 
78   GList *children;
79 
80   AdwSqueezerPage *visible_child;
81 
82   gboolean homogeneous;
83 
84   AdwSqueezerTransitionType transition_type;
85   guint transition_duration;
86 
87   AdwSqueezerPage *last_visible_child;
88   guint tick_id;
89   GtkProgressTracker tracker;
90   gboolean first_frame_skipped;
91 
92   int last_visible_widget_width;
93   int last_visible_widget_height;
94 
95   AdwSqueezerTransitionType active_transition_type;
96 
97   gboolean interpolate_size;
98 
99   float xalign;
100   float yalign;
101 
102   GtkOrientation orientation;
103 
104   GtkSelectionModel *pages;
105 };
106 
107 enum  {
108   PROP_0,
109   PROP_HOMOGENEOUS,
110   PROP_VISIBLE_CHILD,
111   PROP_TRANSITION_DURATION,
112   PROP_TRANSITION_TYPE,
113   PROP_TRANSITION_RUNNING,
114   PROP_INTERPOLATE_SIZE,
115   PROP_XALIGN,
116   PROP_YALIGN,
117   PROP_PAGES,
118 
119   /* Overridden properties */
120   PROP_ORIENTATION,
121 
122   LAST_PROP = PROP_PAGES + 1,
123 };
124 
125 static GParamSpec *props[LAST_PROP];
126 
127 static void adw_squeezer_buildable_init (GtkBuildableIface *iface);
128 
129 G_DEFINE_TYPE_WITH_CODE (AdwSqueezer, adw_squeezer, GTK_TYPE_WIDGET,
130                          G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)
131                          G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, adw_squeezer_buildable_init))
132 
133 static GtkBuildableIface *parent_buildable_iface;
134 
135 static void
adw_squeezer_page_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)136 adw_squeezer_page_get_property (GObject    *object,
137                                 guint       property_id,
138                                 GValue     *value,
139                                 GParamSpec *pspec)
140 {
141   AdwSqueezerPage *self = ADW_SQUEEZER_PAGE (object);
142 
143   switch (property_id) {
144   case PAGE_PROP_CHILD:
145     g_value_set_object (value, adw_squeezer_page_get_child (self));
146     break;
147   case PAGE_PROP_ENABLED:
148     g_value_set_boolean (value, adw_squeezer_page_get_enabled (self));
149     break;
150   default:
151     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
152     break;
153   }
154 }
155 
156 static void
adw_squeezer_page_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)157 adw_squeezer_page_set_property (GObject      *object,
158                                 guint         property_id,
159                                 const GValue *value,
160                                 GParamSpec   *pspec)
161 {
162   AdwSqueezerPage *self = ADW_SQUEEZER_PAGE (object);
163 
164   switch (property_id) {
165   case PAGE_PROP_CHILD:
166     g_set_object (&self->widget, g_value_get_object (value));
167     break;
168   case PAGE_PROP_ENABLED:
169     adw_squeezer_page_set_enabled (self, g_value_get_boolean (value));
170     break;
171   default:
172     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
173     break;
174   }
175 }
176 
177 static void
adw_squeezer_page_finalize(GObject * object)178 adw_squeezer_page_finalize (GObject *object)
179 {
180   AdwSqueezerPage *self = ADW_SQUEEZER_PAGE (object);
181 
182   g_clear_object (&self->widget);
183 
184   if (self->last_focus)
185     g_object_remove_weak_pointer (G_OBJECT (self->last_focus),
186                                   (gpointer *) &self->last_focus);
187 
188   G_OBJECT_CLASS (adw_squeezer_page_parent_class)->finalize (object);
189 }
190 
191 static void
adw_squeezer_page_class_init(AdwSqueezerPageClass * klass)192 adw_squeezer_page_class_init (AdwSqueezerPageClass *klass)
193 {
194   GObjectClass *object_class = G_OBJECT_CLASS (klass);
195 
196   object_class->get_property = adw_squeezer_page_get_property;
197   object_class->set_property = adw_squeezer_page_set_property;
198   object_class->finalize = adw_squeezer_page_finalize;
199 
200   page_props[PAGE_PROP_CHILD] =
201     g_param_spec_object ("child",
202                          "Child",
203                          "The child of the page",
204                          GTK_TYPE_WIDGET,
205                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
206 
207   page_props[PAGE_PROP_ENABLED] =
208     g_param_spec_boolean ("enabled",
209                           "Enabled",
210                           "Whether the child can be picked or should be ignored when looking for the child fitting the available size best",
211                           TRUE,
212                           G_PARAM_READWRITE);
213 
214   g_object_class_install_properties (object_class, LAST_PAGE_PROP, page_props);
215 }
216 
217 static void
adw_squeezer_page_init(AdwSqueezerPage * self)218 adw_squeezer_page_init (AdwSqueezerPage *self)
219 {
220   self->enabled = TRUE;
221 }
222 
223 #define ADW_TYPE_SQUEEZER_PAGES (adw_squeezer_pages_get_type ())
224 
225 G_DECLARE_FINAL_TYPE (AdwSqueezerPages, adw_squeezer_pages, ADW, SQUEEZER_PAGES, GObject)
226 
227 struct _AdwSqueezerPages
228 {
229   GObject parent_instance;
230   AdwSqueezer *squeezer;
231 };
232 
233 static GType
adw_squeezer_pages_get_item_type(GListModel * model)234 adw_squeezer_pages_get_item_type (GListModel *model)
235 {
236   return ADW_TYPE_SQUEEZER_PAGE;
237 }
238 
239 static guint
adw_squeezer_pages_get_n_items(GListModel * model)240 adw_squeezer_pages_get_n_items (GListModel *model)
241 {
242   AdwSqueezerPages *self = ADW_SQUEEZER_PAGES (model);
243 
244   return g_list_length (self->squeezer->children);
245 }
246 
247 static gpointer
adw_squeezer_pages_get_item(GListModel * model,guint position)248 adw_squeezer_pages_get_item (GListModel *model,
249                              guint       position)
250 {
251   AdwSqueezerPages *self = ADW_SQUEEZER_PAGES (model);
252   AdwSqueezerPage *page;
253 
254   page = g_list_nth_data (self->squeezer->children, position);
255 
256   if (!page)
257     return NULL;
258 
259   return g_object_ref (page);
260 }
261 
262 static void
adw_squeezer_pages_list_model_init(GListModelInterface * iface)263 adw_squeezer_pages_list_model_init (GListModelInterface *iface)
264 {
265   iface->get_item_type = adw_squeezer_pages_get_item_type;
266   iface->get_n_items = adw_squeezer_pages_get_n_items;
267   iface->get_item = adw_squeezer_pages_get_item;
268 }
269 
270 static gboolean
adw_squeezer_pages_is_selected(GtkSelectionModel * model,guint position)271 adw_squeezer_pages_is_selected (GtkSelectionModel *model,
272                                 guint              position)
273 {
274   AdwSqueezerPages *self = ADW_SQUEEZER_PAGES (model);
275   AdwSqueezerPage *page;
276 
277   page = g_list_nth_data (self->squeezer->children, position);
278 
279   return page && page == self->squeezer->visible_child;
280 }
281 
282 static void
adw_squeezer_pages_selection_model_init(GtkSelectionModelInterface * iface)283 adw_squeezer_pages_selection_model_init (GtkSelectionModelInterface *iface)
284 {
285   iface->is_selected = adw_squeezer_pages_is_selected;
286 }
287 
G_DEFINE_TYPE_WITH_CODE(AdwSqueezerPages,adw_squeezer_pages,G_TYPE_OBJECT,G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL,adw_squeezer_pages_list_model_init)G_IMPLEMENT_INTERFACE (GTK_TYPE_SELECTION_MODEL,adw_squeezer_pages_selection_model_init))288 G_DEFINE_TYPE_WITH_CODE (AdwSqueezerPages, adw_squeezer_pages, G_TYPE_OBJECT,
289                          G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, adw_squeezer_pages_list_model_init)
290                          G_IMPLEMENT_INTERFACE (GTK_TYPE_SELECTION_MODEL, adw_squeezer_pages_selection_model_init))
291 
292 static void
293 adw_squeezer_pages_init (AdwSqueezerPages *pages)
294 {
295 }
296 
297 static void
adw_squeezer_pages_class_init(AdwSqueezerPagesClass * klass)298 adw_squeezer_pages_class_init (AdwSqueezerPagesClass *klass)
299 {
300 }
301 
302 static AdwSqueezerPages *
adw_squeezer_pages_new(AdwSqueezer * squeezer)303 adw_squeezer_pages_new (AdwSqueezer *squeezer)
304 {
305   AdwSqueezerPages *pages;
306 
307   pages = g_object_new (ADW_TYPE_SQUEEZER_PAGES, NULL);
308   pages->squeezer = squeezer;
309 
310   return pages;
311 }
312 
313 static GtkOrientation
get_orientation(AdwSqueezer * self)314 get_orientation (AdwSqueezer *self)
315 {
316   return self->orientation;
317 }
318 
319 static void
set_orientation(AdwSqueezer * self,GtkOrientation orientation)320 set_orientation (AdwSqueezer    *self,
321                  GtkOrientation  orientation)
322 {
323   if (self->orientation == orientation)
324     return;
325 
326   self->orientation = orientation;
327   gtk_widget_queue_resize (GTK_WIDGET (self));
328   g_object_notify (G_OBJECT (self), "orientation");
329 }
330 
331 static AdwSqueezerPage *
find_page_for_widget(AdwSqueezer * self,GtkWidget * child)332 find_page_for_widget (AdwSqueezer *self,
333                       GtkWidget   *child)
334 {
335   AdwSqueezerPage *page;
336   GList *l;
337 
338   for (l = self->children; l != NULL; l = l->next) {
339     page = l->data;
340     if (page->widget == child)
341       return page;
342   }
343 
344   return NULL;
345 }
346 
347 static void
adw_squeezer_progress_updated(AdwSqueezer * self)348 adw_squeezer_progress_updated (AdwSqueezer *self)
349 {
350   if (!self->homogeneous)
351     gtk_widget_queue_resize (GTK_WIDGET (self));
352   else
353     gtk_widget_queue_draw (GTK_WIDGET (self));
354 
355   if (gtk_progress_tracker_get_state (&self->tracker) == GTK_PROGRESS_STATE_AFTER &&
356       self->last_visible_child != NULL) {
357     gtk_widget_set_child_visible (self->last_visible_child->widget, FALSE);
358     self->last_visible_child = NULL;
359   }
360 }
361 
362 static gboolean
adw_squeezer_transition_cb(GtkWidget * widget,GdkFrameClock * frame_clock,gpointer user_data)363 adw_squeezer_transition_cb (GtkWidget     *widget,
364                             GdkFrameClock *frame_clock,
365                             gpointer       user_data)
366 {
367   AdwSqueezer *self = ADW_SQUEEZER (widget);
368 
369   if (self->first_frame_skipped) {
370     gtk_progress_tracker_advance_frame (&self->tracker,
371                                         gdk_frame_clock_get_frame_time (frame_clock));
372   } else {
373     self->first_frame_skipped = TRUE;
374   }
375 
376   /* Finish the animation early if the widget isn't mapped anymore. */
377   if (!gtk_widget_get_mapped (widget))
378     gtk_progress_tracker_finish (&self->tracker);
379 
380   adw_squeezer_progress_updated (ADW_SQUEEZER (widget));
381 
382   if (gtk_progress_tracker_get_state (&self->tracker) == GTK_PROGRESS_STATE_AFTER) {
383     self->tick_id = 0;
384     g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_RUNNING]);
385 
386     return FALSE;
387   }
388 
389   return TRUE;
390 }
391 
392 static void
adw_squeezer_schedule_ticks(AdwSqueezer * self)393 adw_squeezer_schedule_ticks (AdwSqueezer *self)
394 {
395   if (self->tick_id == 0) {
396     self->tick_id =
397       gtk_widget_add_tick_callback (GTK_WIDGET (self), adw_squeezer_transition_cb, self, NULL);
398     g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_RUNNING]);
399   }
400 }
401 
402 static void
adw_squeezer_unschedule_ticks(AdwSqueezer * self)403 adw_squeezer_unschedule_ticks (AdwSqueezer *self)
404 {
405   if (self->tick_id != 0) {
406     gtk_widget_remove_tick_callback (GTK_WIDGET (self), self->tick_id);
407     self->tick_id = 0;
408     g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_RUNNING]);
409   }
410 }
411 
412 static void
adw_squeezer_start_transition(AdwSqueezer * self,AdwSqueezerTransitionType transition_type,guint transition_duration)413 adw_squeezer_start_transition (AdwSqueezer               *self,
414                                AdwSqueezerTransitionType  transition_type,
415                                guint                      transition_duration)
416 {
417   GtkWidget *widget = GTK_WIDGET (self);
418 
419   if (gtk_widget_get_mapped (widget) &&
420       adw_get_enable_animations (widget) &&
421       transition_type != ADW_SQUEEZER_TRANSITION_TYPE_NONE &&
422       transition_duration != 0 &&
423       self->last_visible_child != NULL) {
424     self->active_transition_type = transition_type;
425     self->first_frame_skipped = FALSE;
426     adw_squeezer_schedule_ticks (self);
427     gtk_progress_tracker_start (&self->tracker,
428                                 self->transition_duration * 1000,
429                                 0,
430                                 1.0);
431   } else {
432     adw_squeezer_unschedule_ticks (self);
433     self->active_transition_type = ADW_SQUEEZER_TRANSITION_TYPE_NONE;
434     gtk_progress_tracker_finish (&self->tracker);
435   }
436 
437   adw_squeezer_progress_updated (ADW_SQUEEZER (widget));
438 }
439 
440 static void
set_visible_child(AdwSqueezer * self,AdwSqueezerPage * page,AdwSqueezerTransitionType transition_type,guint transition_duration)441 set_visible_child (AdwSqueezer               *self,
442                    AdwSqueezerPage           *page,
443                    AdwSqueezerTransitionType  transition_type,
444                    guint                      transition_duration)
445 {
446   GtkWidget *widget = GTK_WIDGET (self);
447   GtkRoot *root;
448   GtkWidget *focus;
449   gboolean contains_focus = FALSE;
450   guint old_pos = GTK_INVALID_LIST_POSITION;
451   guint new_pos = GTK_INVALID_LIST_POSITION;
452 
453   /* If we are being destroyed, do not bother with transitions and
454    * notifications.
455    */
456   if (gtk_widget_in_destruction (widget))
457     return;
458 
459   /* If none, pick the first visible. */
460   if (!page) {
461     GList *l;
462 
463     for (l = self->children; l; l = l->next) {
464       AdwSqueezerPage *p = l->data;
465       if (gtk_widget_get_visible (p->widget)) {
466         page = p;
467         break;
468       }
469     }
470   }
471 
472   if (page == self->visible_child)
473     return;
474 
475   if (self->pages) {
476     guint position;
477     GList *l;
478 
479     for (l = self->children, position = 0; l; l = l->next, position++) {
480       AdwSqueezerPage *p = l->data;
481       if (p == self->visible_child)
482         old_pos = position;
483       else if (p == page)
484         new_pos = position;
485     }
486   }
487 
488   root = gtk_widget_get_root (widget);
489   if (root)
490     focus = gtk_root_get_focus (root);
491   else
492     focus = NULL;
493 
494   if (focus &&
495       self->visible_child &&
496       self->visible_child->widget &&
497       gtk_widget_is_ancestor (focus, self->visible_child->widget)) {
498     contains_focus = TRUE;
499 
500     if (self->visible_child->last_focus)
501       g_object_remove_weak_pointer (G_OBJECT (self->visible_child->last_focus),
502                                     (gpointer *)&self->visible_child->last_focus);
503     self->visible_child->last_focus = focus;
504     g_object_add_weak_pointer (G_OBJECT (self->visible_child->last_focus),
505                                (gpointer *)&self->visible_child->last_focus);
506   }
507 
508   if (self->last_visible_child != NULL)
509     gtk_widget_set_child_visible (self->last_visible_child->widget, FALSE);
510   self->last_visible_child = NULL;
511 
512   if (self->visible_child && self->visible_child->widget) {
513     if (gtk_widget_is_visible (widget)) {
514       self->last_visible_child = self->visible_child;
515       self->last_visible_widget_width = gtk_widget_get_width (self->last_visible_child->widget);
516       self->last_visible_widget_height = gtk_widget_get_height (self->last_visible_child->widget);
517     } else {
518       gtk_widget_set_child_visible (self->visible_child->widget, FALSE);
519     }
520   }
521 
522   self->visible_child = page;
523 
524   if (page) {
525     gtk_widget_set_child_visible (page->widget, TRUE);
526 
527     if (contains_focus) {
528       if (page->last_focus)
529         gtk_widget_grab_focus (page->last_focus);
530       else
531         gtk_widget_child_focus (page->widget, GTK_DIR_TAB_FORWARD);
532     }
533   }
534 
535   if (self->homogeneous)
536     gtk_widget_queue_allocate (widget);
537   else
538     gtk_widget_queue_resize (widget);
539 
540   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VISIBLE_CHILD]);
541 
542   if (self->pages) {
543     if (old_pos == GTK_INVALID_LIST_POSITION && new_pos == GTK_INVALID_LIST_POSITION)
544       ; /* nothing to do */
545     else if (old_pos == GTK_INVALID_LIST_POSITION)
546       gtk_selection_model_selection_changed (self->pages, new_pos, 1);
547     else if (new_pos == GTK_INVALID_LIST_POSITION)
548       gtk_selection_model_selection_changed (self->pages, old_pos, 1);
549     else
550       gtk_selection_model_selection_changed (self->pages,
551                                              MIN (old_pos, new_pos),
552                                              MAX (old_pos, new_pos) - MIN (old_pos, new_pos) + 1);
553   }
554 
555   adw_squeezer_start_transition (self, transition_type, transition_duration);
556 }
557 
558 static void
update_child_visible(AdwSqueezer * self,AdwSqueezerPage * page)559 update_child_visible (AdwSqueezer     *self,
560                       AdwSqueezerPage *page)
561 {
562   gboolean enabled;
563 
564   enabled = page->enabled && gtk_widget_get_visible (page->widget);
565 
566   if (self->visible_child == NULL && enabled)
567     set_visible_child (self, page, self->transition_type, self->transition_duration);
568   else if (self->visible_child == page && !enabled)
569     set_visible_child (self, NULL, self->transition_type, self->transition_duration);
570 
571   if (page == self->last_visible_child) {
572     gtk_widget_set_child_visible (self->last_visible_child->widget, FALSE);
573     self->last_visible_child = NULL;
574   }
575 }
576 
577 static void
squeezer_child_visibility_notify_cb(GObject * obj,GParamSpec * pspec,gpointer user_data)578 squeezer_child_visibility_notify_cb (GObject    *obj,
579                                      GParamSpec *pspec,
580                                      gpointer    user_data)
581 {
582   AdwSqueezer *self = ADW_SQUEEZER (user_data);
583   GtkWidget *child = GTK_WIDGET (obj);
584   AdwSqueezerPage *page;
585 
586   page = find_page_for_widget (self, child);
587   g_return_if_fail (page != NULL);
588 
589   update_child_visible (self, page);
590 }
591 
592 static void
add_page(AdwSqueezer * self,AdwSqueezerPage * page)593 add_page (AdwSqueezer     *self,
594           AdwSqueezerPage *page)
595 {
596   g_return_if_fail (page->widget != NULL);
597 
598   self->children = g_list_append (self->children, g_object_ref (page));
599 
600   gtk_widget_set_child_visible (page->widget, FALSE);
601   gtk_widget_set_parent (page->widget, GTK_WIDGET (self));
602 
603   if (self->pages)
604     g_list_model_items_changed (G_LIST_MODEL (self->pages), g_list_length (self->children) - 1, 0, 1);
605 
606   g_signal_connect (page->widget, "notify::visible",
607                     G_CALLBACK (squeezer_child_visibility_notify_cb), self);
608 
609   if (self->visible_child == NULL &&
610       gtk_widget_get_visible (page->widget))
611     set_visible_child (self, page, self->transition_type, self->transition_duration);
612 
613   if (self->homogeneous || self->visible_child == page)
614     gtk_widget_queue_resize (GTK_WIDGET (self));
615 }
616 
617 static void
squeezer_remove(AdwSqueezer * self,GtkWidget * child,gboolean in_dispose)618 squeezer_remove (AdwSqueezer *self,
619                  GtkWidget   *child,
620                  gboolean     in_dispose)
621 {
622   AdwSqueezerPage *page;
623   gboolean was_visible;
624 
625   page = find_page_for_widget (self, child);
626   if (!page)
627     return;
628 
629   self->children = g_list_remove (self->children, page);
630 
631   g_signal_handlers_disconnect_by_func (child,
632                                         squeezer_child_visibility_notify_cb,
633                                         self);
634 
635   was_visible = gtk_widget_get_visible (child);
636 
637   g_clear_object (&page->widget);
638 
639   if (self->visible_child == page)
640     {
641       if (in_dispose)
642         self->visible_child = NULL;
643       else
644         set_visible_child (self, NULL, self->transition_type, self->transition_duration);
645     }
646 
647   if (self->last_visible_child == page)
648     self->last_visible_child = NULL;
649 
650   gtk_widget_unparent (child);
651 
652   g_object_unref (page);
653 
654   if (self->homogeneous && was_visible)
655     gtk_widget_queue_resize (GTK_WIDGET (self));
656 }
657 
658 static void
adw_squeezer_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)659 adw_squeezer_get_property (GObject    *object,
660                            guint       property_id,
661                            GValue     *value,
662                            GParamSpec *pspec)
663 {
664   AdwSqueezer *self = ADW_SQUEEZER (object);
665 
666   switch (property_id) {
667   case PROP_HOMOGENEOUS:
668     g_value_set_boolean (value, adw_squeezer_get_homogeneous (self));
669     break;
670   case PROP_VISIBLE_CHILD:
671     g_value_set_object (value, adw_squeezer_get_visible_child (self));
672     break;
673   case PROP_TRANSITION_DURATION:
674     g_value_set_uint (value, adw_squeezer_get_transition_duration (self));
675     break;
676   case PROP_TRANSITION_TYPE:
677     g_value_set_enum (value, adw_squeezer_get_transition_type (self));
678     break;
679   case PROP_TRANSITION_RUNNING:
680     g_value_set_boolean (value, adw_squeezer_get_transition_running (self));
681     break;
682   case PROP_INTERPOLATE_SIZE:
683     g_value_set_boolean (value, adw_squeezer_get_interpolate_size (self));
684     break;
685   case PROP_XALIGN:
686     g_value_set_float (value, adw_squeezer_get_xalign (self));
687     break;
688   case PROP_YALIGN:
689     g_value_set_float (value, adw_squeezer_get_yalign (self));
690     break;
691   case PROP_ORIENTATION:
692     g_value_set_enum (value, get_orientation (self));
693     break;
694   case PROP_PAGES:
695     g_value_set_object (value, adw_squeezer_get_pages (self));
696     break;
697   default:
698     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
699     break;
700   }
701 }
702 
703 static void
adw_squeezer_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)704 adw_squeezer_set_property (GObject      *object,
705                            guint         property_id,
706                            const GValue *value,
707                            GParamSpec   *pspec)
708 {
709   AdwSqueezer *self = ADW_SQUEEZER (object);
710 
711   switch (property_id) {
712   case PROP_HOMOGENEOUS:
713     adw_squeezer_set_homogeneous (self, g_value_get_boolean (value));
714     break;
715   case PROP_TRANSITION_DURATION:
716     adw_squeezer_set_transition_duration (self, g_value_get_uint (value));
717     break;
718   case PROP_TRANSITION_TYPE:
719     adw_squeezer_set_transition_type (self, g_value_get_enum (value));
720     break;
721   case PROP_INTERPOLATE_SIZE:
722     adw_squeezer_set_interpolate_size (self, g_value_get_boolean (value));
723     break;
724   case PROP_XALIGN:
725     adw_squeezer_set_xalign (self, g_value_get_float (value));
726     break;
727   case PROP_YALIGN:
728     adw_squeezer_set_yalign (self, g_value_get_float (value));
729     break;
730   case PROP_ORIENTATION:
731     set_orientation (self, g_value_get_enum (value));
732     break;
733   default:
734     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
735     break;
736   }
737 }
738 
739 static void
adw_squeezer_compute_expand(GtkWidget * widget,gboolean * hexpand_p,gboolean * vexpand_p)740 adw_squeezer_compute_expand (GtkWidget *widget,
741                              gboolean  *hexpand_p,
742                              gboolean  *vexpand_p)
743 {
744   AdwSqueezer *self = ADW_SQUEEZER (widget);
745   gboolean hexpand, vexpand;
746   AdwSqueezerPage *page;
747   GtkWidget *child;
748   GList *l;
749 
750   hexpand = FALSE;
751   vexpand = FALSE;
752   for (l = self->children; l != NULL; l = l->next) {
753     page = l->data;
754     child = page->widget;
755 
756     if (!hexpand &&
757         gtk_widget_compute_expand (child, GTK_ORIENTATION_HORIZONTAL))
758       hexpand = TRUE;
759 
760     if (!vexpand &&
761         gtk_widget_compute_expand (child, GTK_ORIENTATION_VERTICAL))
762       vexpand = TRUE;
763 
764     if (hexpand && vexpand)
765       break;
766   }
767 
768   *hexpand_p = hexpand;
769   *vexpand_p = vexpand;
770 }
771 
772 static void
adw_squeezer_snapshot_crossfade(GtkWidget * widget,GtkSnapshot * snapshot)773 adw_squeezer_snapshot_crossfade (GtkWidget   *widget,
774                                  GtkSnapshot *snapshot)
775 {
776   AdwSqueezer *self = ADW_SQUEEZER (widget);
777   double progress = gtk_progress_tracker_get_progress (&self->tracker, FALSE);
778 
779   gtk_snapshot_push_cross_fade (snapshot, progress);
780 
781   if (self->last_visible_child) {
782     int width_diff = MIN (gtk_widget_get_width (widget) - self->last_visible_widget_width, 0);
783     int height_diff = MIN (gtk_widget_get_height (widget) - self->last_visible_widget_height, 0);
784     float xalign = self->xalign;
785 
786     if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL)
787       xalign = 1 - xalign;
788 
789     gtk_snapshot_translate (snapshot,
790                             &GRAPHENE_POINT_INIT (
791                               width_diff * xalign,
792                               height_diff * self->yalign
793                             ));
794 
795     gtk_widget_snapshot_child (widget,
796                                self->last_visible_child->widget,
797                                snapshot);
798   }
799 
800   gtk_snapshot_pop (snapshot);
801 
802   gtk_widget_snapshot_child (widget,
803                              self->visible_child->widget,
804                              snapshot);
805   gtk_snapshot_pop (snapshot);
806 }
807 
808 
809 static void
adw_squeezer_snapshot(GtkWidget * widget,GtkSnapshot * snapshot)810 adw_squeezer_snapshot (GtkWidget   *widget,
811                        GtkSnapshot *snapshot)
812 {
813   AdwSqueezer *self = ADW_SQUEEZER (widget);
814 
815   if (self->visible_child) {
816     if (gtk_progress_tracker_get_state (&self->tracker) != GTK_PROGRESS_STATE_AFTER) {
817       gtk_snapshot_push_clip (snapshot,
818                               &GRAPHENE_RECT_INIT(
819                                   0, 0,
820                                   gtk_widget_get_width (widget),
821                                   gtk_widget_get_height (widget)
822                               ));
823 
824       switch (self->active_transition_type)
825         {
826         case ADW_SQUEEZER_TRANSITION_TYPE_CROSSFADE:
827           adw_squeezer_snapshot_crossfade (widget, snapshot);
828           break;
829         case ADW_SQUEEZER_TRANSITION_TYPE_NONE:
830         default:
831           g_assert_not_reached ();
832         }
833 
834       gtk_snapshot_pop (snapshot);
835     } else
836       gtk_widget_snapshot_child (widget,
837                                  self->visible_child->widget,
838                                  snapshot);
839   }
840 }
841 
842 static void
adw_squeezer_size_allocate(GtkWidget * widget,int width,int height,int baseline)843 adw_squeezer_size_allocate (GtkWidget *widget,
844                             int        width,
845                             int        height,
846                             int        baseline)
847 {
848   AdwSqueezer *self = ADW_SQUEEZER (widget);
849   AdwSqueezerPage *page = NULL;
850   GtkWidget *child = NULL;
851   int child_min;
852   GList *l;
853   GtkAllocation child_allocation;
854 
855   for (l = self->children; l; l = l->next) {
856     int for_size = -1;
857 
858     page = l->data;
859     child = page->widget;
860 
861     if (!gtk_widget_get_visible (child))
862       continue;
863 
864     if (!page->enabled)
865       continue;
866 
867     if (self->orientation == GTK_ORIENTATION_VERTICAL) {
868       if (gtk_widget_get_request_mode (child) == GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH)
869         for_size = width;
870 
871       gtk_widget_measure (child, self->orientation, for_size,
872                           &child_min, NULL, NULL, NULL);
873 
874       if (child_min <= height)
875         break;
876     } else {
877       if (gtk_widget_get_request_mode (child) == GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT)
878         for_size = height;
879 
880       gtk_widget_measure (child, self->orientation, for_size,
881                           &child_min, NULL, NULL, NULL);
882 
883       if (child_min <= width)
884         break;
885     }
886   }
887 
888   set_visible_child (self, page,
889                      self->transition_type,
890                      self->transition_duration);
891 
892   child_allocation.x = 0;
893   child_allocation.y = 0;
894 
895   if (self->last_visible_child) {
896     int min, nat;
897 
898     if (self->orientation == GTK_ORIENTATION_HORIZONTAL) {
899       gtk_widget_measure (self->last_visible_child->widget, GTK_ORIENTATION_HORIZONTAL,
900                           -1, &min, &nat, NULL, NULL);
901       child_allocation.width = MAX (min, width);
902       gtk_widget_measure (self->last_visible_child->widget, GTK_ORIENTATION_VERTICAL,
903                           child_allocation.width, &min, &nat, NULL, NULL);
904       child_allocation.height = MAX (min, height);
905     } else {
906       gtk_widget_measure (self->last_visible_child->widget, GTK_ORIENTATION_VERTICAL,
907                           -1, &min, &nat, NULL, NULL);
908       child_allocation.height = MAX (min, height);
909       gtk_widget_measure (self->last_visible_child->widget, GTK_ORIENTATION_HORIZONTAL,
910                           child_allocation.height, &min, &nat, NULL, NULL);
911       child_allocation.width = MAX (min, width);
912     }
913 
914     gtk_widget_size_allocate (self->last_visible_child->widget, &child_allocation, -1);
915   }
916 
917   child_allocation.width = width;
918   child_allocation.height = height;
919 
920   if (self->visible_child) {
921     int min;
922 
923     gtk_widget_measure (self->visible_child->widget, GTK_ORIENTATION_HORIZONTAL,
924                         height, &min, NULL, NULL, NULL);
925     child_allocation.width = MAX (child_allocation.width, min);
926 
927     gtk_widget_measure (self->visible_child->widget, GTK_ORIENTATION_VERTICAL,
928                         width, &min, NULL, NULL, NULL);
929     child_allocation.height = MAX (child_allocation.height, min);
930 
931     if (child_allocation.width > width) {
932       if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL)
933         child_allocation.x = (width - child_allocation.width) * (1 - self->xalign);
934       else
935         child_allocation.x = (width - child_allocation.width) * self->xalign;
936     }
937 
938     if (child_allocation.height > height)
939       child_allocation.y = (height - child_allocation.height) * self->yalign;
940 
941     gtk_widget_size_allocate (self->visible_child->widget, &child_allocation, -1);
942   }
943 }
944 
945 static void
adw_squeezer_measure(GtkWidget * widget,GtkOrientation orientation,int for_size,int * minimum,int * natural,int * minimum_baseline,int * natural_baseline)946 adw_squeezer_measure (GtkWidget      *widget,
947                       GtkOrientation  orientation,
948                       int             for_size,
949                       int            *minimum,
950                       int            *natural,
951                       int            *minimum_baseline,
952                       int            *natural_baseline)
953 {
954   AdwSqueezer *self = ADW_SQUEEZER (widget);
955   int child_min, child_nat;
956   GList *l;
957   int min = 0, nat = 0;
958 
959   for (l = self->children; l != NULL; l = l->next) {
960     AdwSqueezerPage *page = l->data;
961     GtkWidget *child = page->widget;
962 
963     if (self->orientation != orientation && !self->homogeneous &&
964         self->visible_child != page)
965       continue;
966 
967     if (!gtk_widget_get_visible (child))
968       continue;
969 
970     /* Disabled children are taken into account when measuring the widget, to
971      * keep its size request and allocation consistent. This avoids the
972      * appearant size and position of a child to changes suddenly when a larger
973      * child gets enabled/disabled.
974      */
975     gtk_widget_measure (child, orientation, for_size,
976                         &child_min, &child_nat, NULL, NULL);
977 
978     if (self->orientation == orientation)
979       min = min == 0 ? child_min : MIN (min, child_min);
980     else
981       min = MAX (min, child_min);
982 
983     nat = MAX (nat, child_nat);
984   }
985 
986   if (self->orientation != orientation && !self->homogeneous &&
987       self->interpolate_size &&
988       self->last_visible_child != NULL) {
989     double t = gtk_progress_tracker_get_ease_out_cubic (&self->tracker, FALSE);
990 
991     if (orientation == GTK_ORIENTATION_VERTICAL) {
992       min = adw_lerp (self->last_visible_widget_height, min, t);
993       nat = adw_lerp (self->last_visible_widget_height, nat, t);
994     } else {
995       min = adw_lerp (self->last_visible_widget_width, min, t);
996       nat = adw_lerp (self->last_visible_widget_width, nat, t);
997     }
998   }
999 
1000   if (minimum)
1001     *minimum = min;
1002   if (natural)
1003     *natural = nat;
1004   if (minimum_baseline)
1005     *minimum_baseline = -1;
1006   if (natural_baseline)
1007     *natural_baseline = -1;
1008 }
1009 
1010 static void
adw_squeezer_dispose(GObject * object)1011 adw_squeezer_dispose (GObject *object)
1012 {
1013   AdwSqueezer *self = ADW_SQUEEZER (object);
1014   GtkWidget *child;
1015 
1016   if (self->pages)
1017     g_list_model_items_changed (G_LIST_MODEL (self->pages), 0,
1018                                 g_list_length (self->children), 0);
1019 
1020   while ((child = gtk_widget_get_first_child (GTK_WIDGET (self))))
1021     squeezer_remove (self, child, TRUE);
1022 
1023   G_OBJECT_CLASS (adw_squeezer_parent_class)->dispose (object);
1024 }
1025 
1026 static void
adw_squeezer_finalize(GObject * object)1027 adw_squeezer_finalize (GObject *object)
1028 {
1029   AdwSqueezer *self = ADW_SQUEEZER (object);
1030 
1031   if (self->pages)
1032     g_object_remove_weak_pointer (G_OBJECT (self->pages),
1033                                   (gpointer *) &self->pages);
1034 
1035   adw_squeezer_unschedule_ticks (self);
1036 
1037   G_OBJECT_CLASS (adw_squeezer_parent_class)->finalize (object);
1038 }
1039 
1040 static void
adw_squeezer_class_init(AdwSqueezerClass * klass)1041 adw_squeezer_class_init (AdwSqueezerClass *klass)
1042 {
1043   GObjectClass *object_class = G_OBJECT_CLASS (klass);
1044   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
1045 
1046   object_class->get_property = adw_squeezer_get_property;
1047   object_class->set_property = adw_squeezer_set_property;
1048   object_class->dispose = adw_squeezer_dispose;
1049   object_class->finalize = adw_squeezer_finalize;
1050 
1051   widget_class->size_allocate = adw_squeezer_size_allocate;
1052   widget_class->snapshot = adw_squeezer_snapshot;
1053   widget_class->measure = adw_squeezer_measure;
1054   widget_class->compute_expand = adw_squeezer_compute_expand;
1055 
1056   g_object_class_override_property (object_class,
1057                                     PROP_ORIENTATION,
1058                                     "orientation");
1059 
1060   props[PROP_HOMOGENEOUS] =
1061     g_param_spec_boolean ("homogeneous",
1062                           "Homogeneous",
1063                           "Homogeneous sizing",
1064                           FALSE,
1065                           G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
1066 
1067   props[PROP_VISIBLE_CHILD] =
1068     g_param_spec_object ("visible-child",
1069                          "Visible child",
1070                          "The widget currently visible in the squeezer",
1071                          GTK_TYPE_WIDGET,
1072                          G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
1073 
1074   props[PROP_TRANSITION_DURATION] =
1075     g_param_spec_uint ("transition-duration",
1076                        "Transition duration",
1077                        "The animation duration, in milliseconds",
1078                        0, G_MAXUINT, 200,
1079                        G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
1080 
1081   props[PROP_TRANSITION_TYPE] =
1082     g_param_spec_enum ("transition-type",
1083                        "Transition type",
1084                        "The type of animation used to transition",
1085                        ADW_TYPE_SQUEEZER_TRANSITION_TYPE,
1086                        ADW_SQUEEZER_TRANSITION_TYPE_NONE,
1087                        G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
1088 
1089   props[PROP_TRANSITION_RUNNING] =
1090     g_param_spec_boolean ("transition-running",
1091                           "Transition running",
1092                           "Whether or not the transition is currently running",
1093                           FALSE,
1094                           G_PARAM_READABLE);
1095 
1096   props[PROP_INTERPOLATE_SIZE] =
1097     g_param_spec_boolean ("interpolate-size",
1098                           "Interpolate size",
1099                           "Whether or not the size should smoothly change when changing between differently sized children",
1100                           FALSE,
1101                           G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
1102 
1103   /**
1104    * AdwSqueezer:xalign:
1105    *
1106    * The xalign property determines the horizontal alignment of the children
1107    * inside the squeezer's size allocation.
1108    * Compare this to #GtkWidget:halign, which determines how the squeezer's size
1109    * allocation is positioned in the space available for the squeezer.
1110    * The range goes from 0 (start) to 1 (end).
1111    *
1112    * This will affect the position of children too wide to fit in the squeezer
1113    * as they are fading out.
1114    *
1115    * Since: 1.0
1116    */
1117   props[PROP_XALIGN] =
1118     g_param_spec_float ("xalign",
1119                         "X align",
1120                         "The horizontal alignment, from 0 (start) to 1 (end)",
1121                         0.0, 1.0,
1122                         0.5,
1123                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
1124 
1125   /**
1126    * AdwSqueezer:yalign:
1127    *
1128    * The yalign property determines the vertical alignment of the children inside
1129    * the squeezer's size allocation.
1130    * Compare this to #GtkWidget:valign, which determines how the squeezer's size
1131    * allocation is positioned in the space available for the squeezer.
1132    * The range goes from 0 (top) to 1 (bottom).
1133    *
1134    * This will affect the position of children too tall to fit in the squeezer
1135    * as they are fading out.
1136    *
1137    * Since: 1.0
1138    */
1139   props[PROP_YALIGN] =
1140     g_param_spec_float ("yalign",
1141                         "Y align",
1142                         "The vertical alignment, from 0 (top) to 1 (bottom)",
1143                         0.0, 1.0,
1144                         0.5,
1145                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
1146 
1147   props[PROP_PAGES] =
1148     g_param_spec_object ("pages",
1149                          "Pages",
1150                          "A selection model with the squeezer's pages",
1151                          GTK_TYPE_SELECTION_MODEL,
1152                          G_PARAM_READABLE);
1153 
1154   g_object_class_install_properties (object_class, LAST_PROP, props);
1155 
1156   gtk_widget_class_set_css_name (widget_class, "squeezer");
1157 }
1158 
1159 static void
adw_squeezer_init(AdwSqueezer * self)1160 adw_squeezer_init (AdwSqueezer *self)
1161 {
1162   self->homogeneous = TRUE;
1163   self->transition_duration = 200;
1164   self->transition_type = ADW_SQUEEZER_TRANSITION_TYPE_NONE;
1165   self->xalign = 0.5;
1166   self->yalign = 0.5;
1167 }
1168 
1169 static void
adw_squeezer_buildable_add_child(GtkBuildable * buildable,GtkBuilder * builder,GObject * child,const char * type)1170 adw_squeezer_buildable_add_child (GtkBuildable *buildable,
1171                                   GtkBuilder   *builder,
1172                                   GObject      *child,
1173                                   const char   *type)
1174 {
1175   if (ADW_IS_SQUEEZER_PAGE (child))
1176     add_page (ADW_SQUEEZER (buildable), ADW_SQUEEZER_PAGE (child));
1177   else if (GTK_IS_WIDGET (child))
1178     adw_squeezer_add (ADW_SQUEEZER (buildable), GTK_WIDGET (child));
1179   else
1180     parent_buildable_iface->add_child (buildable, builder, child, type);
1181 }
1182 
1183 static void
adw_squeezer_buildable_init(GtkBuildableIface * iface)1184 adw_squeezer_buildable_init (GtkBuildableIface *iface)
1185 {
1186   parent_buildable_iface = g_type_interface_peek_parent (iface);
1187 
1188   iface->add_child = adw_squeezer_buildable_add_child;
1189 }
1190 
1191 /**
1192  * adw_squeezer_page_get_child:
1193  * @self: a #AdwSqueezerPage
1194  *
1195  * Returns the squeezer child to which @self belongs.
1196  *
1197  * Returns: (transfer none): the child to which @self belongs
1198  *
1199  * Since: 1.0
1200  */
1201 GtkWidget *
adw_squeezer_page_get_child(AdwSqueezerPage * self)1202 adw_squeezer_page_get_child (AdwSqueezerPage *self)
1203 {
1204   g_return_val_if_fail (ADW_IS_SQUEEZER_PAGE (self), NULL);
1205 
1206   return self->widget;
1207 }
1208 
1209 /**
1210  * adw_squeezer_page_get_enabled:
1211  * @self: a #AdwSqueezerPage
1212  *
1213  * Returns whether @self is enabled in its #AdwSqueezer. This is independent
1214  * from the #GtkWidget:visible value of its #GtkWidget.
1215  *
1216  * See adw_squeezer_page_set_enabled().
1217  *
1218  * Returns: %TRUE if @self is enabled, %FALSE otherwise
1219  *
1220  * Since: 1.0
1221  */
1222 gboolean
adw_squeezer_page_get_enabled(AdwSqueezerPage * self)1223 adw_squeezer_page_get_enabled (AdwSqueezerPage *self)
1224 {
1225   g_return_val_if_fail (ADW_IS_SQUEEZER_PAGE (self), FALSE);
1226 
1227   return self->enabled;
1228 }
1229 
1230 /**
1231  * adw_squeezer_page_set_enabled:
1232  * @self: a #AdwSqueezerPage
1233  * @enabled: %TRUE to enable the child, %FALSE to disable it
1234  *
1235  * Make the squeezer enable or disable @child. If a child is disabled, it will
1236  * be ignored when looking for the child fitting the available size best. This
1237  * allows to programmatically and prematurely hide a child even if it fits in
1238  * the available space.
1239  *
1240  * This can be used e.g. to ensure a certain child is hidden below a certain
1241  * window width, or any other constraint you find suitable.
1242  *
1243  * Sets the new value of the #AdwSqueezerPage:enabled property to @enabled.
1244  *
1245  * Since: 1.0
1246  */
1247 void
adw_squeezer_page_set_enabled(AdwSqueezerPage * self,gboolean enabled)1248 adw_squeezer_page_set_enabled (AdwSqueezerPage *self,
1249                                gboolean         enabled)
1250 {
1251   g_return_if_fail (ADW_IS_SQUEEZER_PAGE (self));
1252 
1253   enabled = !!enabled;
1254 
1255   if (enabled == self->enabled)
1256     return;
1257 
1258   self->enabled = enabled;
1259 
1260   if (self->widget && gtk_widget_get_parent (self->widget)) {
1261     AdwSqueezer *squeezer = ADW_SQUEEZER (gtk_widget_get_parent (self->widget));
1262 
1263     gtk_widget_queue_resize (GTK_WIDGET (squeezer));
1264     update_child_visible (squeezer, self);
1265   }
1266 
1267   g_object_notify_by_pspec (G_OBJECT (self), page_props[PAGE_PROP_ENABLED]);
1268 }
1269 
1270 /**
1271  * adw_squeezer_new:
1272  *
1273  * Creates a new #AdwSqueezer container.
1274  *
1275  * Returns: a new #AdwSqueezer
1276  *
1277  * Since: 1.0
1278  */
1279 GtkWidget *
adw_squeezer_new(void)1280 adw_squeezer_new (void)
1281 {
1282   return g_object_new (ADW_TYPE_SQUEEZER, NULL);
1283 }
1284 
1285 /**
1286  * adw_squeezer_add:
1287  * @self: a #AdwSqueezer
1288  * @child: the widget to add
1289  *
1290  * Adds a child to @self.
1291  *
1292  * Returns: (transfer none): the #AdwSqueezerPage for @child
1293  *
1294  * Since: 1.0
1295  */
1296 AdwSqueezerPage *
adw_squeezer_add(AdwSqueezer * self,GtkWidget * child)1297 adw_squeezer_add (AdwSqueezer *self,
1298                   GtkWidget   *child)
1299 {
1300   AdwSqueezerPage *page;
1301 
1302   g_return_val_if_fail (ADW_IS_SQUEEZER (self), NULL);
1303   g_return_val_if_fail (GTK_IS_WIDGET (child), NULL);
1304 
1305   page = g_object_new (ADW_TYPE_SQUEEZER_PAGE, NULL);
1306   page->widget = g_object_ref (child);
1307 
1308   add_page (self, page);
1309 
1310   g_object_unref (page);
1311 
1312   return page;
1313 }
1314 
1315 /**
1316  * adw_squeezer_remove:
1317  * @self: a #AdwSqueezer
1318  * @child: the child to remove
1319  *
1320  * Removes a child widget from @self.
1321  *
1322  * Since: 1.0
1323  */
1324 void
adw_squeezer_remove(AdwSqueezer * self,GtkWidget * child)1325 adw_squeezer_remove (AdwSqueezer *self,
1326                      GtkWidget   *child)
1327 {
1328   GList *l;
1329   guint position;
1330 
1331   g_return_if_fail (ADW_IS_SQUEEZER (self));
1332   g_return_if_fail (GTK_IS_WIDGET (child));
1333   g_return_if_fail (gtk_widget_get_parent (child) == GTK_WIDGET (self));
1334 
1335   for (l = self->children, position = 0; l; l = l->next, position++) {
1336     AdwSqueezerPage *page = l->data;
1337 
1338     if (page->widget == child)
1339       break;
1340   }
1341 
1342   squeezer_remove (self, child, FALSE);
1343 
1344   if (self->pages)
1345     g_list_model_items_changed (G_LIST_MODEL (self->pages), position, 1, 0);
1346 }
1347 
1348 /**
1349  * adw_squeezer_get_page:
1350  * @self: a #AdwSqueezer
1351  * @child: a child of @self
1352  *
1353  * Returns the #AdwSqueezerPage object for @child.
1354  *
1355  * Returns: (transfer none): the #AdwSqueezerPage for @child
1356  *
1357  * Since: 1.0
1358  */
1359 AdwSqueezerPage *
adw_squeezer_get_page(AdwSqueezer * self,GtkWidget * child)1360 adw_squeezer_get_page (AdwSqueezer *self,
1361                        GtkWidget   *child)
1362 {
1363   g_return_val_if_fail (ADW_IS_SQUEEZER (self), NULL);
1364   g_return_val_if_fail (GTK_IS_WIDGET (child), NULL);
1365 
1366   return find_page_for_widget (self, child);
1367 }
1368 
1369 /**
1370  * adw_squeezer_get_homogeneous:
1371  * @self: a #AdwSqueezer
1372  *
1373  * Gets whether @self is homogeneous.
1374  *
1375  * See adw_squeezer_set_homogeneous().
1376  *
1377  * Returns: %TRUE if @self is homogeneous, %FALSE is not
1378  *
1379  * Since: 1.0
1380  */
1381 gboolean
adw_squeezer_get_homogeneous(AdwSqueezer * self)1382 adw_squeezer_get_homogeneous (AdwSqueezer *self)
1383 {
1384   g_return_val_if_fail (ADW_IS_SQUEEZER (self), FALSE);
1385 
1386   return self->homogeneous;
1387 }
1388 
1389 /**
1390  * adw_squeezer_set_homogeneous:
1391  * @self: a #AdwSqueezer
1392  * @homogeneous: %TRUE to make @self homogeneous
1393  *
1394  * Sets @self to be homogeneous or not. If it is homogeneous, @self will request
1395  * the same size for all its children for its opposite orientation, e.g. if
1396  * @self is oriented horizontally and is homogeneous, it will request the same
1397  * height for all its children. If it isn't, @self may change size when a
1398  * different child becomes visible.
1399  *
1400  * Since: 1.0
1401  */
1402 void
adw_squeezer_set_homogeneous(AdwSqueezer * self,gboolean homogeneous)1403 adw_squeezer_set_homogeneous (AdwSqueezer *self,
1404                               gboolean     homogeneous)
1405 {
1406   g_return_if_fail (ADW_IS_SQUEEZER (self));
1407 
1408   homogeneous = !!homogeneous;
1409 
1410   if (self->homogeneous == homogeneous)
1411     return;
1412 
1413   self->homogeneous = homogeneous;
1414 
1415   if (gtk_widget_get_visible (GTK_WIDGET(self)))
1416     gtk_widget_queue_resize (GTK_WIDGET (self));
1417 
1418   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_HOMOGENEOUS]);
1419 }
1420 
1421 /**
1422  * adw_squeezer_get_transition_duration:
1423  * @self: a #AdwSqueezer
1424  *
1425  * Gets the amount of time (in milliseconds) that transitions between children
1426  * in @self will take.
1427  *
1428  * Returns: the transition duration
1429  *
1430  * Since: 1.0
1431  */
1432 guint
adw_squeezer_get_transition_duration(AdwSqueezer * self)1433 adw_squeezer_get_transition_duration (AdwSqueezer *self)
1434 {
1435   g_return_val_if_fail (ADW_IS_SQUEEZER (self), 0);
1436 
1437   return self->transition_duration;
1438 }
1439 
1440 /**
1441  * adw_squeezer_set_transition_duration:
1442  * @self: a #AdwSqueezer
1443  * @duration: the new duration, in milliseconds
1444  *
1445  * Sets the duration that transitions between children in @self will take.
1446  *
1447  * Since: 1.0
1448  */
1449 void
adw_squeezer_set_transition_duration(AdwSqueezer * self,guint duration)1450 adw_squeezer_set_transition_duration (AdwSqueezer *self,
1451                                       guint        duration)
1452 {
1453   g_return_if_fail (ADW_IS_SQUEEZER (self));
1454 
1455   if (self->transition_duration == duration)
1456     return;
1457 
1458   self->transition_duration = duration;
1459   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_DURATION]);
1460 }
1461 
1462 /**
1463  * adw_squeezer_get_transition_type:
1464  * @self: a #AdwSqueezer
1465  *
1466  * Gets the type of animation that will be used for transitions between children
1467  * in @self.
1468  *
1469  * Returns: the current transition type of @self
1470  *
1471  * Since: 1.0
1472  */
1473 AdwSqueezerTransitionType
adw_squeezer_get_transition_type(AdwSqueezer * self)1474 adw_squeezer_get_transition_type (AdwSqueezer *self)
1475 {
1476   g_return_val_if_fail (ADW_IS_SQUEEZER (self), ADW_SQUEEZER_TRANSITION_TYPE_NONE);
1477 
1478   return self->transition_type;
1479 }
1480 
1481 /**
1482  * adw_squeezer_set_transition_type:
1483  * @self: a #AdwSqueezer
1484  * @transition: the new transition type
1485  *
1486  * Sets the type of animation that will be used for transitions between children
1487  * in @self. Available types include various kinds of fades and slides.
1488  *
1489  * The transition type can be changed without problems at runtime, so it is
1490  * possible to change the animation based on the child that is about to become
1491  * current.
1492  *
1493  * Since: 1.0
1494  */
1495 void
adw_squeezer_set_transition_type(AdwSqueezer * self,AdwSqueezerTransitionType transition)1496 adw_squeezer_set_transition_type (AdwSqueezer               *self,
1497                                   AdwSqueezerTransitionType  transition)
1498 {
1499   g_return_if_fail (ADW_IS_SQUEEZER (self));
1500 
1501   if (self->transition_type == transition)
1502     return;
1503 
1504   self->transition_type = transition;
1505   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_TYPE]);
1506 }
1507 
1508 /**
1509  * adw_squeezer_get_transition_running:
1510  * @self: a #AdwSqueezer
1511  *
1512  * Gets whether @self is currently in a transition from one child to another.
1513  *
1514  * Returns: %TRUE if the transition is currently running, %FALSE otherwise.
1515  *
1516  * Since: 1.0
1517  */
1518 gboolean
adw_squeezer_get_transition_running(AdwSqueezer * self)1519 adw_squeezer_get_transition_running (AdwSqueezer *self)
1520 {
1521   g_return_val_if_fail (ADW_IS_SQUEEZER (self), FALSE);
1522 
1523   return (self->tick_id != 0);
1524 }
1525 
1526 /**
1527  * adw_squeezer_get_interpolate_size:
1528  * @self: A #AdwSqueezer
1529  *
1530  * Gets whether @self should interpolate its size on visible child change.
1531  *
1532  * See adw_squeezer_set_interpolate_size().
1533  *
1534  * Returns: %TRUE if @self interpolates its size on visible child change, %FALSE if not
1535  *
1536  * Since: 1.0
1537  */
1538 gboolean
adw_squeezer_get_interpolate_size(AdwSqueezer * self)1539 adw_squeezer_get_interpolate_size (AdwSqueezer *self)
1540 {
1541   g_return_val_if_fail (ADW_IS_SQUEEZER (self), FALSE);
1542 
1543   return self->interpolate_size;
1544 }
1545 
1546 /**
1547  * adw_squeezer_set_interpolate_size:
1548  * @self: A #AdwSqueezer
1549  * @interpolate_size: %TRUE to interpolate the size
1550  *
1551  * Sets whether or not @self will interpolate the size of its opposing
1552  * orientation when changing the visible child. If %TRUE, @self will interpolate
1553  * its size between the one of the previous visible child and the one of the new
1554  * visible child, according to the set transition duration and the orientation,
1555  * e.g. if @self is horizontal, it will interpolate the its height.
1556  *
1557  * Since: 1.0
1558  */
1559 void
adw_squeezer_set_interpolate_size(AdwSqueezer * self,gboolean interpolate_size)1560 adw_squeezer_set_interpolate_size (AdwSqueezer *self,
1561                                    gboolean     interpolate_size)
1562 {
1563   g_return_if_fail (ADW_IS_SQUEEZER (self));
1564 
1565   interpolate_size = !!interpolate_size;
1566 
1567   if (self->interpolate_size == interpolate_size)
1568     return;
1569 
1570   self->interpolate_size = interpolate_size;
1571   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INTERPOLATE_SIZE]);
1572 }
1573 
1574 /**
1575  * adw_squeezer_get_visible_child:
1576  * @self: a #AdwSqueezer
1577  *
1578  * Gets the currently visible child of @self, or %NULL if there are no visible
1579  * children.
1580  *
1581  * Returns: (transfer none) (nullable): the visible child of the #AdwSqueezer
1582  *
1583  * Since: 1.0
1584  */
1585 GtkWidget *
adw_squeezer_get_visible_child(AdwSqueezer * self)1586 adw_squeezer_get_visible_child (AdwSqueezer *self)
1587 {
1588   g_return_val_if_fail (ADW_IS_SQUEEZER (self), NULL);
1589 
1590   return self->visible_child ? self->visible_child->widget : NULL;
1591 }
1592 
1593 /**
1594  * adw_squeezer_get_xalign:
1595  * @self: a #AdwSqueezer
1596  *
1597  * Gets the #AdwSqueezer:xalign property for @self.
1598  *
1599  * Returns: the xalign property
1600  *
1601  * Since: 1.0
1602  */
1603 float
adw_squeezer_get_xalign(AdwSqueezer * self)1604 adw_squeezer_get_xalign (AdwSqueezer *self)
1605 {
1606   g_return_val_if_fail (ADW_IS_SQUEEZER (self), 0.5);
1607 
1608   return self->xalign;
1609 }
1610 
1611 /**
1612  * adw_squeezer_set_xalign:
1613  * @self: a #AdwSqueezer
1614  * @xalign: the new xalign value, between 0 and 1
1615  *
1616  * Sets the #AdwSqueezer:xalign property for @self.
1617  *
1618  * Since: 1.0
1619  */
1620 void
adw_squeezer_set_xalign(AdwSqueezer * self,float xalign)1621 adw_squeezer_set_xalign (AdwSqueezer *self,
1622                          float        xalign)
1623 {
1624   g_return_if_fail (ADW_IS_SQUEEZER (self));
1625 
1626   xalign = CLAMP (xalign, 0.0, 1.0);
1627 
1628   if (self->xalign == xalign)
1629     return;
1630 
1631   self->xalign = xalign;
1632   gtk_widget_queue_draw (GTK_WIDGET (self));
1633   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_XALIGN]);
1634 }
1635 
1636 /**
1637  * adw_squeezer_get_yalign:
1638  * @self: a #AdwSqueezer
1639  *
1640  * Gets the #AdwSqueezer:yalign property for @self.
1641  *
1642  * Returns: the yalign property
1643  *
1644  * Since: 1.0
1645  */
1646 float
adw_squeezer_get_yalign(AdwSqueezer * self)1647 adw_squeezer_get_yalign (AdwSqueezer *self)
1648 {
1649   g_return_val_if_fail (ADW_IS_SQUEEZER (self), 0.5);
1650 
1651   return self->yalign;
1652 }
1653 
1654 /**
1655  * adw_squeezer_set_yalign:
1656  * @self: a #AdwSqueezer
1657  * @yalign: the new yalign value, between 0 and 1
1658  *
1659  * Sets the #AdwSqueezer:yalign property for @self.
1660  *
1661  * Since: 1.0
1662  */
1663 void
adw_squeezer_set_yalign(AdwSqueezer * self,float yalign)1664 adw_squeezer_set_yalign (AdwSqueezer *self,
1665                          float        yalign)
1666 {
1667   g_return_if_fail (ADW_IS_SQUEEZER (self));
1668 
1669   yalign = CLAMP (yalign, 0.0, 1.0);
1670 
1671   if (self->yalign == yalign)
1672     return;
1673 
1674   self->yalign = yalign;
1675   gtk_widget_queue_draw (GTK_WIDGET (self));
1676   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_YALIGN]);
1677 }
1678 
1679 /**
1680  * adw_squeezer_get_pages:
1681  * @self: a #AdwSqueezer
1682  *
1683  * Returns a #GListModel that contains the pages of the squeezer, and can be
1684  * used to keep an up-to-date view. The model also implements #GtkSelectionModel
1685  * and can be used to track the visible page.
1686  *
1687  * Returns: (transfer full): a #GtkSelectionModel for the squeezer's children
1688  *
1689  * Since: 1.0
1690  */
1691 GtkSelectionModel *
adw_squeezer_get_pages(AdwSqueezer * self)1692 adw_squeezer_get_pages (AdwSqueezer *self)
1693 {
1694   g_return_val_if_fail (ADW_IS_SQUEEZER (self), NULL);
1695 
1696   if (self->pages)
1697     return g_object_ref (self->pages);
1698 
1699   self->pages = GTK_SELECTION_MODEL (adw_squeezer_pages_new (self));
1700   g_object_add_weak_pointer (G_OBJECT (self->pages), (gpointer *) &self->pages);
1701 
1702   return self->pages;
1703 }
1704