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 | |
56 | typedef struct _GtkMenuTrackerSection ; |
57 | |
58 | struct |
59 | { |
60 | GtkActionObservable *; |
61 | guint : 1; |
62 | guint : 1; |
63 | GtkMenuTrackerInsertFunc ; |
64 | GtkMenuTrackerRemoveFunc ; |
65 | gpointer ; |
66 | |
67 | GtkMenuTrackerSection *; |
68 | }; |
69 | |
70 | struct |
71 | { |
72 | gpointer ; /* may be a GtkMenuTrackerItem or a GMenuModel */ |
73 | GSList *; |
74 | char *; |
75 | |
76 | guint : 1; |
77 | guint : 1; |
78 | guint : 1; |
79 | guint : 1; |
80 | |
81 | gulong handler; |
82 | }; |
83 | |
84 | static 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); |
90 | static void gtk_menu_tracker_section_free (GtkMenuTrackerSection *section); |
91 | |
92 | static GtkMenuTrackerSection * |
93 | (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 | */ |
160 | static int |
161 | (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 | |
220 | static void |
221 | (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 | |
256 | static int |
257 | (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 | |
276 | static void |
277 | (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 | |
300 | static void |
301 | (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 *; |
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 | |
409 | static void |
410 | (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 = §ion->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 | |
460 | static void |
461 | (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 | |
473 | static GtkMenuTrackerSection * |
474 | (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: §ion->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 | */ |
550 | GtkMenuTracker * |
551 | (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 | |
577 | GtkMenuTracker * |
578 | (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 *; |
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 | */ |
609 | void |
610 | (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 | |