1 /*
2  * Copyright (C) 2019 Zander Brown <zbrown@gnome.org>
3  * Copyright (C) 2019 Purism SPC
4  *
5  * SPDX-License-Identifier: LGPL-2.1-or-later
6  */
7 
8 #include "config.h"
9 
10 #include "adw-view-switcher-title.h"
11 #include "adw-squeezer.h"
12 #include "adw-window-title.h"
13 
14 /**
15  * AdwViewSwitcherTitle:
16  *
17  * A view switcher title.
18  *
19  * A widget letting you switch between multiple views contained by a
20  * [class@Adw.ViewStack] via an [class@Adw.ViewSwitcher].
21  *
22  * It is designed to be used as the title widget of a [class@Adw.HeaderBar], and
23  * will display the window's title when the window is too narrow to fit the view
24  * switcher e.g. on mobile phones, or if there are less than two views.
25  *
26  * You can conveniently bind the [property@Adw.ViewSwitcherBar:reveal] property
27  * to [property@Adw.ViewSwitcherTitle:title-visible] to automatically reveal the
28  * view switcher bar when the title label is displayed in place of the view
29  * switcher.
30  *
31  * An example of the UI definition for a common use case:
32  *
33  * ```xml
34  * <object class="GtkWindow"/>
35  *   <child type="titlebar">
36  *     <object class="AdwHeaderBar">
37  *       <property name="centering-policy">strict</property>
38  *       <child type="title">
39  *         <object class="AdwViewSwitcherTitle" id="title">
40  *           <property name="stack">stack</property>
41  *         </object>
42  *       </child>
43  *     </object>
44  *   </child>
45  *   <child>
46  *     <object class="GtkBox">
47  *       <child>
48  *         <object class="AdwViewStack" id="stack"/>
49  *       </child>
50  *       <child>
51  *         <object class="AdwViewSwitcherBar">
52  *           <property name="stack">stack</property>
53  *           <binding name="reveal">
54  *             <lookup name="title-visible">title</lookup>
55  *           </binding>
56  *         </object>
57  *       </child>
58  *     </object>
59  *   </child>
60  * </object>
61  * ```
62  *
63  * ## CSS nodes
64  *
65  * `AdwViewSwitcherTitle` has a single CSS node with name `viewswitchertitle`.
66  *
67  * Since: 1.0
68  */
69 
70 enum {
71   PROP_0,
72   PROP_POLICY,
73   PROP_STACK,
74   PROP_TITLE,
75   PROP_SUBTITLE,
76   PROP_VIEW_SWITCHER_ENABLED,
77   PROP_TITLE_VISIBLE,
78   LAST_PROP,
79 };
80 
81 struct _AdwViewSwitcherTitle
82 {
83   GtkWidget parent_instance;
84 
85   AdwSqueezer *squeezer;
86   AdwWindowTitle *title_widget;
87   AdwViewSwitcher *view_switcher;
88 
89   gboolean view_switcher_enabled;
90   GtkSelectionModel *pages;
91 };
92 
93 static GParamSpec *props[LAST_PROP];
94 
G_DEFINE_TYPE(AdwViewSwitcherTitle,adw_view_switcher_title,GTK_TYPE_WIDGET)95 G_DEFINE_TYPE (AdwViewSwitcherTitle, adw_view_switcher_title, GTK_TYPE_WIDGET)
96 
97 static void
98 update_view_switcher_visible (AdwViewSwitcherTitle *self)
99 {
100   AdwSqueezerPage *switcher_page;
101   int count = 0;
102 
103   if (!self->squeezer)
104     return;
105 
106   if (self->view_switcher_enabled && self->pages) {
107     guint i, n;
108 
109     n = g_list_model_get_n_items (G_LIST_MODEL (self->pages));
110     for (i = 0; i < n; i++) {
111       AdwViewStackPage *page = g_list_model_get_item (G_LIST_MODEL (self->pages), i);
112 
113       if (adw_view_stack_page_get_visible (page))
114         count++;
115     }
116   }
117 
118   switcher_page = adw_squeezer_get_page (self->squeezer, GTK_WIDGET (self->view_switcher));
119   adw_squeezer_page_set_enabled (switcher_page, count > 1);
120 }
121 
122 static void
notify_squeezer_visible_child_cb(GObject * self)123 notify_squeezer_visible_child_cb (GObject *self)
124 {
125   g_object_notify_by_pspec (self, props[PROP_TITLE_VISIBLE]);
126 }
127 
128 static void
adw_view_switcher_title_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)129 adw_view_switcher_title_get_property (GObject    *object,
130                                       guint       prop_id,
131                                       GValue     *value,
132                                       GParamSpec *pspec)
133 {
134   AdwViewSwitcherTitle *self = ADW_VIEW_SWITCHER_TITLE (object);
135 
136   switch (prop_id) {
137   case PROP_POLICY:
138     g_value_set_enum (value, adw_view_switcher_title_get_policy (self));
139     break;
140   case PROP_STACK:
141     g_value_set_object (value, adw_view_switcher_title_get_stack (self));
142     break;
143   case PROP_TITLE:
144     g_value_set_string (value, adw_view_switcher_title_get_title (self));
145     break;
146   case PROP_SUBTITLE:
147     g_value_set_string (value, adw_view_switcher_title_get_subtitle (self));
148     break;
149   case PROP_VIEW_SWITCHER_ENABLED:
150     g_value_set_boolean (value, adw_view_switcher_title_get_view_switcher_enabled (self));
151     break;
152   case PROP_TITLE_VISIBLE:
153     g_value_set_boolean (value, adw_view_switcher_title_get_title_visible (self));
154     break;
155   default:
156     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
157     break;
158   }
159 }
160 
161 static void
adw_view_switcher_title_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)162 adw_view_switcher_title_set_property (GObject      *object,
163                                       guint         prop_id,
164                                       const GValue *value,
165                                       GParamSpec   *pspec)
166 {
167   AdwViewSwitcherTitle *self = ADW_VIEW_SWITCHER_TITLE (object);
168 
169   switch (prop_id) {
170   case PROP_POLICY:
171     adw_view_switcher_title_set_policy (self, g_value_get_enum (value));
172     break;
173   case PROP_STACK:
174     adw_view_switcher_title_set_stack (self, g_value_get_object (value));
175     break;
176   case PROP_TITLE:
177     adw_view_switcher_title_set_title (self, g_value_get_string (value));
178     break;
179   case PROP_SUBTITLE:
180     adw_view_switcher_title_set_subtitle (self, g_value_get_string (value));
181     break;
182   case PROP_VIEW_SWITCHER_ENABLED:
183     adw_view_switcher_title_set_view_switcher_enabled (self, g_value_get_boolean (value));
184     break;
185   default:
186     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
187     break;
188   }
189 }
190 
191 static void
adw_view_switcher_title_dispose(GObject * object)192 adw_view_switcher_title_dispose (GObject *object) {
193   AdwViewSwitcherTitle *self = (AdwViewSwitcherTitle *)object;
194 
195   if (self->pages)
196     g_signal_handlers_disconnect_by_func (self->pages, G_CALLBACK (update_view_switcher_visible), self);
197 
198   if (self->squeezer)
199     gtk_widget_unparent (GTK_WIDGET (self->squeezer));
200 
201   G_OBJECT_CLASS (adw_view_switcher_title_parent_class)->dispose (object);
202 }
203 
204 static void
adw_view_switcher_title_class_init(AdwViewSwitcherTitleClass * klass)205 adw_view_switcher_title_class_init (AdwViewSwitcherTitleClass *klass)
206 {
207   GObjectClass *object_class = G_OBJECT_CLASS (klass);
208   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
209 
210   object_class->dispose = adw_view_switcher_title_dispose;
211   object_class->get_property = adw_view_switcher_title_get_property;
212   object_class->set_property = adw_view_switcher_title_set_property;
213 
214   /**
215    * AdwViewSwitcherTitle:policy: (attributes org.gtk.Property.get=adw_view_switcher_title_get_policy org.gtk.Property.set=adw_view_switcher_title_set_policy)
216    *
217    * The policy to determine which mode to use.
218    *
219    * Since: 1.0
220    */
221   props[PROP_POLICY] =
222     g_param_spec_enum ("policy",
223                        "Policy",
224                        "The policy to determine the mode to use",
225                        ADW_TYPE_VIEW_SWITCHER_POLICY,
226                        ADW_VIEW_SWITCHER_POLICY_AUTO,
227                        G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
228 
229   /**
230    * AdwViewSwitcherTitle:stack: (attributes org.gtk.Property.get=adw_view_switcher_title_get_stack org.gtk.Property.set=adw_view_switcher_title_set_stack)
231    *
232    * The stack the view switcher controls.
233    *
234    * Since: 1.0
235    */
236   props[PROP_STACK] =
237     g_param_spec_object ("stack",
238                          "Stack",
239                          "The stack the view switcher controls",
240                          ADW_TYPE_VIEW_STACK,
241                          G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
242 
243   /**
244    * AdwViewSwitcherTitle:title: (attributes org.gtk.Property.get=adw_view_switcher_title_get_title org.gtk.Property.set=adw_view_switcher_title_set_title)
245    *
246    * The title to display.
247    *
248    * The title should give a user additional details. A good title should not
249    * include the application name.
250    *
251    * Since: 1.0
252    */
253   props[PROP_TITLE] =
254     g_param_spec_string ("title",
255                          "Title",
256                          "The title to display",
257                          "",
258                          G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
259 
260   /**
261    * AdwViewSwitcherTitle:subtitle: (attributes org.gtk.Property.get=adw_view_switcher_title_get_subtitle org.gtk.Property.set=adw_view_switcher_title_set_subtitle)
262    *
263    * The subtitle to display.
264    *
265    * The subtitle should give a user additional details.
266    *
267    * Since: 1.0
268    */
269   props[PROP_SUBTITLE] =
270     g_param_spec_string ("subtitle",
271                          "Subtitle",
272                          "The subtitle to display",
273                          "",
274                          G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
275 
276   /**
277    * AdwViewSwitcherTitle:view-switcher-enabled: (attributes org.gtk.Property.get=adw_view_switcher_title_get_view_switcher_enabled org.gtk.Property.set=adw_view_switcher_title_set_view_switcher_enabled)
278    *
279    * Whether the view switcher is enabled.
280    *
281    * If it is disabled, the title will be displayed instead. This allows to
282    * programmatically hide the view switcher even if it fits in the available
283    * space.
284    *
285    * This can be used e.g. to ensure the view switcher is hidden below a certain
286    * window width, or any other constraint you find suitable.
287    *
288    * Since: 1.0
289    */
290   props[PROP_VIEW_SWITCHER_ENABLED] =
291     g_param_spec_boolean ("view-switcher-enabled",
292                          "View switcher enabled",
293                          "Whether the view switcher is enabled",
294                          TRUE,
295                          G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
296 
297   /**
298    * AdwViewSwitcherTitle:title-visible: (attributes org.gtk.Property.get=adw_view_switcher_title_get_title_visible)
299    *
300    * Whether the title is currently visible.
301    *
302    * If the title is visible, it means the view switcher is hidden an it may be
303    * wanted to show an alternative switcher, e.g. a [class@Adw.ViewSwitcherBar].
304    *
305    * Since: 1.0
306    */
307   props[PROP_TITLE_VISIBLE] =
308     g_param_spec_boolean ("title-visible",
309                          "Title visible",
310                          "Whether the title is currently visible",
311                          TRUE,
312                          G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
313 
314   g_object_class_install_properties (object_class, LAST_PROP, props);
315 
316   gtk_widget_class_set_css_name (widget_class, "viewswitchertitle");
317   gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
318 
319   gtk_widget_class_set_template_from_resource (widget_class,
320                                                "/org/gnome/Adwaita/ui/adw-view-switcher-title.ui");
321   gtk_widget_class_bind_template_child (widget_class, AdwViewSwitcherTitle, squeezer);
322   gtk_widget_class_bind_template_child (widget_class, AdwViewSwitcherTitle, title_widget);
323   gtk_widget_class_bind_template_child (widget_class, AdwViewSwitcherTitle, view_switcher);
324   gtk_widget_class_bind_template_callback (widget_class, notify_squeezer_visible_child_cb);
325 }
326 
327 static void
adw_view_switcher_title_init(AdwViewSwitcherTitle * self)328 adw_view_switcher_title_init (AdwViewSwitcherTitle *self)
329 {
330   /* This must be initialized before the template so the embedded view switcher
331    * can pick up the correct default value.
332    */
333   self->view_switcher_enabled = TRUE;
334 
335   gtk_widget_init_template (GTK_WIDGET (self));
336 
337   update_view_switcher_visible (self);
338 }
339 
340 /**
341  * adw_view_switcher_title_new:
342  *
343  * Creates a new `AdwViewSwitcherTitle`.
344  *
345  * Returns: the newly created `AdwViewSwitcherTitle`
346  *
347  * Since: 1.0
348  */
349 GtkWidget *
adw_view_switcher_title_new(void)350 adw_view_switcher_title_new (void)
351 {
352   return g_object_new (ADW_TYPE_VIEW_SWITCHER_TITLE, NULL);
353 }
354 
355 /**
356  * adw_view_switcher_title_get_policy: (attributes org.gtk.Method.get_property=policy)
357  * @self: a `AdwViewSwitcherTitle`
358  *
359  * Gets the policy of @self.
360  *
361  * Returns: the policy of @self
362  *
363  * Since: 1.0
364  */
365 AdwViewSwitcherPolicy
adw_view_switcher_title_get_policy(AdwViewSwitcherTitle * self)366 adw_view_switcher_title_get_policy (AdwViewSwitcherTitle *self)
367 {
368   g_return_val_if_fail (ADW_IS_VIEW_SWITCHER_TITLE (self), ADW_VIEW_SWITCHER_POLICY_NARROW);
369 
370   return adw_view_switcher_get_policy (self->view_switcher);
371 }
372 
373 /**
374  * adw_view_switcher_title_set_policy: (attributes org.gtk.Method.set_property=policy)
375  * @self: a `AdwViewSwitcherTitle`
376  * @policy: the new policy
377  *
378  * Sets the policy of @self.
379  *
380  * Since: 1.0
381  */
382 void
adw_view_switcher_title_set_policy(AdwViewSwitcherTitle * self,AdwViewSwitcherPolicy policy)383 adw_view_switcher_title_set_policy (AdwViewSwitcherTitle  *self,
384                                     AdwViewSwitcherPolicy  policy)
385 {
386   g_return_if_fail (ADW_IS_VIEW_SWITCHER_TITLE (self));
387 
388   if (adw_view_switcher_get_policy (self->view_switcher) == policy)
389     return;
390 
391   adw_view_switcher_set_policy (self->view_switcher, policy);
392 
393   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_POLICY]);
394 
395   gtk_widget_queue_resize (GTK_WIDGET (self));
396 }
397 
398 /**
399  * adw_view_switcher_title_get_stack: (attributes org.gtk.Method.get_property=stack)
400  * @self: a `AdwViewSwitcherTitle`
401  *
402  * Gets the stack controlled by @self.
403  *
404  * Returns: (nullable) (transfer none): the stack
405  *
406  * Since: 1.0
407  */
408 AdwViewStack *
adw_view_switcher_title_get_stack(AdwViewSwitcherTitle * self)409 adw_view_switcher_title_get_stack (AdwViewSwitcherTitle *self)
410 {
411   g_return_val_if_fail (ADW_IS_VIEW_SWITCHER_TITLE (self), NULL);
412 
413   return adw_view_switcher_get_stack (self->view_switcher);
414 }
415 
416 /**
417  * adw_view_switcher_title_set_stack: (attributes org.gtk.Method.set_property=stack)
418  * @self: a `AdwViewSwitcherTitle`
419  * @stack: (nullable): a stack
420  *
421  * Sets the stack controlled by @self.
422  *
423  * Since: 1.0
424  */
425 void
adw_view_switcher_title_set_stack(AdwViewSwitcherTitle * self,AdwViewStack * stack)426 adw_view_switcher_title_set_stack (AdwViewSwitcherTitle *self,
427                                    AdwViewStack         *stack)
428 {
429   AdwViewStack *previous_stack;
430 
431   g_return_if_fail (ADW_IS_VIEW_SWITCHER_TITLE (self));
432   g_return_if_fail (stack == NULL || ADW_IS_VIEW_STACK (stack));
433 
434   previous_stack = adw_view_switcher_get_stack (self->view_switcher);
435 
436   if (previous_stack == stack)
437     return;
438 
439   if (previous_stack) {
440     g_signal_handlers_disconnect_by_func (self->pages, G_CALLBACK (update_view_switcher_visible), self);
441     g_clear_object (&self->pages);
442   }
443 
444   adw_view_switcher_set_stack (self->view_switcher, stack);
445 
446   if (stack) {
447     self->pages = adw_view_stack_get_pages (stack);
448 
449     g_signal_connect_swapped (self->pages, "items-changed", G_CALLBACK (update_view_switcher_visible), self);
450   }
451 
452   update_view_switcher_visible (self);
453 
454   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_STACK]);
455 }
456 
457 /**
458  * adw_view_switcher_title_get_title: (attributes org.gtk.Method.get_property=title)
459  * @self: a `AdwViewSwitcherTitle`
460  *
461  * Gets the title of @self.
462  *
463  * Returns: the title
464  *
465  * Since: 1.0
466  */
467 const char *
adw_view_switcher_title_get_title(AdwViewSwitcherTitle * self)468 adw_view_switcher_title_get_title (AdwViewSwitcherTitle *self)
469 {
470   g_return_val_if_fail (ADW_IS_VIEW_SWITCHER_TITLE (self), NULL);
471 
472   return adw_window_title_get_title (self->title_widget);
473 }
474 
475 /**
476  * adw_view_switcher_title_set_title: (attributes org.gtk.Method.set_property=title)
477  * @self: a `AdwViewSwitcherTitle`
478  * @title: a title
479  *
480  * Sets the title of @self.
481  *
482  * Since: 1.0
483  */
484 void
adw_view_switcher_title_set_title(AdwViewSwitcherTitle * self,const char * title)485 adw_view_switcher_title_set_title (AdwViewSwitcherTitle *self,
486                                    const char           *title)
487 {
488   g_return_if_fail (ADW_IS_VIEW_SWITCHER_TITLE (self));
489 
490   if (g_strcmp0 (adw_window_title_get_title (self->title_widget), title) == 0)
491     return;
492 
493   adw_window_title_set_title (self->title_widget, title);
494 
495   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]);
496 }
497 
498 /**
499  * adw_view_switcher_title_get_subtitle: (attributes org.gtk.Method.get_property=subtitle)
500  * @self: a `AdwViewSwitcherTitle`
501  *
502  * Gets the subtitle of @self.
503  *
504  * Returns: the subtitle
505  *
506  * Since: 1.0
507  */
508 const char *
adw_view_switcher_title_get_subtitle(AdwViewSwitcherTitle * self)509 adw_view_switcher_title_get_subtitle (AdwViewSwitcherTitle *self)
510 {
511   g_return_val_if_fail (ADW_IS_VIEW_SWITCHER_TITLE (self), NULL);
512 
513   return adw_window_title_get_subtitle (self->title_widget);
514 }
515 
516 /**
517  * adw_view_switcher_title_set_subtitle: (attributes org.gtk.Method.get_property=subtitle)
518  * @self: a `AdwViewSwitcherTitle`
519  * @subtitle: a subtitle
520  *
521  * Sets the subtitle of @self.
522  *
523  * Since: 1.0
524  */
525 void
adw_view_switcher_title_set_subtitle(AdwViewSwitcherTitle * self,const char * subtitle)526 adw_view_switcher_title_set_subtitle (AdwViewSwitcherTitle *self,
527                                       const char           *subtitle)
528 {
529   g_return_if_fail (ADW_IS_VIEW_SWITCHER_TITLE (self));
530 
531   if (g_strcmp0 (adw_window_title_get_subtitle (self->title_widget), subtitle) == 0)
532     return;
533 
534   adw_window_title_set_subtitle (self->title_widget, subtitle);
535 
536   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SUBTITLE]);
537 }
538 
539 /**
540  * adw_view_switcher_title_get_view_switcher_enabled: (attributes org.gtk.Method.get_property=view-switcher-enabled)
541  * @self: a `AdwViewSwitcherTitle`
542  *
543  * Gets whether @self's view switcher is enabled.
544  *
545  * Returns: whether the view switcher is enabled
546  *
547  * Since: 1.0
548  */
549 gboolean
adw_view_switcher_title_get_view_switcher_enabled(AdwViewSwitcherTitle * self)550 adw_view_switcher_title_get_view_switcher_enabled (AdwViewSwitcherTitle *self)
551 {
552   g_return_val_if_fail (ADW_IS_VIEW_SWITCHER_TITLE (self), FALSE);
553 
554   return self->view_switcher_enabled;
555 }
556 
557 /**
558  * adw_view_switcher_title_set_view_switcher_enabled: (attributes org.gtk.Method.set_property=view-switcher-enabled)
559  * @self: a `AdwViewSwitcherTitle`
560  * @enabled: whether the view switcher is enabled
561  *
562  * Sets whether @self's view switcher is enabled.
563  *
564  * Since: 1.0
565  */
566 void
adw_view_switcher_title_set_view_switcher_enabled(AdwViewSwitcherTitle * self,gboolean enabled)567 adw_view_switcher_title_set_view_switcher_enabled (AdwViewSwitcherTitle *self,
568                                                    gboolean              enabled)
569 {
570   g_return_if_fail (ADW_IS_VIEW_SWITCHER_TITLE (self));
571 
572   enabled = !!enabled;
573 
574   if (self->view_switcher_enabled == enabled)
575     return;
576 
577   self->view_switcher_enabled = enabled;
578   update_view_switcher_visible (self);
579 
580   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VIEW_SWITCHER_ENABLED]);
581 }
582 
583 /**
584  * adw_view_switcher_title_get_title_visible: (attributes org.gtk.Method.get_property=title-visible)
585  * @self: a `AdwViewSwitcherTitle`
586  *
587  * Gets whether the title of @self is currently visible.
588  *
589  * Returns: whether the title of @self is currently visible
590  *
591  * Since: 1.0
592  */
593 gboolean
adw_view_switcher_title_get_title_visible(AdwViewSwitcherTitle * self)594 adw_view_switcher_title_get_title_visible (AdwViewSwitcherTitle *self)
595 {
596   g_return_val_if_fail (ADW_IS_VIEW_SWITCHER_TITLE (self), FALSE);
597 
598   return adw_squeezer_get_visible_child (self->squeezer) == GTK_WIDGET (self->title_widget);
599 }
600