1 /*
2  * Copyright (C) 2019 Zander Brown <zbrown@gnome.org>
3  * Copyright (C) 2019 Purism SPC
4  *
5  * SPDX-License-Identifier: LGPL-2.1+
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  * SECTION:adwviewswitchertitle
16  * @short_description: A view switcher title.
17  * @title: AdwViewSwitcherTitle
18  * @See_also: #AdwHeaderBar, #AdwViewSwitcher, #AdwViewSwitcherBar
19  *
20  * A widget letting you switch between multiple views offered by a #GtkStack,
21  * via an #AdwViewSwitcher. It is designed to be used as the title widget of a
22  * #AdwHeaderBar, and will display the window's title when the window is too
23  * narrow to fit the view switcher e.g. on mobile phones, or if there are less
24  * than two views.
25  *
26  * You can conveniently bind the #AdwViewSwitcherBar:reveal property to
27  * #AdwViewSwitcherTitle:title-visible to automatically reveal the view switcher
28  * bar when the title label is displayed in place of the view switcher.
29  *
30  * An example of the UI definition for a common use case:
31  * |[
32  * <object class="GtkWindow"/>
33  *   <child type="titlebar">
34  *     <object class="AdwHeaderBar">
35  *       <property name="centering-policy">strict</property>
36  *       <child type="title">
37  *         <object class="AdwViewSwitcherTitle"
38  *                 id="view_switcher_title">
39  *           <property name="stack">stack</property>
40  *         </object>
41  *       </child>
42  *     </object>
43  *   </child>
44  *   <child>
45  *     <object class="GtkBox">
46  *       <child>
47  *         <object class="GtkStack" id="stack"/>
48  *       </child>
49  *       <child>
50  *         <object class="AdwViewSwitcherBar">
51  *           <property name="stack">stack</property>
52  *           <property name="reveal"
53  *                     bind-source="view_switcher_title"
54  *                     bind-property="title-visible"
55  *                     bind-flags="sync-create"/>
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       GtkStackPage *page = g_list_model_get_item (G_LIST_MODEL (self->pages), i);
112 
113       if (gtk_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:
216    *
217    * The #AdwViewSwitcherPolicy the #AdwViewSwitcher should use to determine
218    * which mode to use.
219    *
220    * Since: 1.0
221    */
222   props[PROP_POLICY] =
223     g_param_spec_enum ("policy",
224                        "Policy",
225                        "The policy to determine the mode to use",
226                        ADW_TYPE_VIEW_SWITCHER_POLICY, ADW_VIEW_SWITCHER_POLICY_AUTO,
227                        G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
228 
229   /**
230    * AdwViewSwitcherTitle:stack:
231    *
232    * The #GtkStack the #AdwViewSwitcher controls.
233    *
234    * Since: 1.0
235    */
236   props[PROP_STACK] =
237     g_param_spec_object ("stack",
238                          "Stack",
239                          "Stack",
240                          GTK_TYPE_STACK,
241                          G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
242 
243   /**
244    * AdwViewSwitcherTitle:title:
245    *
246    * The title of the #AdwViewSwitcher.
247    *
248    * Since: 1.0
249    */
250   props[PROP_TITLE] =
251     g_param_spec_string ("title",
252                          "Title",
253                          "The title to display",
254                          NULL,
255                          G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
256 
257   /**
258    * AdwViewSwitcherTitle:subtitle:
259    *
260    * The subtitle of the #AdwViewSwitcher.
261    *
262    * Since: 1.0
263    */
264   props[PROP_SUBTITLE] =
265     g_param_spec_string ("subtitle",
266                          "Subtitle",
267                          "The subtitle to display",
268                          NULL,
269                          G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
270 
271   /**
272    * AdwViewSwitcherTitle:view-switcher-enabled:
273    *
274    * Whether the bar should be revealed or hidden.
275    *
276    * Since: 1.0
277    */
278   props[PROP_VIEW_SWITCHER_ENABLED] =
279     g_param_spec_boolean ("view-switcher-enabled",
280                          "View switcher enabled",
281                          "Whether the view switcher is enabled",
282                          TRUE,
283                          G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
284 
285   /**
286    * AdwViewSwitcherTitle:title-visible:
287    *
288    * Whether the bar should be revealed or hidden.
289    *
290    * Since: 1.0
291    */
292   props[PROP_TITLE_VISIBLE] =
293     g_param_spec_boolean ("title-visible",
294                          "Title visible",
295                          "Whether the title label is visible",
296                          TRUE,
297                          G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
298 
299   g_object_class_install_properties (object_class, LAST_PROP, props);
300 
301   gtk_widget_class_set_css_name (widget_class, "viewswitchertitle");
302   gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
303 
304   gtk_widget_class_set_template_from_resource (widget_class,
305                                                "/org/gnome/Adwaita/ui/adw-view-switcher-title.ui");
306   gtk_widget_class_bind_template_child (widget_class, AdwViewSwitcherTitle, squeezer);
307   gtk_widget_class_bind_template_child (widget_class, AdwViewSwitcherTitle, title_widget);
308   gtk_widget_class_bind_template_child (widget_class, AdwViewSwitcherTitle, view_switcher);
309   gtk_widget_class_bind_template_callback (widget_class, notify_squeezer_visible_child_cb);
310 }
311 
312 static void
adw_view_switcher_title_init(AdwViewSwitcherTitle * self)313 adw_view_switcher_title_init (AdwViewSwitcherTitle *self)
314 {
315   /* This must be initialized before the template so the embedded view switcher
316    * can pick up the correct default value.
317    */
318   self->view_switcher_enabled = TRUE;
319 
320   gtk_widget_init_template (GTK_WIDGET (self));
321 
322   update_view_switcher_visible (self);
323 }
324 
325 /**
326  * adw_view_switcher_title_new:
327  *
328  * Creates a new #AdwViewSwitcherTitle widget.
329  *
330  * Returns: a new #AdwViewSwitcherTitle
331  *
332  * Since: 1.0
333  */
334 GtkWidget *
adw_view_switcher_title_new(void)335 adw_view_switcher_title_new (void)
336 {
337   return g_object_new (ADW_TYPE_VIEW_SWITCHER_TITLE, NULL);
338 }
339 
340 /**
341  * adw_view_switcher_title_get_policy:
342  * @self: a #AdwViewSwitcherTitle
343  *
344  * Gets the policy of @self.
345  *
346  * Returns: the policy of @self
347  *
348  * Since: 1.0
349  */
350 AdwViewSwitcherPolicy
adw_view_switcher_title_get_policy(AdwViewSwitcherTitle * self)351 adw_view_switcher_title_get_policy (AdwViewSwitcherTitle *self)
352 {
353   g_return_val_if_fail (ADW_IS_VIEW_SWITCHER_TITLE (self), ADW_VIEW_SWITCHER_POLICY_NARROW);
354 
355   return adw_view_switcher_get_policy (self->view_switcher);
356 }
357 
358 /**
359  * adw_view_switcher_title_set_policy:
360  * @self: a #AdwViewSwitcherTitle
361  * @policy: the new policy
362  *
363  * Sets the policy of @self.
364  *
365  * Since: 1.0
366  */
367 void
adw_view_switcher_title_set_policy(AdwViewSwitcherTitle * self,AdwViewSwitcherPolicy policy)368 adw_view_switcher_title_set_policy (AdwViewSwitcherTitle  *self,
369                                     AdwViewSwitcherPolicy  policy)
370 {
371   g_return_if_fail (ADW_IS_VIEW_SWITCHER_TITLE (self));
372 
373   if (adw_view_switcher_get_policy (self->view_switcher) == policy)
374     return;
375 
376   adw_view_switcher_set_policy (self->view_switcher, policy);
377 
378   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_POLICY]);
379 
380   gtk_widget_queue_resize (GTK_WIDGET (self));
381 }
382 
383 /**
384  * adw_view_switcher_title_get_stack:
385  * @self: a #AdwViewSwitcherTitle
386  *
387  * Get the #GtkStack being controlled by the #AdwViewSwitcher.
388  *
389  * Returns: (nullable) (transfer none): the #GtkStack, or %NULL if none has been set
390  *
391  * Since: 1.0
392  */
393 GtkStack *
adw_view_switcher_title_get_stack(AdwViewSwitcherTitle * self)394 adw_view_switcher_title_get_stack (AdwViewSwitcherTitle *self)
395 {
396   g_return_val_if_fail (ADW_IS_VIEW_SWITCHER_TITLE (self), NULL);
397 
398   return adw_view_switcher_get_stack (self->view_switcher);
399 }
400 
401 /**
402  * adw_view_switcher_title_set_stack:
403  * @self: a #AdwViewSwitcherTitle
404  * @stack: (nullable): a #GtkStack
405  *
406  * Sets the #GtkStack to control.
407  *
408  * Since: 1.0
409  */
410 void
adw_view_switcher_title_set_stack(AdwViewSwitcherTitle * self,GtkStack * stack)411 adw_view_switcher_title_set_stack (AdwViewSwitcherTitle *self,
412                                    GtkStack             *stack)
413 {
414   GtkStack *previous_stack;
415 
416   g_return_if_fail (ADW_IS_VIEW_SWITCHER_TITLE (self));
417   g_return_if_fail (stack == NULL || GTK_IS_STACK (stack));
418 
419   previous_stack = adw_view_switcher_get_stack (self->view_switcher);
420 
421   if (previous_stack == stack)
422     return;
423 
424   if (previous_stack) {
425     g_signal_handlers_disconnect_by_func (self->pages, G_CALLBACK (update_view_switcher_visible), self);
426     g_clear_object (&self->pages);
427   }
428 
429   adw_view_switcher_set_stack (self->view_switcher, stack);
430 
431   if (stack) {
432     self->pages = gtk_stack_get_pages (stack);
433 
434     g_signal_connect_swapped (self->pages, "items-changed", G_CALLBACK (update_view_switcher_visible), self);
435   }
436 
437   update_view_switcher_visible (self);
438 
439   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_STACK]);
440 }
441 
442 /**
443  * adw_view_switcher_title_get_title:
444  * @self: a #AdwViewSwitcherTitle
445  *
446  * Gets the title of @self. See adw_view_switcher_title_set_title().
447  *
448  * Returns: (transfer none) (nullable): the title of @self, or %NULL.
449  *
450  * Since: 1.0
451  */
452 const char *
adw_view_switcher_title_get_title(AdwViewSwitcherTitle * self)453 adw_view_switcher_title_get_title (AdwViewSwitcherTitle *self)
454 {
455   g_return_val_if_fail (ADW_IS_VIEW_SWITCHER_TITLE (self), NULL);
456 
457   return adw_window_title_get_title (self->title_widget);
458 }
459 
460 /**
461  * adw_view_switcher_title_set_title:
462  * @self: a #AdwViewSwitcherTitle
463  * @title: (nullable): a title, or %NULL
464  *
465  * Sets the title of @self. The title should give a user additional details. A
466  * good title should not include the application name.
467  *
468  * Since: 1.0
469  */
470 void
adw_view_switcher_title_set_title(AdwViewSwitcherTitle * self,const char * title)471 adw_view_switcher_title_set_title (AdwViewSwitcherTitle *self,
472                                    const char           *title)
473 {
474   g_return_if_fail (ADW_IS_VIEW_SWITCHER_TITLE (self));
475 
476   if (g_strcmp0 (adw_window_title_get_title (self->title_widget), title) == 0)
477     return;
478 
479   adw_window_title_set_title (self->title_widget, title);
480 
481   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]);
482 }
483 
484 /**
485  * adw_view_switcher_title_get_subtitle:
486  * @self: a #AdwViewSwitcherTitle
487  *
488  * Gets the subtitle of @self. See adw_view_switcher_title_set_subtitle().
489  *
490  * Returns: (transfer none) (nullable): the subtitle of @self, or %NULL.
491  *
492  * Since: 1.0
493  */
494 const char *
adw_view_switcher_title_get_subtitle(AdwViewSwitcherTitle * self)495 adw_view_switcher_title_get_subtitle (AdwViewSwitcherTitle *self)
496 {
497   g_return_val_if_fail (ADW_IS_VIEW_SWITCHER_TITLE (self), NULL);
498 
499   return adw_window_title_get_subtitle (self->title_widget);
500 }
501 
502 /**
503  * adw_view_switcher_title_set_subtitle:
504  * @self: a #AdwViewSwitcherTitle
505  * @subtitle: (nullable): a subtitle, or %NULL
506  *
507  * Sets the subtitle of @self. The subtitle should give a user additional
508  * details.
509  *
510  * Since: 1.0
511  */
512 void
adw_view_switcher_title_set_subtitle(AdwViewSwitcherTitle * self,const char * subtitle)513 adw_view_switcher_title_set_subtitle (AdwViewSwitcherTitle *self,
514                                       const char           *subtitle)
515 {
516   g_return_if_fail (ADW_IS_VIEW_SWITCHER_TITLE (self));
517 
518   if (g_strcmp0 (adw_window_title_get_subtitle (self->title_widget), subtitle) == 0)
519     return;
520 
521   adw_window_title_set_subtitle (self->title_widget, subtitle);
522 
523   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SUBTITLE]);
524 }
525 
526 /**
527  * adw_view_switcher_title_get_view_switcher_enabled:
528  * @self: a #AdwViewSwitcherTitle
529  *
530  * Gets whether @self's view switcher is enabled.
531  *
532  * See adw_view_switcher_title_set_view_switcher_enabled().
533  *
534  * Returns: %TRUE if the view switcher is enabled, %FALSE otherwise.
535  *
536  * Since: 1.0
537  */
538 gboolean
adw_view_switcher_title_get_view_switcher_enabled(AdwViewSwitcherTitle * self)539 adw_view_switcher_title_get_view_switcher_enabled (AdwViewSwitcherTitle *self)
540 {
541   g_return_val_if_fail (ADW_IS_VIEW_SWITCHER_TITLE (self), FALSE);
542 
543   return self->view_switcher_enabled;
544 }
545 
546 /**
547  * adw_view_switcher_title_set_view_switcher_enabled:
548  * @self: a #AdwViewSwitcherTitle
549  * @enabled: %TRUE to enable the view switcher, %FALSE to disable it
550  *
551  * Make @self enable or disable its view switcher. If it is disabled, the title
552  * will be displayed instead. This allows to programmatically and prematurely
553  * hide the view switcher of @self even if it fits in the available space.
554  *
555  * This can be used e.g. to ensure the view switcher is hidden below a certain
556  * window width, or any other constraint you find suitable.
557  *
558  * Since: 1.0
559  */
560 void
adw_view_switcher_title_set_view_switcher_enabled(AdwViewSwitcherTitle * self,gboolean enabled)561 adw_view_switcher_title_set_view_switcher_enabled (AdwViewSwitcherTitle *self,
562                                                    gboolean              enabled)
563 {
564   g_return_if_fail (ADW_IS_VIEW_SWITCHER_TITLE (self));
565 
566   enabled = !!enabled;
567 
568   if (self->view_switcher_enabled == enabled)
569     return;
570 
571   self->view_switcher_enabled = enabled;
572   update_view_switcher_visible (self);
573 
574   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VIEW_SWITCHER_ENABLED]);
575 }
576 
577 /**
578  * adw_view_switcher_title_get_title_visible:
579  * @self: a #AdwViewSwitcherTitle
580  *
581  * Get whether the title label of @self is visible.
582  *
583  * Returns: %TRUE if the title label of @self is visible, %FALSE if not.
584  *
585  * Since: 1.0
586  */
587 gboolean
adw_view_switcher_title_get_title_visible(AdwViewSwitcherTitle * self)588 adw_view_switcher_title_get_title_visible (AdwViewSwitcherTitle *self)
589 {
590   g_return_val_if_fail (ADW_IS_VIEW_SWITCHER_TITLE (self), FALSE);
591 
592   return adw_squeezer_get_visible_child (self->squeezer) == GTK_WIDGET (self->title_widget);
593 }
594