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