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