1 /*
2 * Copyright (c) 2014 Intel Corporation
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or (at your
7 * option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful, but
10 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
12 * License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this program; if not, write to the Free Software Foundation,
16 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17 *
18 * Author:
19 * Ikey Doherty <michael.i.doherty@intel.com>
20 */
21
22 #include "config.h"
23
24 #include "gtkstacksidebar.h"
25
26 #include "gtklabel.h"
27 #include "gtklistbox.h"
28 #include "gtkscrolledwindow.h"
29 #include "gtkseparator.h"
30 #include "gtkstylecontext.h"
31 #include "gtkprivate.h"
32 #include "gtkintl.h"
33
34 /**
35 * SECTION:gtkstacksidebar
36 * @Title: GtkStackSidebar
37 * @Short_description: An automatic sidebar widget
38 *
39 * A GtkStackSidebar enables you to quickly and easily provide a
40 * consistent "sidebar" object for your user interface.
41 *
42 * In order to use a GtkStackSidebar, you simply use a GtkStack to
43 * organize your UI flow, and add the sidebar to your sidebar area. You
44 * can use gtk_stack_sidebar_set_stack() to connect the #GtkStackSidebar
45 * to the #GtkStack.
46 *
47 * # CSS nodes
48 *
49 * GtkStackSidebar has a single CSS node with name stacksidebar and
50 * style class .sidebar.
51 *
52 * When circumstances require it, GtkStackSidebar adds the
53 * .needs-attention style class to the widgets representing the stack
54 * pages.
55 *
56 * Since: 3.16
57 */
58
59 struct _GtkStackSidebarPrivate
60 {
61 GtkListBox *list;
62 GtkStack *stack;
63 GHashTable *rows;
64 gboolean in_child_changed;
65 };
66
67 G_DEFINE_TYPE_WITH_PRIVATE (GtkStackSidebar, gtk_stack_sidebar, GTK_TYPE_BIN)
68
69 enum
70 {
71 PROP_0,
72 PROP_STACK,
73 N_PROPERTIES
74 };
75 static GParamSpec *obj_properties[N_PROPERTIES] = { NULL, };
76
77 static void
gtk_stack_sidebar_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)78 gtk_stack_sidebar_set_property (GObject *object,
79 guint prop_id,
80 const GValue *value,
81 GParamSpec *pspec)
82 {
83 switch (prop_id)
84 {
85 case PROP_STACK:
86 gtk_stack_sidebar_set_stack (GTK_STACK_SIDEBAR (object), g_value_get_object (value));
87 break;
88
89 default:
90 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
91 break;
92 }
93 }
94
95 static void
gtk_stack_sidebar_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)96 gtk_stack_sidebar_get_property (GObject *object,
97 guint prop_id,
98 GValue *value,
99 GParamSpec *pspec)
100 {
101 GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (GTK_STACK_SIDEBAR (object));
102
103 switch (prop_id)
104 {
105 case PROP_STACK:
106 g_value_set_object (value, priv->stack);
107 break;
108
109 default:
110 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
111 break;
112 }
113 }
114
115 static void
update_header(GtkListBoxRow * row,GtkListBoxRow * before,gpointer userdata)116 update_header (GtkListBoxRow *row,
117 GtkListBoxRow *before,
118 gpointer userdata)
119 {
120 GtkWidget *ret = NULL;
121
122 if (before && !gtk_list_box_row_get_header (row))
123 {
124 ret = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
125 gtk_list_box_row_set_header (row, ret);
126 }
127 }
128
129 static gint
sort_list(GtkListBoxRow * row1,GtkListBoxRow * row2,gpointer userdata)130 sort_list (GtkListBoxRow *row1,
131 GtkListBoxRow *row2,
132 gpointer userdata)
133 {
134 GtkStackSidebar *sidebar = GTK_STACK_SIDEBAR (userdata);
135 GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar);
136 GtkWidget *item;
137 GtkWidget *widget;
138 gint left = 0; gint right = 0;
139
140
141 if (row1)
142 {
143 item = gtk_bin_get_child (GTK_BIN (row1));
144 widget = g_object_get_data (G_OBJECT (item), "stack-child");
145 gtk_container_child_get (GTK_CONTAINER (priv->stack), widget,
146 "position", &left,
147 NULL);
148 }
149
150 if (row2)
151 {
152 item = gtk_bin_get_child (GTK_BIN (row2));
153 widget = g_object_get_data (G_OBJECT (item), "stack-child");
154 gtk_container_child_get (GTK_CONTAINER (priv->stack), widget,
155 "position", &right,
156 NULL);
157 }
158
159 if (left < right)
160 return -1;
161
162 if (left == right)
163 return 0;
164
165 return 1;
166 }
167
168 static void
gtk_stack_sidebar_row_selected(GtkListBox * box,GtkListBoxRow * row,gpointer userdata)169 gtk_stack_sidebar_row_selected (GtkListBox *box,
170 GtkListBoxRow *row,
171 gpointer userdata)
172 {
173 GtkStackSidebar *sidebar = GTK_STACK_SIDEBAR (userdata);
174 GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar);
175 GtkWidget *item;
176 GtkWidget *widget;
177
178 if (priv->in_child_changed)
179 return;
180
181 if (!row)
182 return;
183
184 item = gtk_bin_get_child (GTK_BIN (row));
185 widget = g_object_get_data (G_OBJECT (item), "stack-child");
186 gtk_stack_set_visible_child (priv->stack, widget);
187 }
188
189 static void
gtk_stack_sidebar_init(GtkStackSidebar * sidebar)190 gtk_stack_sidebar_init (GtkStackSidebar *sidebar)
191 {
192 GtkStyleContext *style;
193 GtkStackSidebarPrivate *priv;
194 GtkWidget *sw;
195
196 priv = gtk_stack_sidebar_get_instance_private (sidebar);
197
198 sw = gtk_scrolled_window_new (NULL, NULL);
199 gtk_widget_show (sw);
200 gtk_widget_set_no_show_all (sw, TRUE);
201 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw),
202 GTK_POLICY_NEVER,
203 GTK_POLICY_AUTOMATIC);
204
205 gtk_container_add (GTK_CONTAINER (sidebar), sw);
206
207 priv->list = GTK_LIST_BOX (gtk_list_box_new ());
208 gtk_widget_show (GTK_WIDGET (priv->list));
209
210 gtk_container_add (GTK_CONTAINER (sw), GTK_WIDGET (priv->list));
211
212 gtk_list_box_set_header_func (priv->list, update_header, sidebar, NULL);
213 gtk_list_box_set_sort_func (priv->list, sort_list, sidebar, NULL);
214
215 g_signal_connect (priv->list, "row-selected",
216 G_CALLBACK (gtk_stack_sidebar_row_selected), sidebar);
217
218 style = gtk_widget_get_style_context (GTK_WIDGET (sidebar));
219 gtk_style_context_add_class (style, "sidebar");
220
221 priv->rows = g_hash_table_new (NULL, NULL);
222 }
223
224 static void
update_row(GtkStackSidebar * sidebar,GtkWidget * widget,GtkWidget * row)225 update_row (GtkStackSidebar *sidebar,
226 GtkWidget *widget,
227 GtkWidget *row)
228 {
229 GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar);
230 GtkWidget *item;
231 gchar *title;
232 gboolean needs_attention;
233 GtkStyleContext *context;
234
235 gtk_container_child_get (GTK_CONTAINER (priv->stack), widget,
236 "title", &title,
237 "needs-attention", &needs_attention,
238 NULL);
239
240 item = gtk_bin_get_child (GTK_BIN (row));
241 gtk_label_set_text (GTK_LABEL (item), title);
242
243 gtk_widget_set_visible (row, gtk_widget_get_visible (widget) && title != NULL);
244
245 context = gtk_widget_get_style_context (row);
246 if (needs_attention)
247 gtk_style_context_add_class (context, GTK_STYLE_CLASS_NEEDS_ATTENTION);
248 else
249 gtk_style_context_remove_class (context, GTK_STYLE_CLASS_NEEDS_ATTENTION);
250
251 g_free (title);
252 }
253
254 static void
on_position_updated(GtkWidget * widget,GParamSpec * pspec,GtkStackSidebar * sidebar)255 on_position_updated (GtkWidget *widget,
256 GParamSpec *pspec,
257 GtkStackSidebar *sidebar)
258 {
259 GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar);
260
261 gtk_list_box_invalidate_sort (priv->list);
262 }
263
264 static void
on_child_updated(GtkWidget * widget,GParamSpec * pspec,GtkStackSidebar * sidebar)265 on_child_updated (GtkWidget *widget,
266 GParamSpec *pspec,
267 GtkStackSidebar *sidebar)
268 {
269 GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar);
270 GtkWidget *row;
271
272 row = g_hash_table_lookup (priv->rows, widget);
273 update_row (sidebar, widget, row);
274 }
275
276 static void
add_child(GtkWidget * widget,GtkStackSidebar * sidebar)277 add_child (GtkWidget *widget,
278 GtkStackSidebar *sidebar)
279 {
280 GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar);
281 GtkWidget *item;
282 GtkWidget *row;
283
284 /* Check we don't actually already know about this widget */
285 if (g_hash_table_lookup (priv->rows, widget))
286 return;
287
288 /* Make a pretty item when we add kids */
289 item = gtk_label_new ("");
290 gtk_widget_set_halign (item, GTK_ALIGN_START);
291 gtk_widget_set_valign (item, GTK_ALIGN_CENTER);
292 row = gtk_list_box_row_new ();
293 gtk_container_add (GTK_CONTAINER (row), item);
294 gtk_widget_show (item);
295
296 update_row (sidebar, widget, row);
297
298 /* Hook up for events */
299 g_signal_connect (widget, "child-notify::title",
300 G_CALLBACK (on_child_updated), sidebar);
301 g_signal_connect (widget, "child-notify::needs-attention",
302 G_CALLBACK (on_child_updated), sidebar);
303 g_signal_connect (widget, "notify::visible",
304 G_CALLBACK (on_child_updated), sidebar);
305 g_signal_connect (widget, "child-notify::position",
306 G_CALLBACK (on_position_updated), sidebar);
307
308 g_object_set_data (G_OBJECT (item), "stack-child", widget);
309 g_hash_table_insert (priv->rows, widget, row);
310 gtk_container_add (GTK_CONTAINER (priv->list), row);
311 }
312
313 static void
remove_child(GtkWidget * widget,GtkStackSidebar * sidebar)314 remove_child (GtkWidget *widget,
315 GtkStackSidebar *sidebar)
316 {
317 GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar);
318 GtkWidget *row;
319
320 row = g_hash_table_lookup (priv->rows, widget);
321 if (!row)
322 return;
323
324 g_signal_handlers_disconnect_by_func (widget, on_child_updated, sidebar);
325 g_signal_handlers_disconnect_by_func (widget, on_position_updated, sidebar);
326
327 gtk_container_remove (GTK_CONTAINER (priv->list), row);
328 g_hash_table_remove (priv->rows, widget);
329 }
330
331 static void
populate_sidebar(GtkStackSidebar * sidebar)332 populate_sidebar (GtkStackSidebar *sidebar)
333 {
334 GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar);
335 GtkWidget *widget, *row;
336
337 gtk_container_foreach (GTK_CONTAINER (priv->stack), (GtkCallback)add_child, sidebar);
338
339 widget = gtk_stack_get_visible_child (priv->stack);
340 if (widget)
341 {
342 row = g_hash_table_lookup (priv->rows, widget);
343 gtk_list_box_select_row (priv->list, GTK_LIST_BOX_ROW (row));
344 }
345 }
346
347 static void
clear_sidebar(GtkStackSidebar * sidebar)348 clear_sidebar (GtkStackSidebar *sidebar)
349 {
350 GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar);
351
352 gtk_container_foreach (GTK_CONTAINER (priv->stack), (GtkCallback)remove_child, sidebar);
353 }
354
355 static void
on_child_changed(GtkWidget * widget,GParamSpec * pspec,GtkStackSidebar * sidebar)356 on_child_changed (GtkWidget *widget,
357 GParamSpec *pspec,
358 GtkStackSidebar *sidebar)
359 {
360 GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar);
361 GtkWidget *child;
362 GtkWidget *row;
363
364 child = gtk_stack_get_visible_child (GTK_STACK (widget));
365 row = g_hash_table_lookup (priv->rows, child);
366 if (row != NULL)
367 {
368 priv->in_child_changed = TRUE;
369 gtk_list_box_select_row (priv->list, GTK_LIST_BOX_ROW (row));
370 priv->in_child_changed = FALSE;
371 }
372 }
373
374 static void
on_stack_child_added(GtkContainer * container,GtkWidget * widget,GtkStackSidebar * sidebar)375 on_stack_child_added (GtkContainer *container,
376 GtkWidget *widget,
377 GtkStackSidebar *sidebar)
378 {
379 add_child (widget, sidebar);
380 }
381
382 static void
on_stack_child_removed(GtkContainer * container,GtkWidget * widget,GtkStackSidebar * sidebar)383 on_stack_child_removed (GtkContainer *container,
384 GtkWidget *widget,
385 GtkStackSidebar *sidebar)
386 {
387 remove_child (widget, sidebar);
388 }
389
390 static void
disconnect_stack_signals(GtkStackSidebar * sidebar)391 disconnect_stack_signals (GtkStackSidebar *sidebar)
392 {
393 GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar);
394
395 g_signal_handlers_disconnect_by_func (priv->stack, on_stack_child_added, sidebar);
396 g_signal_handlers_disconnect_by_func (priv->stack, on_stack_child_removed, sidebar);
397 g_signal_handlers_disconnect_by_func (priv->stack, on_child_changed, sidebar);
398 g_signal_handlers_disconnect_by_func (priv->stack, disconnect_stack_signals, sidebar);
399 }
400
401 static void
connect_stack_signals(GtkStackSidebar * sidebar)402 connect_stack_signals (GtkStackSidebar *sidebar)
403 {
404 GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar);
405
406 g_signal_connect_after (priv->stack, "add",
407 G_CALLBACK (on_stack_child_added), sidebar);
408 g_signal_connect_after (priv->stack, "remove",
409 G_CALLBACK (on_stack_child_removed), sidebar);
410 g_signal_connect (priv->stack, "notify::visible-child",
411 G_CALLBACK (on_child_changed), sidebar);
412 g_signal_connect_swapped (priv->stack, "destroy",
413 G_CALLBACK (disconnect_stack_signals), sidebar);
414 }
415
416 static void
gtk_stack_sidebar_dispose(GObject * object)417 gtk_stack_sidebar_dispose (GObject *object)
418 {
419 GtkStackSidebar *sidebar = GTK_STACK_SIDEBAR (object);
420
421 gtk_stack_sidebar_set_stack (sidebar, NULL);
422
423 G_OBJECT_CLASS (gtk_stack_sidebar_parent_class)->dispose (object);
424 }
425
426 static void
gtk_stack_sidebar_finalize(GObject * object)427 gtk_stack_sidebar_finalize (GObject *object)
428 {
429 GtkStackSidebar *sidebar = GTK_STACK_SIDEBAR (object);
430 GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar);
431
432 g_hash_table_destroy (priv->rows);
433
434 G_OBJECT_CLASS (gtk_stack_sidebar_parent_class)->finalize (object);
435 }
436
437 static void
gtk_stack_sidebar_class_init(GtkStackSidebarClass * klass)438 gtk_stack_sidebar_class_init (GtkStackSidebarClass *klass)
439 {
440 GObjectClass *object_class = G_OBJECT_CLASS (klass);
441 GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
442
443 object_class->dispose = gtk_stack_sidebar_dispose;
444 object_class->finalize = gtk_stack_sidebar_finalize;
445 object_class->set_property = gtk_stack_sidebar_set_property;
446 object_class->get_property = gtk_stack_sidebar_get_property;
447
448 obj_properties[PROP_STACK] =
449 g_param_spec_object (I_("stack"), P_("Stack"),
450 P_("Associated stack for this GtkStackSidebar"),
451 GTK_TYPE_STACK,
452 G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS|G_PARAM_EXPLICIT_NOTIFY);
453
454 g_object_class_install_properties (object_class, N_PROPERTIES, obj_properties);
455
456 gtk_widget_class_set_css_name (widget_class, "stacksidebar");
457 }
458
459 /**
460 * gtk_stack_sidebar_new:
461 *
462 * Creates a new sidebar.
463 *
464 * Returns: the new #GtkStackSidebar
465 *
466 * Since: 3.16
467 */
468 GtkWidget *
gtk_stack_sidebar_new(void)469 gtk_stack_sidebar_new (void)
470 {
471 return GTK_WIDGET (g_object_new (GTK_TYPE_STACK_SIDEBAR, NULL));
472 }
473
474 /**
475 * gtk_stack_sidebar_set_stack:
476 * @sidebar: a #GtkStackSidebar
477 * @stack: a #GtkStack
478 *
479 * Set the #GtkStack associated with this #GtkStackSidebar.
480 *
481 * The sidebar widget will automatically update according to the order
482 * (packing) and items within the given #GtkStack.
483 *
484 * Since: 3.16
485 */
486 void
gtk_stack_sidebar_set_stack(GtkStackSidebar * sidebar,GtkStack * stack)487 gtk_stack_sidebar_set_stack (GtkStackSidebar *sidebar,
488 GtkStack *stack)
489 {
490 GtkStackSidebarPrivate *priv;
491
492 g_return_if_fail (GTK_IS_STACK_SIDEBAR (sidebar));
493 g_return_if_fail (GTK_IS_STACK (stack) || stack == NULL);
494
495 priv = gtk_stack_sidebar_get_instance_private (sidebar);
496
497 if (priv->stack == stack)
498 return;
499
500 if (priv->stack)
501 {
502 disconnect_stack_signals (sidebar);
503 clear_sidebar (sidebar);
504 g_clear_object (&priv->stack);
505 }
506 if (stack)
507 {
508 priv->stack = g_object_ref (stack);
509 populate_sidebar (sidebar);
510 connect_stack_signals (sidebar);
511 }
512
513 gtk_widget_queue_resize (GTK_WIDGET (sidebar));
514
515 g_object_notify (G_OBJECT (sidebar), "stack");
516 }
517
518 /**
519 * gtk_stack_sidebar_get_stack:
520 * @sidebar: a #GtkStackSidebar
521 *
522 * Retrieves the stack.
523 * See gtk_stack_sidebar_set_stack().
524 *
525 * Returns: (nullable) (transfer none): the associated #GtkStack or
526 * %NULL if none has been set explicitly
527 *
528 * Since: 3.16
529 */
530 GtkStack *
gtk_stack_sidebar_get_stack(GtkStackSidebar * sidebar)531 gtk_stack_sidebar_get_stack (GtkStackSidebar *sidebar)
532 {
533 GtkStackSidebarPrivate *priv;
534
535 g_return_val_if_fail (GTK_IS_STACK_SIDEBAR (sidebar), NULL);
536
537 priv = gtk_stack_sidebar_get_instance_private (sidebar);
538
539 return GTK_STACK (priv->stack);
540 }
541