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 #include <glib/gi18n-lib.h>
10 
11 #include "hdy-view-switcher-title.h"
12 #include "hdy-squeezer.h"
13 
14 /**
15  * SECTION:hdy-view-switcher-title
16  * @short_description: A view switcher title.
17  * @title: HdyViewSwitcherTitle
18  * @See_also: #HdyHeaderBar, #HdyViewSwitcher, #HdyViewSwitcherBar
19  *
20  * A widget letting you switch between multiple views offered by a #GtkStack,
21  * via an #HdyViewSwitcher. It is designed to be used as the title widget of a
22  * #HdyHeaderBar, 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 #HdyViewSwitcherBar:reveal property to
27  * #HdyViewSwitcherTitle: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="HdyHeaderBar">
35  *       <property name="centering-policy">strict</property>
36  *       <child type="title">
37  *         <object class="HdyViewSwitcherTitle"
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="HdyViewSwitcherBar">
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  * #HdyViewSwitcherTitle 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 _HdyViewSwitcherTitle
82 {
83   GtkBin parent_instance;
84 
85   HdySqueezer *squeezer;
86   GtkLabel *subtitle_label;
87   GtkBox *title_box;
88   GtkLabel *title_label;
89   HdyViewSwitcher *view_switcher;
90 
91   gboolean view_switcher_enabled;
92 };
93 
94 static GParamSpec *props[LAST_PROP];
95 
G_DEFINE_TYPE(HdyViewSwitcherTitle,hdy_view_switcher_title,GTK_TYPE_BIN)96 G_DEFINE_TYPE (HdyViewSwitcherTitle, hdy_view_switcher_title, GTK_TYPE_BIN)
97 
98 static void
99 update_subtitle_label (HdyViewSwitcherTitle *self)
100 {
101   const gchar *subtitle = gtk_label_get_label (self->subtitle_label);
102 
103   gtk_widget_set_visible (GTK_WIDGET (self->subtitle_label), subtitle && subtitle[0]);
104 
105   gtk_widget_queue_resize (GTK_WIDGET (self));
106 }
107 
108 static void
count_children_cb(GtkWidget * widget,gint * count)109 count_children_cb (GtkWidget *widget,
110                    gint      *count)
111 {
112   (*count)++;
113 }
114 
115 static void
update_view_switcher_visible(HdyViewSwitcherTitle * self)116 update_view_switcher_visible (HdyViewSwitcherTitle *self)
117 {
118   GtkStack *stack = hdy_view_switcher_get_stack (self->view_switcher);
119   gint count = 0;
120 
121   if (self->view_switcher_enabled && stack)
122     gtk_container_foreach (GTK_CONTAINER (stack), (GtkCallback) count_children_cb, &count);
123 
124   hdy_squeezer_set_child_enabled (self->squeezer, GTK_WIDGET (self->view_switcher), count > 1);
125 }
126 
127 static void
notify_squeezer_visible_child_cb(GObject * self)128 notify_squeezer_visible_child_cb (GObject *self)
129 {
130   g_object_notify_by_pspec (self, props[PROP_TITLE_VISIBLE]);
131 }
132 
133 static void
hdy_view_switcher_title_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)134 hdy_view_switcher_title_get_property (GObject    *object,
135                                       guint       prop_id,
136                                       GValue     *value,
137                                       GParamSpec *pspec)
138 {
139   HdyViewSwitcherTitle *self = HDY_VIEW_SWITCHER_TITLE (object);
140 
141   switch (prop_id) {
142   case PROP_POLICY:
143     g_value_set_enum (value, hdy_view_switcher_title_get_policy (self));
144     break;
145   case PROP_STACK:
146     g_value_set_object (value, hdy_view_switcher_title_get_stack (self));
147     break;
148   case PROP_TITLE:
149     g_value_set_string (value, hdy_view_switcher_title_get_title (self));
150     break;
151   case PROP_SUBTITLE:
152     g_value_set_string (value, hdy_view_switcher_title_get_subtitle (self));
153     break;
154   case PROP_VIEW_SWITCHER_ENABLED:
155     g_value_set_boolean (value, hdy_view_switcher_title_get_view_switcher_enabled (self));
156     break;
157   case PROP_TITLE_VISIBLE:
158     g_value_set_boolean (value, hdy_view_switcher_title_get_title_visible (self));
159     break;
160   default:
161     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
162     break;
163   }
164 }
165 
166 static void
hdy_view_switcher_title_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)167 hdy_view_switcher_title_set_property (GObject      *object,
168                                       guint         prop_id,
169                                       const GValue *value,
170                                       GParamSpec   *pspec)
171 {
172   HdyViewSwitcherTitle *self = HDY_VIEW_SWITCHER_TITLE (object);
173 
174   switch (prop_id) {
175   case PROP_POLICY:
176     hdy_view_switcher_title_set_policy (self, g_value_get_enum (value));
177     break;
178   case PROP_STACK:
179     hdy_view_switcher_title_set_stack (self, g_value_get_object (value));
180     break;
181   case PROP_TITLE:
182     hdy_view_switcher_title_set_title (self, g_value_get_string (value));
183     break;
184   case PROP_SUBTITLE:
185     hdy_view_switcher_title_set_subtitle (self, g_value_get_string (value));
186     break;
187   case PROP_VIEW_SWITCHER_ENABLED:
188     hdy_view_switcher_title_set_view_switcher_enabled (self, g_value_get_boolean (value));
189     break;
190   default:
191     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
192     break;
193   }
194 }
195 
196 static void
hdy_view_switcher_title_dispose(GObject * object)197 hdy_view_switcher_title_dispose (GObject *object) {
198   HdyViewSwitcherTitle *self = (HdyViewSwitcherTitle *)object;
199 
200   if (self->view_switcher) {
201     GtkStack *stack = hdy_view_switcher_get_stack (self->view_switcher);
202 
203     if (stack)
204       g_signal_handlers_disconnect_by_func (stack, G_CALLBACK (update_view_switcher_visible), self);
205   }
206 
207   G_OBJECT_CLASS (hdy_view_switcher_title_parent_class)->dispose (object);
208 }
209 
210 static void
hdy_view_switcher_title_class_init(HdyViewSwitcherTitleClass * klass)211 hdy_view_switcher_title_class_init (HdyViewSwitcherTitleClass *klass)
212 {
213   GObjectClass *object_class = G_OBJECT_CLASS (klass);
214   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
215 
216   object_class->dispose = hdy_view_switcher_title_dispose;
217   object_class->get_property = hdy_view_switcher_title_get_property;
218   object_class->set_property = hdy_view_switcher_title_set_property;
219 
220   /**
221    * HdyViewSwitcherTitle:policy:
222    *
223    * The #HdyViewSwitcherPolicy the #HdyViewSwitcher should use to determine
224    * which mode to use.
225    *
226    * Since: 1.0
227    */
228   props[PROP_POLICY] =
229     g_param_spec_enum ("policy",
230                        _("Policy"),
231                        _("The policy to determine the mode to use"),
232                        HDY_TYPE_VIEW_SWITCHER_POLICY, HDY_VIEW_SWITCHER_POLICY_AUTO,
233                        G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
234 
235   /**
236    * HdyViewSwitcherTitle:stack:
237    *
238    * The #GtkStack the #HdyViewSwitcher controls.
239    *
240    * Since: 1.0
241    */
242   props[PROP_STACK] =
243     g_param_spec_object ("stack",
244                          _("Stack"),
245                          _("Stack"),
246                          GTK_TYPE_STACK,
247                          G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
248 
249   /**
250    * HdyViewSwitcherTitle:title:
251    *
252    * The title of the #HdyViewSwitcher.
253    *
254    * Since: 1.0
255    */
256   props[PROP_TITLE] =
257     g_param_spec_string ("title",
258                          _("Title"),
259                          _("The title to display"),
260                          NULL,
261                          G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
262 
263   /**
264    * HdyViewSwitcherTitle:subtitle:
265    *
266    * The subtitle of the #HdyViewSwitcher.
267    *
268    * Since: 1.0
269    */
270   props[PROP_SUBTITLE] =
271     g_param_spec_string ("subtitle",
272                          _("Subtitle"),
273                          _("The subtitle to display"),
274                          NULL,
275                          G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
276 
277   /**
278    * HdyViewSwitcherTitle:view-switcher-enabled:
279    *
280    * Whether the bar should be revealed or hidden.
281    *
282    * Since: 1.0
283    */
284   props[PROP_VIEW_SWITCHER_ENABLED] =
285     g_param_spec_boolean ("view-switcher-enabled",
286                          _("View switcher enabled"),
287                          _("Whether the view switcher is enabled"),
288                          TRUE,
289                          G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
290 
291   /**
292    * HdyViewSwitcherTitle:title-visible:
293    *
294    * Whether the bar should be revealed or hidden.
295    *
296    * Since: 1.0
297    */
298   props[PROP_TITLE_VISIBLE] =
299     g_param_spec_boolean ("title-visible",
300                          _("Title visible"),
301                          _("Whether the title label is visible"),
302                          TRUE,
303                          G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
304 
305   g_object_class_install_properties (object_class, LAST_PROP, props);
306 
307   gtk_widget_class_set_css_name (widget_class, "viewswitchertitle");
308 
309   gtk_widget_class_set_template_from_resource (widget_class,
310                                                "/sm/puri/handy/ui/hdy-view-switcher-title.ui");
311   gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherTitle, squeezer);
312   gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherTitle, subtitle_label);
313   gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherTitle, title_box);
314   gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherTitle, title_label);
315   gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherTitle, view_switcher);
316   gtk_widget_class_bind_template_callback (widget_class, notify_squeezer_visible_child_cb);
317 }
318 
319 static void
hdy_view_switcher_title_init(HdyViewSwitcherTitle * self)320 hdy_view_switcher_title_init (HdyViewSwitcherTitle *self)
321 {
322   /* This must be initialized before the template so the embedded view switcher
323    * can pick up the correct default value.
324    */
325   self->view_switcher_enabled = TRUE;
326 
327   gtk_widget_init_template (GTK_WIDGET (self));
328 
329   update_subtitle_label (self);
330   update_view_switcher_visible (self);
331 }
332 
333 /**
334  * hdy_view_switcher_title_new:
335  *
336  * Creates a new #HdyViewSwitcherTitle widget.
337  *
338  * Returns: a new #HdyViewSwitcherTitle
339  *
340  * Since: 1.0
341  */
342 HdyViewSwitcherTitle *
hdy_view_switcher_title_new(void)343 hdy_view_switcher_title_new (void)
344 {
345   return g_object_new (HDY_TYPE_VIEW_SWITCHER_TITLE, NULL);
346 }
347 
348 /**
349  * hdy_view_switcher_title_get_policy:
350  * @self: a #HdyViewSwitcherTitle
351  *
352  * Gets the policy of @self.
353  *
354  * Returns: the policy of @self
355  *
356  * Since: 1.0
357  */
358 HdyViewSwitcherPolicy
hdy_view_switcher_title_get_policy(HdyViewSwitcherTitle * self)359 hdy_view_switcher_title_get_policy (HdyViewSwitcherTitle *self)
360 {
361   g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self), HDY_VIEW_SWITCHER_POLICY_NARROW);
362 
363   return hdy_view_switcher_get_policy (self->view_switcher);
364 }
365 
366 /**
367  * hdy_view_switcher_title_set_policy:
368  * @self: a #HdyViewSwitcherTitle
369  * @policy: the new policy
370  *
371  * Sets the policy of @self.
372  *
373  * Since: 1.0
374  */
375 void
hdy_view_switcher_title_set_policy(HdyViewSwitcherTitle * self,HdyViewSwitcherPolicy policy)376 hdy_view_switcher_title_set_policy (HdyViewSwitcherTitle  *self,
377                                     HdyViewSwitcherPolicy  policy)
378 {
379   g_return_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self));
380 
381   if (hdy_view_switcher_get_policy (self->view_switcher) == policy)
382     return;
383 
384   hdy_view_switcher_set_policy (self->view_switcher, policy);
385 
386   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_POLICY]);
387 
388   gtk_widget_queue_resize (GTK_WIDGET (self));
389 }
390 
391 /**
392  * hdy_view_switcher_title_get_stack:
393  * @self: a #HdyViewSwitcherTitle
394  *
395  * Get the #GtkStack being controlled by the #HdyViewSwitcher.
396  *
397  * Returns: (nullable) (transfer none): the #GtkStack, or %NULL if none has been set
398  *
399  * Since: 1.0
400  */
401 GtkStack *
hdy_view_switcher_title_get_stack(HdyViewSwitcherTitle * self)402 hdy_view_switcher_title_get_stack (HdyViewSwitcherTitle *self)
403 {
404   g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self), NULL);
405 
406   return hdy_view_switcher_get_stack (self->view_switcher);
407 }
408 
409 /**
410  * hdy_view_switcher_title_set_stack:
411  * @self: a #HdyViewSwitcherTitle
412  * @stack: (nullable): a #GtkStack
413  *
414  * Sets the #GtkStack to control.
415  *
416  * Since: 1.0
417  */
418 void
hdy_view_switcher_title_set_stack(HdyViewSwitcherTitle * self,GtkStack * stack)419 hdy_view_switcher_title_set_stack (HdyViewSwitcherTitle *self,
420                                    GtkStack             *stack)
421 {
422   GtkStack *previous_stack;
423 
424   g_return_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self));
425   g_return_if_fail (stack == NULL || GTK_IS_STACK (stack));
426 
427   previous_stack = hdy_view_switcher_get_stack (self->view_switcher);
428 
429   if (previous_stack == stack)
430     return;
431 
432   if (previous_stack)
433     g_signal_handlers_disconnect_by_func (previous_stack, G_CALLBACK (update_view_switcher_visible), self);
434 
435   hdy_view_switcher_set_stack (self->view_switcher, stack);
436 
437   if (stack) {
438     g_signal_connect_swapped (stack, "add", G_CALLBACK (update_view_switcher_visible), self);
439     g_signal_connect_swapped (stack, "remove", G_CALLBACK (update_view_switcher_visible), self);
440   }
441 
442   update_view_switcher_visible (self);
443 
444   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_STACK]);
445 }
446 
447 /**
448  * hdy_view_switcher_title_get_title:
449  * @self: a #HdyViewSwitcherTitle
450  *
451  * Gets the title of @self. See hdy_view_switcher_title_set_title().
452  *
453  * Returns: (transfer none) (nullable): the title of @self, or %NULL.
454  *
455  * Since: 1.0
456  */
457 const gchar *
hdy_view_switcher_title_get_title(HdyViewSwitcherTitle * self)458 hdy_view_switcher_title_get_title (HdyViewSwitcherTitle *self)
459 {
460   g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self), NULL);
461 
462   return gtk_label_get_label (self->title_label);
463 }
464 
465 /**
466  * hdy_view_switcher_title_set_title:
467  * @self: a #HdyViewSwitcherTitle
468  * @title: (nullable): a title, or %NULL
469  *
470  * Sets the title of @self. The title should give a user additional details. A
471  * good title should not include the application name.
472  *
473  * Since: 1.0
474  */
475 void
hdy_view_switcher_title_set_title(HdyViewSwitcherTitle * self,const gchar * title)476 hdy_view_switcher_title_set_title (HdyViewSwitcherTitle *self,
477                                    const gchar          *title)
478 {
479   g_return_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self));
480 
481   if (g_strcmp0 (gtk_label_get_label (self->title_label), title) == 0)
482     return;
483 
484   gtk_label_set_label (self->title_label, title);
485   gtk_widget_set_visible (GTK_WIDGET (self->title_label), title && title[0]);
486 
487   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]);
488 }
489 
490 /**
491  * hdy_view_switcher_title_get_subtitle:
492  * @self: a #HdyViewSwitcherTitle
493  *
494  * Gets the subtitle of @self. See hdy_view_switcher_title_set_subtitle().
495  *
496  * Returns: (transfer none) (nullable): the subtitle of @self, or %NULL.
497  *
498  * Since: 1.0
499  */
500 const gchar *
hdy_view_switcher_title_get_subtitle(HdyViewSwitcherTitle * self)501 hdy_view_switcher_title_get_subtitle (HdyViewSwitcherTitle *self)
502 {
503   g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self), NULL);
504 
505   return gtk_label_get_label (self->subtitle_label);
506 }
507 
508 /**
509  * hdy_view_switcher_title_set_subtitle:
510  * @self: a #HdyViewSwitcherTitle
511  * @subtitle: (nullable): a subtitle, or %NULL
512  *
513  * Sets the subtitle of @self. The subtitle should give a user additional
514  * details.
515  *
516  * Since: 1.0
517  */
518 void
hdy_view_switcher_title_set_subtitle(HdyViewSwitcherTitle * self,const gchar * subtitle)519 hdy_view_switcher_title_set_subtitle (HdyViewSwitcherTitle *self,
520                                       const gchar          *subtitle)
521 {
522   g_return_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self));
523 
524   if (g_strcmp0 (gtk_label_get_label (self->subtitle_label), subtitle) == 0)
525     return;
526 
527   gtk_label_set_label (self->subtitle_label, subtitle);
528   update_subtitle_label (self);
529 
530   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SUBTITLE]);
531 }
532 
533 /**
534  * hdy_view_switcher_title_get_view_switcher_enabled:
535  * @self: a #HdyViewSwitcherTitle
536  *
537  * Gets whether @self's view switcher is enabled.
538  *
539  * See hdy_view_switcher_title_set_view_switcher_enabled().
540  *
541  * Returns: %TRUE if the view switcher is enabled, %FALSE otherwise.
542  *
543  * Since: 1.0
544  */
545 gboolean
hdy_view_switcher_title_get_view_switcher_enabled(HdyViewSwitcherTitle * self)546 hdy_view_switcher_title_get_view_switcher_enabled (HdyViewSwitcherTitle *self)
547 {
548   g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self), FALSE);
549 
550   return self->view_switcher_enabled;
551 }
552 
553 /**
554  * hdy_view_switcher_title_set_view_switcher_enabled:
555  * @self: a #HdyViewSwitcherTitle
556  * @enabled: %TRUE to enable the view switcher, %FALSE to disable it
557  *
558  * Make @self enable or disable its view switcher. If it is disabled, the title
559  * will be displayed instead. This allows to programmatically and prematurely
560  * hide the view switcher of @self even if it fits in the available space.
561  *
562  * This can be used e.g. to ensure the view switcher is hidden below a certain
563  * window width, or any other constraint you find suitable.
564  *
565  * Since: 1.0
566  */
567 void
hdy_view_switcher_title_set_view_switcher_enabled(HdyViewSwitcherTitle * self,gboolean enabled)568 hdy_view_switcher_title_set_view_switcher_enabled (HdyViewSwitcherTitle *self,
569                                                    gboolean              enabled)
570 {
571   g_return_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self));
572 
573   enabled = !!enabled;
574 
575   if (self->view_switcher_enabled == enabled)
576     return;
577 
578   self->view_switcher_enabled = enabled;
579   update_view_switcher_visible (self);
580 
581   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VIEW_SWITCHER_ENABLED]);
582 }
583 
584 /**
585  * hdy_view_switcher_title_get_title_visible:
586  * @self: a #HdyViewSwitcherTitle
587  *
588  * Get whether the title label of @self is visible.
589  *
590  * Returns: %TRUE if the title label of @self is visible, %FALSE if not.
591  *
592  * Since: 1.0
593  */
594 gboolean
hdy_view_switcher_title_get_title_visible(HdyViewSwitcherTitle * self)595 hdy_view_switcher_title_get_title_visible (HdyViewSwitcherTitle *self)
596 {
597   g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self), FALSE);
598 
599   return hdy_squeezer_get_visible_child (self->squeezer) == (GtkWidget *) self->title_box;
600 }
601