1/*
2 * Copyright © 2013 Canonical Limited
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the licence, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library; if not, see <http://www.gnu.org/licenses/>.
16 *
17 * Author: Ryan Lortie <desrt@desrt.ca>
18 */
19
20#include "config.h"
21
22#include "gtkmenutrackerprivate.h"
23
24/*< private >
25 * GtkMenuTracker:
26 *
27 * GtkMenuTracker is a simple object to ease implementations of GMenuModel.
28 * Given a GtkActionObservable (usually a GActionMuxer) along with a
29 * GMenuModel, it will tell you which menu items to create and where to place
30 * them. If a menu item is removed, it will tell you the position of the menu
31 * item to remove.
32 *
33 * Using GtkMenuTracker is fairly simple. The only guarantee you must make
34 * to GtkMenuTracker is that you must obey all insert signals and track the
35 * position of items that GtkMenuTracker gives you. That is, GtkMenuTracker
36 * expects positions of all the latter items to change when it calls your
37 * insertion callback with an early position, as it may ask you to remove
38 * an item with a readjusted position later.
39 *
40 * GtkMenuTracker will give you a GtkMenuTrackerItem in your callback. You
41 * must hold onto this object until a remove signal is emitted. This item
42 * represents a single menu item, which can be one of three classes: normal item,
43 * separator, or submenu.
44 *
45 * Certain properties on the GtkMenuTrackerItem are mutable, and you must
46 * listen for changes in the item. For more details, see the documentation
47 * for GtkMenuTrackerItem along with https://wiki.gnome.org/Projects/GLib/GApplication/GMenuModel.
48 *
49 * The idea of @with_separators is for special cases where menu models may
50 * be tracked in places where separators are not available, like in toplevel
51 * "File", “Edit” menu bars. Ignoring separator items is wrong, as GtkMenuTracker
52 * expects the position to change, so we must tell GtkMenuTracker to ignore
53 * separators itself.
54 */
55
56typedef struct _GtkMenuTrackerSection GtkMenuTrackerSection;
57
58struct _GtkMenuTracker
59{
60 GtkActionObservable *observable;
61 guint merge_sections : 1;
62 guint mac_os_mode : 1;
63 GtkMenuTrackerInsertFunc insert_func;
64 GtkMenuTrackerRemoveFunc remove_func;
65 gpointer user_data;
66
67 GtkMenuTrackerSection *toplevel;
68};
69
70struct _GtkMenuTrackerSection
71{
72 gpointer model; /* may be a GtkMenuTrackerItem or a GMenuModel */
73 GSList *items;
74 char *action_namespace;
75
76 guint separator_label : 1;
77 guint with_separators : 1;
78 guint has_separator : 1;
79 guint is_fake : 1;
80
81 gulong handler;
82};
83
84static GtkMenuTrackerSection * gtk_menu_tracker_section_new (GtkMenuTracker *tracker,
85 GMenuModel *model,
86 gboolean with_separators,
87 gboolean separator_label,
88 int offset,
89 const char *action_namespace);
90static void gtk_menu_tracker_section_free (GtkMenuTrackerSection *section);
91
92static GtkMenuTrackerSection *
93gtk_menu_tracker_section_find_model (GtkMenuTrackerSection *section,
94 gpointer model,
95 int *offset)
96{
97 GSList *item;
98
99 if (section->has_separator)
100 (*offset)++;
101
102 if (section->model == model)
103 return section;
104
105 for (item = section->items; item; item = item->next)
106 {
107 GtkMenuTrackerSection *subsection = item->data;
108
109 if (subsection)
110 {
111 GtkMenuTrackerSection *found_section;
112
113 found_section = gtk_menu_tracker_section_find_model (section: subsection, model, offset);
114
115 if (found_section)
116 return found_section;
117 }
118 else
119 (*offset)++;
120 }
121
122 return NULL;
123}
124
125/* this is responsible for syncing the showing of a separator for a
126 * single subsection (and its children).
127 *
128 * we only ever show separators if we have _actual_ children (ie: we do
129 * not show a separator if the section contains only empty child
130 * sections). it’s difficult to determine this on-the-fly, so we have
131 * this separate function to come back later and figure it out.
132 *
133 * 'section' is that section.
134 *
135 * 'tracker' is passed in so that we can emit callbacks when we decide
136 * to add/remove separators.
137 *
138 * 'offset' is passed in so we know which position to emit in our
139 * callbacks. ie: if we add a separator right at the top of this
140 * section then we would emit it with this offset. deeper inside, we
141 * adjust accordingly.
142 *
143 * could_have_separator is true in two situations:
144 *
145 * - our parent section had with_separators defined and there are items
146 * before us (ie: we should add a separator if we have content in
147 * order to divide us from the items above)
148 *
149 * - if we had a “label” attribute set for this section
150 *
151 * parent_model and parent_index are passed in so that we can give them
152 * to the insertion callback so that it can see the label (and anything
153 * else that happens to be defined on the section).
154 *
155 * we iterate each item in ourselves. for subsections, we recursively
156 * run ourselves to sync separators. after we are done, we notice if we
157 * have any items in us or if we are completely empty and sync if our
158 * separator is shown or not.
159 */
160static int
161gtk_menu_tracker_section_sync_separators (GtkMenuTrackerSection *section,
162 GtkMenuTracker *tracker,
163 int offset,
164 gboolean could_have_separator,
165 GMenuModel *parent_model,
166 int parent_index)
167{
168 gboolean should_have_separator;
169 int n_items = 0;
170 GSList *item;
171 int i = 0;
172
173 for (item = section->items; item; item = item->next)
174 {
175 GtkMenuTrackerSection *subsection = item->data;
176
177 if (subsection)
178 {
179 gboolean separator;
180
181 separator = (section->with_separators && n_items > 0) || subsection->separator_label;
182
183 /* Only pass the parent_model and parent_index in case they may be used to create the separator. */
184 n_items += gtk_menu_tracker_section_sync_separators (section: subsection, tracker, offset: offset + n_items,
185 could_have_separator: separator,
186 parent_model: separator ? section->model : NULL,
187 parent_index: separator ? i : 0);
188 }
189 else
190 n_items++;
191
192 i++;
193 }
194
195 should_have_separator = !section->is_fake && could_have_separator && n_items != 0;
196
197 if (should_have_separator > section->has_separator)
198 {
199 /* Add a separator */
200 GtkMenuTrackerItem *separator;
201
202 separator = _gtk_menu_tracker_item_new (observable: tracker->observable, model: parent_model, item_index: parent_index, FALSE, NULL, TRUE);
203 (* tracker->insert_func) (separator, offset, tracker->user_data);
204 g_object_unref (object: separator);
205
206 section->has_separator = TRUE;
207 }
208 else if (should_have_separator < section->has_separator)
209 {
210 /* Remove a separator */
211 (* tracker->remove_func) (offset, tracker->user_data);
212 section->has_separator = FALSE;
213 }
214
215 n_items += section->has_separator;
216
217 return n_items;
218}
219
220static void
221gtk_menu_tracker_item_visibility_changed (GtkMenuTrackerItem *item,
222 GParamSpec *pspec,
223 gpointer user_data)
224{
225 GtkMenuTracker *tracker = user_data;
226 GtkMenuTrackerSection *section;
227 gboolean is_now_visible;
228 gboolean was_visible;
229 int offset = 0;
230
231 is_now_visible = gtk_menu_tracker_item_get_is_visible (self: item);
232
233 /* remember: the item is our model */
234 section = gtk_menu_tracker_section_find_model (section: tracker->toplevel, model: item, offset: &offset);
235 g_assert (section);
236
237 was_visible = section->items != NULL;
238
239 if (is_now_visible == was_visible)
240 return;
241
242 if (is_now_visible)
243 {
244 section->items = g_slist_prepend (NULL, NULL);
245 (* tracker->insert_func) (section->model, offset, tracker->user_data);
246 }
247 else
248 {
249 section->items = g_slist_delete_link (list: section->items, link_: section->items);
250 (* tracker->remove_func) (offset, tracker->user_data);
251 }
252
253 gtk_menu_tracker_section_sync_separators (section: tracker->toplevel, tracker, offset: 0, FALSE, NULL, parent_index: 0);
254}
255
256static int
257gtk_menu_tracker_section_measure (GtkMenuTrackerSection *section)
258{
259 GSList *item;
260 int n_items;
261
262 if (section == NULL)
263 return 1;
264
265 n_items = 0;
266
267 if (section->has_separator)
268 n_items++;
269
270 for (item = section->items; item; item = item->next)
271 n_items += gtk_menu_tracker_section_measure (section: item->data);
272
273 return n_items;
274}
275
276static void
277gtk_menu_tracker_remove_items (GtkMenuTracker *tracker,
278 GSList **change_point,
279 int offset,
280 int n_items)
281{
282 int i;
283
284 for (i = 0; i < n_items; i++)
285 {
286 GtkMenuTrackerSection *subsection;
287 int n;
288
289 subsection = (*change_point)->data;
290 *change_point = g_slist_delete_link (list: *change_point, link_: *change_point);
291
292 n = gtk_menu_tracker_section_measure (section: subsection);
293 gtk_menu_tracker_section_free (section: subsection);
294
295 while (n--)
296 (* tracker->remove_func) (offset, tracker->user_data);
297 }
298}
299
300static void
301gtk_menu_tracker_add_items (GtkMenuTracker *tracker,
302 GtkMenuTrackerSection *section,
303 GSList **change_point,
304 int offset,
305 GMenuModel *model,
306 int position,
307 int n_items)
308{
309 while (n_items--)
310 {
311 GMenuModel *submenu;
312
313 submenu = g_menu_model_get_item_link (model, item_index: position + n_items, G_MENU_LINK_SECTION);
314 g_assert (submenu != model);
315
316 if (submenu != NULL && tracker->merge_sections)
317 {
318 GtkMenuTrackerSection *subsection;
319 char *action_namespace = NULL;
320 gboolean has_label;
321
322 has_label = g_menu_model_get_item_attribute (model, item_index: position + n_items,
323 G_MENU_ATTRIBUTE_LABEL, format_string: "s", NULL);
324
325 g_menu_model_get_item_attribute (model, item_index: position + n_items,
326 G_MENU_ATTRIBUTE_ACTION_NAMESPACE, format_string: "s", &action_namespace);
327
328 if (section->action_namespace)
329 {
330 char *namespace;
331
332 namespace = g_strjoin (separator: ".", section->action_namespace, action_namespace, NULL);
333 subsection = gtk_menu_tracker_section_new (tracker, model: submenu, FALSE, separator_label: has_label, offset, action_namespace: namespace);
334 g_free (mem: namespace);
335 }
336 else
337 subsection = gtk_menu_tracker_section_new (tracker, model: submenu, FALSE, separator_label: has_label, offset, action_namespace);
338
339 *change_point = g_slist_prepend (list: *change_point, data: subsection);
340 g_free (mem: action_namespace);
341 g_object_unref (object: submenu);
342 }
343 else
344 {
345 GtkMenuTrackerItem *item;
346
347 item = _gtk_menu_tracker_item_new (observable: tracker->observable, model, item_index: position + n_items,
348 mac_os_mode: tracker->mac_os_mode,
349 action_namespace: section->action_namespace, is_separator: submenu != NULL);
350
351 /* In the case that the item may disappear we handle that by
352 * treating the item that we just created as being its own
353 * subsection. This happens as so:
354 *
355 * - the subsection is created without the possibility of
356 * showing a separator
357 *
358 * - the subsection will have either 0 or 1 item in it at all
359 * times: either the shown item or not (in the case it is
360 * hidden)
361 *
362 * - the created item acts as the "model" for this section
363 * and we use its "visiblity-changed" signal in the same
364 * way that we use the "items-changed" signal from a real
365 * GMenuModel
366 *
367 * We almost never use the '->model' stored in the section for
368 * anything other than lookups and for dropped the ref and
369 * disconnecting the signal when we destroy the menu, and we
370 * need to do exactly those things in this case as well.
371 *
372 * The only other thing that '->model' is used for is in the
373 * case that we want to show a separator, but we will never do
374 * that because separators are not shown for this fake section.
375 */
376 if (gtk_menu_tracker_item_may_disappear (self: item))
377 {
378 GtkMenuTrackerSection *fake_section;
379
380 fake_section = g_slice_new0 (GtkMenuTrackerSection);
381 fake_section->is_fake = TRUE;
382 fake_section->model = g_object_ref (item);
383 fake_section->handler = g_signal_connect (item, "notify::is-visible",
384 G_CALLBACK (gtk_menu_tracker_item_visibility_changed),
385 tracker);
386 *change_point = g_slist_prepend (list: *change_point, data: fake_section);
387
388 if (gtk_menu_tracker_item_get_is_visible (self: item))
389 {
390 (* tracker->insert_func) (item, offset, tracker->user_data);
391 fake_section->items = g_slist_prepend (NULL, NULL);
392 }
393 }
394 else
395 {
396 /* In the normal case, we store NULL in the linked list.
397 * The measurement and lookup code count NULL always as
398 * exactly 1: an item that will always be there.
399 */
400 (* tracker->insert_func) (item, offset, tracker->user_data);
401 *change_point = g_slist_prepend (list: *change_point, NULL);
402 }
403
404 g_object_unref (object: item);
405 }
406 }
407}
408
409static void
410gtk_menu_tracker_model_changed (GMenuModel *model,
411 int position,
412 int removed,
413 int added,
414 gpointer user_data)
415{
416 GtkMenuTracker *tracker = user_data;
417 GtkMenuTrackerSection *section;
418 GSList **change_point;
419 int offset = 0;
420 int i;
421
422 /* First find which section the changed model corresponds to, and the
423 * position of that section within the overall menu.
424 */
425 section = gtk_menu_tracker_section_find_model (section: tracker->toplevel, model, offset: &offset);
426 g_assert (section);
427
428 /* Next, seek through that section to the change point. This gives us
429 * the correct GSList** to make the change to and also finds the final
430 * offset at which we will make the changes (by measuring the number
431 * of items within each item of the section before the change point).
432 */
433 change_point = &section->items;
434 for (i = 0; i < position; i++)
435 {
436 offset += gtk_menu_tracker_section_measure (section: (*change_point)->data);
437 change_point = &(*change_point)->next;
438 }
439
440 /* We remove items in order and add items in reverse order. This
441 * means that the offset used for all inserts and removes caused by a
442 * single change will be the same.
443 *
444 * This also has a performance advantage: GtkMenuShell stores the
445 * menu items in a linked list. In the case where we are creating a
446 * menu for the first time, adding the items in reverse order means
447 * that we only ever insert at index zero, prepending the list. This
448 * means that we can populate in O(n) time instead of O(n^2) that we
449 * would do by appending.
450 */
451 gtk_menu_tracker_remove_items (tracker, change_point, offset, n_items: removed);
452 gtk_menu_tracker_add_items (tracker, section, change_point, offset, model, position, n_items: added);
453
454 /* The offsets for insertion/removal of separators will be all over
455 * the place, however...
456 */
457 gtk_menu_tracker_section_sync_separators (section: tracker->toplevel, tracker, offset: 0, FALSE, NULL, parent_index: 0);
458}
459
460static void
461gtk_menu_tracker_section_free (GtkMenuTrackerSection *section)
462{
463 if (section == NULL)
464 return;
465
466 g_signal_handler_disconnect (instance: section->model, handler_id: section->handler);
467 g_slist_free_full (list: section->items, free_func: (GDestroyNotify) gtk_menu_tracker_section_free);
468 g_free (mem: section->action_namespace);
469 g_object_unref (object: section->model);
470 g_slice_free (GtkMenuTrackerSection, section);
471}
472
473static GtkMenuTrackerSection *
474gtk_menu_tracker_section_new (GtkMenuTracker *tracker,
475 GMenuModel *model,
476 gboolean with_separators,
477 gboolean separator_label,
478 int offset,
479 const char *action_namespace)
480{
481 GtkMenuTrackerSection *section;
482
483 section = g_slice_new0 (GtkMenuTrackerSection);
484 section->model = g_object_ref (model);
485 section->with_separators = with_separators;
486 section->action_namespace = g_strdup (str: action_namespace);
487 section->separator_label = separator_label;
488
489 gtk_menu_tracker_add_items (tracker, section, change_point: &section->items, offset, model, position: 0, n_items: g_menu_model_get_n_items (model));
490 section->handler = g_signal_connect (model, "items-changed", G_CALLBACK (gtk_menu_tracker_model_changed), tracker);
491
492 return section;
493}
494
495/*< private >
496 * gtk_menu_tracker_new:
497 * @model: the model to flatten
498 * @with_separators: if the toplevel should have separators (ie: TRUE
499 * for menus, FALSE for menubars)
500 * @merge_sections: if sections should have their items merged in the
501 * usual way or reported only as separators (which can be queried to
502 * manually handle the items)
503 * @mac_os_mode: if this is on behalf of the Mac OS menubar
504 * @action_namespace: the passed-in action namespace
505 * @insert_func: insert callback
506 * @remove_func: remove callback
507 * @user_data user data for callbacks
508 *
509 * Creates a GtkMenuTracker for @model, holding a ref on @model for as
510 * long as the tracker is alive.
511 *
512 * This flattens out the model, merging sections and inserting
513 * separators where appropriate. It monitors for changes and performs
514 * updates on the fly. It also handles action_namespace for subsections
515 * (but you will need to handle it yourself for submenus).
516 *
517 * When the tracker is first created, @insert_func will be called many
518 * times to populate the menu with the initial contents of @model
519 * (unless it is empty), before gtk_menu_tracker_new() returns. For
520 * this reason, the menu that is using the tracker ought to be empty
521 * when it creates the tracker.
522 *
523 * Future changes to @model will result in more calls to @insert_func
524 * and @remove_func.
525 *
526 * The position argument to both functions is the linear 0-based
527 * position in the menu at which the item in question should be inserted
528 * or removed.
529 *
530 * For @insert_func, @model and @item_index are used to get the
531 * information about the menu item to insert. @action_namespace is the
532 * action namespace that actions referred to from that item should place
533 * themselves in. Note that if the item is a submenu and the
534 * “action-namespace” attribute is defined on the item, it will _not_ be
535 * applied to the @action_namespace argument as it is meant for the
536 * items inside of the submenu, not the submenu item itself.
537 *
538 * @is_separator is set to %TRUE in case the item being added is a
539 * separator. @model and @item_index will still be meaningfully set in
540 * this case -- to the section menu item corresponding to the separator.
541 * This is useful if the section specifies a label, for example. If
542 * there is an “action-namespace” attribute on this menu item then it
543 * should be ignored by the consumer because GtkMenuTracker has already
544 * handled it.
545 *
546 * When using GtkMenuTracker there is no need to hold onto @model or
547 * monitor it for changes. The model will be unreffed when
548 * gtk_menu_tracker_free() is called.
549 */
550GtkMenuTracker *
551gtk_menu_tracker_new (GtkActionObservable *observable,
552 GMenuModel *model,
553 gboolean with_separators,
554 gboolean merge_sections,
555 gboolean mac_os_mode,
556 const char *action_namespace,
557 GtkMenuTrackerInsertFunc insert_func,
558 GtkMenuTrackerRemoveFunc remove_func,
559 gpointer user_data)
560{
561 GtkMenuTracker *tracker;
562
563 tracker = g_slice_new (GtkMenuTracker);
564 tracker->merge_sections = merge_sections;
565 tracker->mac_os_mode = mac_os_mode;
566 tracker->observable = g_object_ref (observable);
567 tracker->insert_func = insert_func;
568 tracker->remove_func = remove_func;
569 tracker->user_data = user_data;
570
571 tracker->toplevel = gtk_menu_tracker_section_new (tracker, model, with_separators, FALSE, offset: 0, action_namespace);
572 gtk_menu_tracker_section_sync_separators (section: tracker->toplevel, tracker, offset: 0, FALSE, NULL, parent_index: 0);
573
574 return tracker;
575}
576
577GtkMenuTracker *
578gtk_menu_tracker_new_for_item_link (GtkMenuTrackerItem *item,
579 const char *link_name,
580 gboolean merge_sections,
581 gboolean mac_os_mode,
582 GtkMenuTrackerInsertFunc insert_func,
583 GtkMenuTrackerRemoveFunc remove_func,
584 gpointer user_data)
585{
586 GtkMenuTracker *tracker;
587 GMenuModel *submenu;
588 char *namespace;
589
590 submenu = _gtk_menu_tracker_item_get_link (self: item, link_name);
591 namespace = _gtk_menu_tracker_item_get_link_namespace (self: item);
592
593 tracker = gtk_menu_tracker_new (observable: _gtk_menu_tracker_item_get_observable (self: item), model: submenu,
594 TRUE, merge_sections, mac_os_mode,
595 action_namespace: namespace, insert_func, remove_func, user_data);
596
597 g_object_unref (object: submenu);
598 g_free (mem: namespace);
599
600 return tracker;
601}
602
603/*< private >
604 * gtk_menu_tracker_free:
605 * @tracker: a GtkMenuTracker
606 *
607 * Frees the tracker, ...
608 */
609void
610gtk_menu_tracker_free (GtkMenuTracker *tracker)
611{
612 gtk_menu_tracker_section_free (section: tracker->toplevel);
613 g_object_unref (object: tracker->observable);
614 g_slice_free (GtkMenuTracker, tracker);
615}
616

source code of gtk/gtk/gtkmenutracker.c