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 "gtkbinlayout.h" |
27 | #include "gtklabel.h" |
28 | #include "gtklistbox.h" |
29 | #include "gtkscrolledwindow.h" |
30 | #include "gtkseparator.h" |
31 | #include "gtkselectionmodel.h" |
32 | #include "gtkstack.h" |
33 | #include "gtkprivate.h" |
34 | #include "gtkwidgetprivate.h" |
35 | #include "gtkintl.h" |
36 | |
37 | #include <glib/gi18n-lib.h> |
38 | |
39 | /** |
40 | * GtkStackSidebar: |
41 | * |
42 | * A `GtkStackSidebar` uses a sidebar to switch between `GtkStack` pages. |
43 | * |
44 | * In order to use a `GtkStackSidebar`, you simply use a `GtkStack` to |
45 | * organize your UI flow, and add the sidebar to your sidebar area. You |
46 | * can use [method@Gtk.StackSidebar.set_stack] to connect the `GtkStackSidebar` |
47 | * to the `GtkStack`. |
48 | * |
49 | * # CSS nodes |
50 | * |
51 | * `GtkStackSidebar` has a single CSS node with name stacksidebar and |
52 | * style class .sidebar. |
53 | * |
54 | * When circumstances require it, `GtkStackSidebar` adds the |
55 | * .needs-attention style class to the widgets representing the stack |
56 | * pages. |
57 | */ |
58 | |
59 | typedef struct _GtkStackSidebarClass ; |
60 | |
61 | struct |
62 | { |
63 | GtkWidget ; |
64 | |
65 | GtkListBox *; |
66 | GtkStack *; |
67 | GtkSelectionModel *; |
68 | GHashTable *; |
69 | }; |
70 | |
71 | struct |
72 | { |
73 | GtkWidgetClass ; |
74 | }; |
75 | |
76 | G_DEFINE_TYPE (GtkStackSidebar, gtk_stack_sidebar, GTK_TYPE_WIDGET) |
77 | |
78 | enum |
79 | { |
80 | PROP_0, |
81 | PROP_STACK, |
82 | N_PROPERTIES |
83 | }; |
84 | static GParamSpec *obj_properties[N_PROPERTIES] = { NULL, }; |
85 | |
86 | static void |
87 | (GObject *object, |
88 | guint prop_id, |
89 | const GValue *value, |
90 | GParamSpec *pspec) |
91 | { |
92 | switch (prop_id) |
93 | { |
94 | case PROP_STACK: |
95 | gtk_stack_sidebar_set_stack (GTK_STACK_SIDEBAR (object), stack: g_value_get_object (value)); |
96 | break; |
97 | |
98 | default: |
99 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
100 | break; |
101 | } |
102 | } |
103 | |
104 | static void |
105 | (GObject *object, |
106 | guint prop_id, |
107 | GValue *value, |
108 | GParamSpec *pspec) |
109 | { |
110 | GtkStackSidebar *self = GTK_STACK_SIDEBAR (object); |
111 | |
112 | switch (prop_id) |
113 | { |
114 | case PROP_STACK: |
115 | g_value_set_object (value, v_object: self->stack); |
116 | break; |
117 | |
118 | default: |
119 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
120 | break; |
121 | } |
122 | } |
123 | |
124 | static void |
125 | (GtkListBox *box, |
126 | GtkListBoxRow *row, |
127 | gpointer userdata) |
128 | { |
129 | GtkStackSidebar *self = GTK_STACK_SIDEBAR (userdata); |
130 | guint index; |
131 | |
132 | if (row == NULL) |
133 | return; |
134 | |
135 | index = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (row), "child-index" )); |
136 | gtk_selection_model_select_item (model: self->pages, position: index, TRUE); |
137 | } |
138 | |
139 | static void |
140 | (GtkStackSidebar *self) |
141 | { |
142 | GtkWidget *sw; |
143 | |
144 | sw = gtk_scrolled_window_new (); |
145 | gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw), |
146 | hscrollbar_policy: GTK_POLICY_NEVER, |
147 | vscrollbar_policy: GTK_POLICY_AUTOMATIC); |
148 | |
149 | gtk_widget_set_parent (widget: sw, GTK_WIDGET (self)); |
150 | |
151 | self->list = GTK_LIST_BOX (gtk_list_box_new ()); |
152 | gtk_widget_add_css_class (GTK_WIDGET (self->list), css_class: "navigation-sidebar" ); |
153 | gtk_accessible_update_property (self: GTK_ACCESSIBLE (ptr: self->list), |
154 | first_property: GTK_ACCESSIBLE_PROPERTY_LABEL, |
155 | C_("accessibility" , "Sidebar" ), |
156 | -1); |
157 | |
158 | |
159 | gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), GTK_WIDGET (self->list)); |
160 | |
161 | g_signal_connect (self->list, "row-selected" , |
162 | G_CALLBACK (gtk_stack_sidebar_row_selected), self); |
163 | |
164 | gtk_widget_add_css_class (GTK_WIDGET (self), css_class: "sidebar" ); |
165 | |
166 | self->rows = g_hash_table_new (NULL, NULL); |
167 | } |
168 | |
169 | static void |
170 | update_row (GtkStackSidebar *self, |
171 | GtkStackPage *page, |
172 | GtkWidget *row) |
173 | { |
174 | GtkWidget *item; |
175 | char *title; |
176 | gboolean needs_attention; |
177 | gboolean visible; |
178 | |
179 | g_object_get (object: page, |
180 | first_property_name: "title" , &title, |
181 | "needs-attention" , &needs_attention, |
182 | "visible" , &visible, |
183 | NULL); |
184 | |
185 | item = gtk_list_box_row_get_child (GTK_LIST_BOX_ROW (row)); |
186 | gtk_label_set_text (GTK_LABEL (item), str: title); |
187 | |
188 | gtk_widget_set_visible (widget: row, visible: visible && title != NULL); |
189 | |
190 | if (needs_attention) |
191 | gtk_widget_add_css_class (widget: row, css_class: "needs-attention" ); |
192 | else |
193 | gtk_widget_remove_css_class (widget: row, css_class: "needs-attention" ); |
194 | |
195 | g_free (mem: title); |
196 | } |
197 | |
198 | static void |
199 | on_page_updated (GtkStackPage *page, |
200 | GParamSpec *pspec, |
201 | GtkStackSidebar *self) |
202 | { |
203 | GtkWidget *row; |
204 | |
205 | row = g_hash_table_lookup (hash_table: self->rows, key: page); |
206 | update_row (self, page, row); |
207 | } |
208 | |
209 | static void |
210 | add_child (guint position, |
211 | GtkStackSidebar *self) |
212 | { |
213 | GtkWidget *item; |
214 | GtkWidget *row; |
215 | GtkStackPage *page; |
216 | |
217 | /* Make a pretty item when we add kids */ |
218 | item = gtk_label_new (str: "" ); |
219 | gtk_widget_set_halign (widget: item, align: GTK_ALIGN_START); |
220 | gtk_widget_set_valign (widget: item, align: GTK_ALIGN_CENTER); |
221 | row = gtk_list_box_row_new (); |
222 | gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (row), child: item); |
223 | |
224 | page = g_list_model_get_item (list: G_LIST_MODEL (ptr: self->pages), position); |
225 | update_row (self, page, row); |
226 | |
227 | gtk_list_box_insert (GTK_LIST_BOX (self->list), child: row, position: -1); |
228 | |
229 | g_object_set_data (G_OBJECT (row), key: "child-index" , GUINT_TO_POINTER (position)); |
230 | if (gtk_selection_model_is_selected (model: self->pages, position)) |
231 | gtk_list_box_select_row (box: self->list, GTK_LIST_BOX_ROW (row)); |
232 | else |
233 | gtk_list_box_unselect_row (box: self->list, GTK_LIST_BOX_ROW (row)); |
234 | |
235 | g_signal_connect (page, "notify" , G_CALLBACK (on_page_updated), self); |
236 | |
237 | g_hash_table_insert (hash_table: self->rows, key: page, value: row); |
238 | |
239 | g_object_unref (object: page); |
240 | } |
241 | |
242 | static void |
243 | (GtkStackSidebar *self) |
244 | { |
245 | guint i, n; |
246 | |
247 | n = g_list_model_get_n_items (list: G_LIST_MODEL (ptr: self->pages)); |
248 | for (i = 0; i < n; i++) |
249 | add_child (position: i, self); |
250 | } |
251 | |
252 | static void |
253 | (GtkStackSidebar *self) |
254 | { |
255 | GHashTableIter iter; |
256 | GtkStackPage *page; |
257 | GtkWidget *row; |
258 | |
259 | g_hash_table_iter_init (iter: &iter, hash_table: self->rows); |
260 | while (g_hash_table_iter_next (iter: &iter, key: (gpointer *)&page, value: (gpointer *)&row)) |
261 | { |
262 | gtk_list_box_remove (GTK_LIST_BOX (self->list), child: row); |
263 | g_hash_table_iter_remove (iter: &iter); |
264 | g_signal_handlers_disconnect_by_func (page, on_page_updated, self); |
265 | } |
266 | } |
267 | |
268 | static void |
269 | items_changed_cb (GListModel *model, |
270 | guint position, |
271 | guint removed, |
272 | guint added, |
273 | GtkStackSidebar *self) |
274 | { |
275 | /* FIXME: we can do better */ |
276 | clear_sidebar (self); |
277 | populate_sidebar (self); |
278 | } |
279 | |
280 | static void |
281 | selection_changed_cb (GtkSelectionModel *model, |
282 | guint position, |
283 | guint n_items, |
284 | GtkStackSidebar *self) |
285 | { |
286 | guint i; |
287 | |
288 | for (i = position; i < position + n_items; i++) |
289 | { |
290 | GtkStackPage *page; |
291 | GtkWidget *row; |
292 | |
293 | page = g_list_model_get_item (list: G_LIST_MODEL (ptr: self->pages), position: i); |
294 | row = g_hash_table_lookup (hash_table: self->rows, key: page); |
295 | if (gtk_selection_model_is_selected (model: self->pages, position: i)) |
296 | gtk_list_box_select_row (box: self->list, GTK_LIST_BOX_ROW (row)); |
297 | else |
298 | gtk_list_box_unselect_row (box: self->list, GTK_LIST_BOX_ROW (row)); |
299 | g_object_unref (object: page); |
300 | } |
301 | } |
302 | |
303 | static void |
304 | set_stack (GtkStackSidebar *self, |
305 | GtkStack *stack) |
306 | { |
307 | if (stack) |
308 | { |
309 | self->stack = g_object_ref (stack); |
310 | self->pages = gtk_stack_get_pages (stack); |
311 | populate_sidebar (self); |
312 | g_signal_connect (self->pages, "items-changed" , G_CALLBACK (items_changed_cb), self); |
313 | g_signal_connect (self->pages, "selection-changed" , G_CALLBACK (selection_changed_cb), self); |
314 | } |
315 | } |
316 | |
317 | static void |
318 | unset_stack (GtkStackSidebar *self) |
319 | { |
320 | if (self->stack) |
321 | { |
322 | g_signal_handlers_disconnect_by_func (self->pages, items_changed_cb, self); |
323 | g_signal_handlers_disconnect_by_func (self->pages, selection_changed_cb, self); |
324 | clear_sidebar (self); |
325 | g_clear_object (&self->stack); |
326 | g_clear_object (&self->pages); |
327 | } |
328 | } |
329 | |
330 | static void |
331 | (GObject *object) |
332 | { |
333 | GtkStackSidebar *self = GTK_STACK_SIDEBAR (object); |
334 | GtkWidget *child; |
335 | |
336 | unset_stack (self); |
337 | |
338 | /* The scrolled window */ |
339 | child = gtk_widget_get_first_child (GTK_WIDGET (self)); |
340 | if (child) |
341 | { |
342 | gtk_widget_unparent (widget: child); |
343 | self->list = NULL; |
344 | } |
345 | |
346 | G_OBJECT_CLASS (gtk_stack_sidebar_parent_class)->dispose (object); |
347 | } |
348 | |
349 | static void |
350 | (GObject *object) |
351 | { |
352 | GtkStackSidebar *self = GTK_STACK_SIDEBAR (object); |
353 | |
354 | g_hash_table_destroy (hash_table: self->rows); |
355 | |
356 | G_OBJECT_CLASS (gtk_stack_sidebar_parent_class)->finalize (object); |
357 | } |
358 | |
359 | static void |
360 | (GtkStackSidebarClass *klass) |
361 | { |
362 | GObjectClass *object_class = G_OBJECT_CLASS (klass); |
363 | GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); |
364 | |
365 | object_class->dispose = gtk_stack_sidebar_dispose; |
366 | object_class->finalize = gtk_stack_sidebar_finalize; |
367 | object_class->set_property = gtk_stack_sidebar_set_property; |
368 | object_class->get_property = gtk_stack_sidebar_get_property; |
369 | |
370 | /** |
371 | * GtkStackSidebar:stack: (attributes org.gtk.Property.get=gtk_stack_sidebar_get_stack org.gtk.Property.set=gtk_stack_sidebar_set_stack) |
372 | * |
373 | * The stack. |
374 | */ |
375 | obj_properties[PROP_STACK] = |
376 | g_param_spec_object (I_("stack" ), P_("Stack" ), |
377 | P_("Associated stack for this GtkStackSidebar" ), |
378 | GTK_TYPE_STACK, |
379 | flags: G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS|G_PARAM_EXPLICIT_NOTIFY); |
380 | |
381 | g_object_class_install_properties (oclass: object_class, n_pspecs: N_PROPERTIES, pspecs: obj_properties); |
382 | |
383 | gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT); |
384 | gtk_widget_class_set_css_name (widget_class, I_("stacksidebar" )); |
385 | } |
386 | |
387 | /** |
388 | * gtk_stack_sidebar_new: |
389 | * |
390 | * Creates a new `GtkStackSidebar`. |
391 | * |
392 | * Returns: the new `GtkStackSidebar` |
393 | */ |
394 | GtkWidget * |
395 | (void) |
396 | { |
397 | return GTK_WIDGET (g_object_new (GTK_TYPE_STACK_SIDEBAR, NULL)); |
398 | } |
399 | |
400 | /** |
401 | * gtk_stack_sidebar_set_stack: (attributes org.gtk.Method.set_property=stack) |
402 | * @self: a `GtkStackSidebar` |
403 | * @stack: a `GtkStack` |
404 | * |
405 | * Set the `GtkStack` associated with this `GtkStackSidebar`. |
406 | * |
407 | * The sidebar widget will automatically update according to |
408 | * the order and items within the given `GtkStack`. |
409 | */ |
410 | void |
411 | (GtkStackSidebar *self, |
412 | GtkStack *stack) |
413 | { |
414 | g_return_if_fail (GTK_IS_STACK_SIDEBAR (self)); |
415 | g_return_if_fail (GTK_IS_STACK (stack) || stack == NULL); |
416 | |
417 | |
418 | if (self->stack == stack) |
419 | return; |
420 | |
421 | unset_stack (self); |
422 | set_stack (self, stack); |
423 | |
424 | gtk_widget_queue_resize (GTK_WIDGET (self)); |
425 | |
426 | g_object_notify (G_OBJECT (self), property_name: "stack" ); |
427 | } |
428 | |
429 | /** |
430 | * gtk_stack_sidebar_get_stack: (attributes org.gtk.Method.get_property=stack) |
431 | * @self: a `GtkStackSidebar` |
432 | * |
433 | * Retrieves the stack. |
434 | * |
435 | * Returns: (nullable) (transfer none): the associated `GtkStack` or |
436 | * %NULL if none has been set explicitly |
437 | */ |
438 | GtkStack * |
439 | (GtkStackSidebar *self) |
440 | { |
441 | g_return_val_if_fail (GTK_IS_STACK_SIDEBAR (self), NULL); |
442 | |
443 | return GTK_STACK (self->stack); |
444 | } |
445 | |