1 | /* |
2 | * Copyright © 2011 Canonical Ltd. |
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.1 of the License, or (at your option) any later version. |
8 | * |
9 | * This library is distributed in the hope that it will be useful, but |
10 | * 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 "gdbusmenumodel.h" |
23 | |
24 | #include "gmenumodel.h" |
25 | |
26 | /* Prelude {{{1 */ |
27 | |
28 | /** |
29 | * SECTION:gdbusmenumodel |
30 | * @title: GDBusMenuModel |
31 | * @short_description: A D-Bus GMenuModel implementation |
32 | * @include: gio/gio.h |
33 | * @see_also: [GMenuModel Exporter][gio-GMenuModel-exporter] |
34 | * |
35 | * #GDBusMenuModel is an implementation of #GMenuModel that can be used |
36 | * as a proxy for a menu model that is exported over D-Bus with |
37 | * g_dbus_connection_export_menu_model(). |
38 | */ |
39 | |
40 | /** |
41 | * GDBusMenuModel: |
42 | * |
43 | * #GDBusMenuModel is an opaque data structure and can only be accessed |
44 | * using the following functions. |
45 | */ |
46 | |
47 | /* |
48 | * There are 3 main (quasi-)classes involved here: |
49 | * |
50 | * - GDBusMenuPath |
51 | * - GDBusMenuGroup |
52 | * - GDBusMenuModel |
53 | * |
54 | * Each of these classes exists as a parameterised singleton keyed to a |
55 | * particular thing: |
56 | * |
57 | * - GDBusMenuPath represents a D-Bus object path on a particular |
58 | * unique bus name on a particular GDBusConnection and in a |
59 | * particular GMainContext. |
60 | * |
61 | * - GDBusMenuGroup represents a particular group on a particular |
62 | * GDBusMenuPath. |
63 | * |
64 | * - GDBusMenuModel represents a particular menu within a particular |
65 | * GDBusMenuGroup. |
66 | * |
67 | * There are also two (and a half) utility structs: |
68 | * |
69 | * - PathIdentifier and ConstPathIdentifier |
70 | * - GDBusMenuModelItem |
71 | * |
72 | * PathIdentifier is the 4-tuple of (GMainContext, GDBusConnection, |
73 | * unique name, object path) that uniquely identifies a particular |
74 | * GDBusMenuPath. It holds ownership on each of these things, so we |
75 | * have a ConstPathIdentifier variant that does not. |
76 | * |
77 | * We have a 3-level hierarchy of hashtables: |
78 | * |
79 | * - a global hashtable (g_dbus_menu_paths) maps from PathIdentifier |
80 | * to GDBusMenuPath |
81 | * |
82 | * - each GDBusMenuPath has a hashtable mapping from guint (group |
83 | * number) to GDBusMenuGroup |
84 | * |
85 | * - each GDBusMenuGroup has a hashtable mapping from guint (menu |
86 | * number) to GDBusMenuModel. |
87 | * |
88 | * In this way, each quintuplet of (connection, bus name, object path, |
89 | * group id, menu id) maps to a single GDBusMenuModel instance that can be |
90 | * located via 3 hashtable lookups. |
91 | * |
92 | * All of the 3 classes are refcounted (GDBusMenuPath and |
93 | * GDBusMenuGroup manually, and GDBusMenuModel by virtue of being a |
94 | * GObject). The hashtables do not hold references -- rather, when the |
95 | * last reference is dropped, the object is removed from the hashtable. |
96 | * |
97 | * The hard references go in the other direction: GDBusMenuModel is created |
98 | * as the user requests it and only exists as long as the user holds a |
99 | * reference on it. GDBusMenuModel holds a reference on the GDBusMenuGroup |
100 | * from which it came. GDBusMenuGroup holds a reference on |
101 | * GDBusMenuPath. |
102 | * |
103 | * In addition to refcounts, each object has an 'active' variable (ints |
104 | * for GDBusMenuPath and GDBusMenuGroup, boolean for GDBusMenuModel). |
105 | * |
106 | * - GDBusMenuModel is inactive when created and becomes active only when |
107 | * first queried for information. This prevents extra work from |
108 | * happening just by someone acquiring a GDBusMenuModel (and not |
109 | * actually trying to display it yet). |
110 | * |
111 | * - The active count on GDBusMenuGroup is equal to the number of |
112 | * GDBusMenuModel instances in that group that are active. When the |
113 | * active count transitions from 0 to 1, the group calls the 'Start' |
114 | * method on the service to begin monitoring that group. When it |
115 | * drops from 1 to 0, the group calls the 'End' method to stop |
116 | * monitoring. |
117 | * |
118 | * - The active count on GDBusMenuPath is equal to the number of |
119 | * GDBusMenuGroup instances on that path with a non-zero active |
120 | * count. When the active count transitions from 0 to 1, the path |
121 | * sets up a signal subscription to monitor any changes. The signal |
122 | * subscription is taken down when the active count transitions from |
123 | * 1 to 0. |
124 | * |
125 | * When active, GDBusMenuPath gets incoming signals when changes occur. |
126 | * If the change signal mentions a group for which we currently have an |
127 | * active GDBusMenuGroup, the change signal is passed along to that |
128 | * group. If the group is inactive, the change signal is ignored. |
129 | * |
130 | * Most of the "work" occurs in GDBusMenuGroup. In addition to the |
131 | * hashtable of GDBusMenuModel instances, it keeps a hashtable of the actual |
132 | * menu contents, each encoded as GSequence of GDBusMenuModelItem. It |
133 | * initially populates this table with the results of the "Start" method |
134 | * call and then updates it according to incoming change signals. If |
135 | * the change signal mentions a menu for which we current have an active |
136 | * GDBusMenuModel, the change signal is passed along to that model. If the |
137 | * model is inactive, the change signal is ignored. |
138 | * |
139 | * GDBusMenuModelItem is just a pair of hashtables, one for the attributes |
140 | * and one for the links of the item. Both map strings to GVariant |
141 | * instances. In the case of links, the GVariant has type '(uu)' and is |
142 | * turned into a GDBusMenuModel at the point that the user pulls it through |
143 | * the API. |
144 | * |
145 | * Following the "empty is the same as non-existent" rule, the hashtable |
146 | * of GSequence of GDBusMenuModelItem holds NULL for empty menus. |
147 | * |
148 | * GDBusMenuModel contains very little functionality of its own. It holds a |
149 | * (weak) reference to the GSequence of GDBusMenuModelItem contained in the |
150 | * GDBusMenuGroup. It uses this GSequence to implement the GMenuModel |
151 | * interface. It also emits the "items-changed" signal if it is active |
152 | * and it was told that the contents of the GSequence changed. |
153 | */ |
154 | |
155 | typedef struct _GDBusMenuGroup ; |
156 | typedef struct _GDBusMenuPath ; |
157 | |
158 | static void g_dbus_menu_group_changed (GDBusMenuGroup *group, |
159 | guint , |
160 | gint position, |
161 | gint removed, |
162 | GVariant *added); |
163 | static void g_dbus_menu_model_changed (GDBusMenuModel *proxy, |
164 | GSequence *items, |
165 | gint position, |
166 | gint removed, |
167 | gint added); |
168 | static GDBusMenuGroup * g_dbus_menu_group_get_from_path (GDBusMenuPath *path, |
169 | guint group_id); |
170 | static GDBusMenuModel * g_dbus_menu_model_get_from_group (GDBusMenuGroup *group, |
171 | guint ); |
172 | |
173 | /* PathIdentifier {{{1 */ |
174 | typedef struct |
175 | { |
176 | GMainContext *context; |
177 | GDBusConnection *connection; |
178 | gchar *bus_name; |
179 | gchar *object_path; |
180 | } PathIdentifier; |
181 | |
182 | typedef const struct |
183 | { |
184 | GMainContext *context; |
185 | GDBusConnection *connection; |
186 | const gchar *bus_name; |
187 | const gchar *object_path; |
188 | } ConstPathIdentifier; |
189 | |
190 | static guint |
191 | path_identifier_hash (gconstpointer data) |
192 | { |
193 | ConstPathIdentifier *id = data; |
194 | |
195 | return g_str_hash (v: id->object_path); |
196 | } |
197 | |
198 | static gboolean |
199 | path_identifier_equal (gconstpointer a, |
200 | gconstpointer b) |
201 | { |
202 | ConstPathIdentifier *id_a = a; |
203 | ConstPathIdentifier *id_b = b; |
204 | |
205 | return id_a->connection == id_b->connection && |
206 | g_strcmp0 (str1: id_a->bus_name, str2: id_b->bus_name) == 0 && |
207 | g_str_equal (v1: id_a->object_path, v2: id_b->object_path); |
208 | } |
209 | |
210 | static void |
211 | path_identifier_free (PathIdentifier *id) |
212 | { |
213 | g_main_context_unref (context: id->context); |
214 | g_object_unref (object: id->connection); |
215 | g_free (mem: id->bus_name); |
216 | g_free (mem: id->object_path); |
217 | |
218 | g_slice_free (PathIdentifier, id); |
219 | } |
220 | |
221 | static PathIdentifier * |
222 | path_identifier_new (ConstPathIdentifier *cid) |
223 | { |
224 | PathIdentifier *id; |
225 | |
226 | id = g_slice_new (PathIdentifier); |
227 | id->context = g_main_context_ref (context: cid->context); |
228 | id->connection = g_object_ref (cid->connection); |
229 | id->bus_name = g_strdup (str: cid->bus_name); |
230 | id->object_path = g_strdup (str: cid->object_path); |
231 | |
232 | return id; |
233 | } |
234 | |
235 | /* GDBusMenuPath {{{1 */ |
236 | |
237 | struct |
238 | { |
239 | PathIdentifier *; |
240 | gint ; |
241 | |
242 | GHashTable *; |
243 | gint ; |
244 | guint ; |
245 | }; |
246 | |
247 | static GHashTable *; |
248 | |
249 | static GDBusMenuPath * |
250 | (GDBusMenuPath *path) |
251 | { |
252 | path->ref_count++; |
253 | |
254 | return path; |
255 | } |
256 | |
257 | static void |
258 | (GDBusMenuPath *path) |
259 | { |
260 | if (--path->ref_count == 0) |
261 | { |
262 | g_hash_table_remove (hash_table: g_dbus_menu_paths, key: path->id); |
263 | g_hash_table_unref (hash_table: path->groups); |
264 | path_identifier_free (id: path->id); |
265 | |
266 | g_slice_free (GDBusMenuPath, path); |
267 | } |
268 | } |
269 | |
270 | static void |
271 | (GDBusConnection *connection, |
272 | const gchar *sender_name, |
273 | const gchar *object_path, |
274 | const gchar *interface_name, |
275 | const gchar *signal_name, |
276 | GVariant *parameters, |
277 | gpointer user_data) |
278 | { |
279 | GDBusMenuPath *path = user_data; |
280 | GVariantIter *iter; |
281 | guint group_id; |
282 | guint ; |
283 | guint position; |
284 | guint removes; |
285 | GVariant *adds; |
286 | |
287 | if (!g_variant_is_of_type (value: parameters, G_VARIANT_TYPE ("(a(uuuuaa{sv}))" ))) |
288 | return; |
289 | |
290 | g_variant_get (value: parameters, format_string: "(a(uuuuaa{sv}))" , &iter); |
291 | while (g_variant_iter_loop (iter, format_string: "(uuuu@aa{sv})" , &group_id, &menu_id, &position, &removes, &adds)) |
292 | { |
293 | GDBusMenuGroup *group; |
294 | |
295 | group = g_hash_table_lookup (hash_table: path->groups, GINT_TO_POINTER (group_id)); |
296 | |
297 | if (group != NULL) |
298 | g_dbus_menu_group_changed (group, menu_id, position, removed: removes, added: adds); |
299 | } |
300 | g_variant_iter_free (iter); |
301 | } |
302 | |
303 | static void |
304 | (GDBusMenuPath *path) |
305 | { |
306 | if (path->active++ == 0) |
307 | path->watch_id = g_dbus_connection_signal_subscribe (connection: path->id->connection, sender: path->id->bus_name, |
308 | interface_name: "org.gtk.Menus" , member: "Changed" , object_path: path->id->object_path, |
309 | NULL, flags: G_DBUS_SIGNAL_FLAGS_NONE, |
310 | callback: g_dbus_menu_path_signal, user_data: path, NULL); |
311 | } |
312 | |
313 | static void |
314 | (GDBusMenuPath *path) |
315 | { |
316 | if (--path->active == 0) |
317 | g_dbus_connection_signal_unsubscribe (connection: path->id->connection, subscription_id: path->watch_id); |
318 | } |
319 | |
320 | static GDBusMenuPath * |
321 | (GMainContext *context, |
322 | GDBusConnection *connection, |
323 | const gchar *bus_name, |
324 | const gchar *object_path) |
325 | { |
326 | ConstPathIdentifier cid = { context, connection, bus_name, object_path }; |
327 | GDBusMenuPath *path; |
328 | |
329 | if (g_dbus_menu_paths == NULL) |
330 | g_dbus_menu_paths = g_hash_table_new (hash_func: path_identifier_hash, key_equal_func: path_identifier_equal); |
331 | |
332 | path = g_hash_table_lookup (hash_table: g_dbus_menu_paths, key: &cid); |
333 | |
334 | if (path == NULL) |
335 | { |
336 | path = g_slice_new (GDBusMenuPath); |
337 | path->id = path_identifier_new (cid: &cid); |
338 | path->groups = g_hash_table_new (NULL, NULL); |
339 | path->ref_count = 0; |
340 | path->active = 0; |
341 | |
342 | g_hash_table_insert (hash_table: g_dbus_menu_paths, key: path->id, value: path); |
343 | } |
344 | |
345 | return g_dbus_menu_path_ref (path); |
346 | } |
347 | |
348 | /* GDBusMenuGroup, GDBusMenuModelItem {{{1 */ |
349 | typedef enum |
350 | { |
351 | GROUP_OFFLINE, |
352 | GROUP_PENDING, |
353 | GROUP_ONLINE |
354 | } GroupStatus; |
355 | |
356 | struct |
357 | { |
358 | GDBusMenuPath *; |
359 | guint ; |
360 | |
361 | GHashTable *; /* uint -> unowned GDBusMenuModel */ |
362 | GHashTable *; /* uint -> owned GSequence */ |
363 | gint ; |
364 | GroupStatus ; |
365 | gint ; |
366 | }; |
367 | |
368 | typedef struct |
369 | { |
370 | GHashTable *; |
371 | GHashTable *; |
372 | } ; |
373 | |
374 | static GDBusMenuGroup * |
375 | (GDBusMenuGroup *group) |
376 | { |
377 | group->ref_count++; |
378 | |
379 | return group; |
380 | } |
381 | |
382 | static void |
383 | (GDBusMenuGroup *group) |
384 | { |
385 | if (--group->ref_count == 0) |
386 | { |
387 | g_assert (group->state == GROUP_OFFLINE); |
388 | g_assert (group->active == 0); |
389 | |
390 | g_hash_table_remove (hash_table: group->path->groups, GINT_TO_POINTER (group->id)); |
391 | g_hash_table_unref (hash_table: group->proxies); |
392 | g_hash_table_unref (hash_table: group->menus); |
393 | |
394 | g_dbus_menu_path_unref (path: group->path); |
395 | |
396 | g_slice_free (GDBusMenuGroup, group); |
397 | } |
398 | } |
399 | |
400 | static void |
401 | (gpointer data) |
402 | { |
403 | GDBusMenuModelItem *item = data; |
404 | |
405 | g_hash_table_unref (hash_table: item->attributes); |
406 | g_hash_table_unref (hash_table: item->links); |
407 | |
408 | g_slice_free (GDBusMenuModelItem, item); |
409 | } |
410 | |
411 | static GDBusMenuModelItem * |
412 | (GVariant *description) |
413 | { |
414 | GDBusMenuModelItem *item; |
415 | GVariantIter iter; |
416 | const gchar *key; |
417 | GVariant *value; |
418 | |
419 | item = g_slice_new (GDBusMenuModelItem); |
420 | item->attributes = g_hash_table_new_full (hash_func: g_str_hash, key_equal_func: g_str_equal, key_destroy_func: g_free, value_destroy_func: (GDestroyNotify) g_variant_unref); |
421 | item->links = g_hash_table_new_full (hash_func: g_str_hash, key_equal_func: g_str_equal, key_destroy_func: g_free, value_destroy_func: (GDestroyNotify) g_variant_unref); |
422 | |
423 | g_variant_iter_init (iter: &iter, value: description); |
424 | while (g_variant_iter_loop (iter: &iter, format_string: "{&sv}" , &key, &value)) |
425 | if (key[0] == ':') |
426 | /* key + 1 to skip the ':' */ |
427 | g_hash_table_insert (hash_table: item->links, key: g_strdup (str: key + 1), value: g_variant_ref (value)); |
428 | else |
429 | g_hash_table_insert (hash_table: item->attributes, key: g_strdup (str: key), value: g_variant_ref (value)); |
430 | |
431 | return item; |
432 | } |
433 | |
434 | /* |
435 | * GDBusMenuGroup can be in three states: |
436 | * |
437 | * OFFLINE: not subscribed to this group |
438 | * PENDING: we made the call to subscribe to this group, but the result |
439 | * has not come back yet |
440 | * ONLINE: we are fully subscribed |
441 | * |
442 | * We can get into some nasty situations where we make a call due to an |
443 | * activation request but receive a deactivation request before the call |
444 | * returns. If another activation request occurs then we could risk |
445 | * sending a Start request even though one is already in progress. For |
446 | * this reason, we have to carefully consider what to do in each of the |
447 | * three states for each of the following situations: |
448 | * |
449 | * - activation requested |
450 | * - deactivation requested |
451 | * - Start call finishes |
452 | * |
453 | * To simplify things a bit, we do not have a callback for the Stop |
454 | * call. We just send it and assume that it takes effect immediately. |
455 | * |
456 | * Activation requested: |
457 | * OFFLINE: make the Start call and transition to PENDING |
458 | * PENDING: do nothing -- call is already in progress. |
459 | * ONLINE: this should not be possible |
460 | * |
461 | * Deactivation requested: |
462 | * OFFLINE: this should not be possible |
463 | * PENDING: do nothing -- handle it when the Start call finishes |
464 | * ONLINE: send the Stop call and move to OFFLINE immediately |
465 | * |
466 | * Start call finishes: |
467 | * OFFLINE: this should not be possible |
468 | * PENDING: |
469 | * If we should be active (ie: active count > 0): move to ONLINE |
470 | * If not: send Stop call and move to OFFLINE immediately |
471 | * ONLINE: this should not be possible |
472 | * |
473 | * We have to take care with regards to signal subscriptions (ie: |
474 | * activation of the GDBusMenuPath). The signal subscription is always |
475 | * established when transitioning from OFFLINE to PENDING and taken down |
476 | * when transitioning to OFFLINE (from either PENDING or ONLINE). |
477 | * |
478 | * Since there are two places where we transition to OFFLINE, we split |
479 | * that code out into a separate function. |
480 | */ |
481 | static void |
482 | (GDBusMenuGroup *group) |
483 | { |
484 | g_dbus_menu_path_deactivate (path: group->path); |
485 | g_dbus_connection_call (connection: group->path->id->connection, |
486 | bus_name: group->path->id->bus_name, |
487 | object_path: group->path->id->object_path, |
488 | interface_name: "org.gtk.Menus" , method_name: "End" , |
489 | parameters: g_variant_new_parsed (format: "([ %u ],)" , group->id), |
490 | NULL, flags: G_DBUS_CALL_FLAGS_NONE, timeout_msec: -1, |
491 | NULL, NULL, NULL); |
492 | group->state = GROUP_OFFLINE; |
493 | } |
494 | |
495 | |
496 | static void |
497 | (GObject *source_object, |
498 | GAsyncResult *result, |
499 | gpointer user_data) |
500 | { |
501 | GDBusConnection *connection = G_DBUS_CONNECTION (source_object); |
502 | GDBusMenuGroup *group = user_data; |
503 | GVariant *reply; |
504 | |
505 | g_assert (group->state == GROUP_PENDING); |
506 | |
507 | reply = g_dbus_connection_call_finish (connection, res: result, NULL); |
508 | |
509 | if (group->active) |
510 | { |
511 | group->state = GROUP_ONLINE; |
512 | |
513 | /* If we receive no reply, just act like we got an empty reply. */ |
514 | if (reply) |
515 | { |
516 | GVariantIter *iter; |
517 | GVariant *items; |
518 | guint group_id; |
519 | guint ; |
520 | |
521 | g_variant_get (value: reply, format_string: "(a(uuaa{sv}))" , &iter); |
522 | while (g_variant_iter_loop (iter, format_string: "(uu@aa{sv})" , &group_id, &menu_id, &items)) |
523 | if (group_id == group->id) |
524 | g_dbus_menu_group_changed (group, menu_id, position: 0, removed: 0, added: items); |
525 | g_variant_iter_free (iter); |
526 | } |
527 | } |
528 | else |
529 | g_dbus_menu_group_go_offline (group); |
530 | |
531 | if (reply) |
532 | g_variant_unref (value: reply); |
533 | |
534 | g_dbus_menu_group_unref (group); |
535 | } |
536 | |
537 | static void |
538 | (GDBusMenuGroup *group) |
539 | { |
540 | if (group->active++ == 0) |
541 | { |
542 | g_assert (group->state != GROUP_ONLINE); |
543 | |
544 | if (group->state == GROUP_OFFLINE) |
545 | { |
546 | g_dbus_menu_path_activate (path: group->path); |
547 | |
548 | g_dbus_connection_call (connection: group->path->id->connection, |
549 | bus_name: group->path->id->bus_name, |
550 | object_path: group->path->id->object_path, |
551 | interface_name: "org.gtk.Menus" , method_name: "Start" , |
552 | parameters: g_variant_new_parsed (format: "([ %u ],)" , group->id), |
553 | G_VARIANT_TYPE ("(a(uuaa{sv}))" ), |
554 | flags: G_DBUS_CALL_FLAGS_NONE, timeout_msec: -1, NULL, |
555 | callback: g_dbus_menu_group_start_ready, |
556 | user_data: g_dbus_menu_group_ref (group)); |
557 | group->state = GROUP_PENDING; |
558 | } |
559 | } |
560 | } |
561 | |
562 | static void |
563 | (GDBusMenuGroup *group) |
564 | { |
565 | if (--group->active == 0) |
566 | { |
567 | g_assert (group->state != GROUP_OFFLINE); |
568 | |
569 | if (group->state == GROUP_ONLINE) |
570 | { |
571 | /* We are here because nobody is watching, so just free |
572 | * everything and don't bother with the notifications. |
573 | */ |
574 | g_hash_table_remove_all (hash_table: group->menus); |
575 | |
576 | g_dbus_menu_group_go_offline (group); |
577 | } |
578 | } |
579 | } |
580 | |
581 | static void |
582 | (GDBusMenuGroup *group, |
583 | guint , |
584 | gint position, |
585 | gint removed, |
586 | GVariant *added) |
587 | { |
588 | GSequenceIter *point; |
589 | GVariantIter iter; |
590 | GDBusMenuModel *proxy; |
591 | GSequence *items; |
592 | GVariant *item; |
593 | gint n_added; |
594 | |
595 | /* We could have signals coming to us when we're not active (due to |
596 | * some other process having subscribed to this group) or when we're |
597 | * pending. In both of those cases, we want to ignore the signal |
598 | * since we'll get our own information when we call "Start" for |
599 | * ourselves. |
600 | */ |
601 | if (group->state != GROUP_ONLINE) |
602 | return; |
603 | |
604 | items = g_hash_table_lookup (hash_table: group->menus, GINT_TO_POINTER (menu_id)); |
605 | |
606 | if (items == NULL) |
607 | { |
608 | items = g_sequence_new (data_destroy: g_dbus_menu_model_item_free); |
609 | g_hash_table_insert (hash_table: group->menus, GINT_TO_POINTER (menu_id), value: items); |
610 | } |
611 | |
612 | point = g_sequence_get_iter_at_pos (seq: items, pos: position + removed); |
613 | |
614 | g_return_if_fail (point != NULL); |
615 | |
616 | if (removed) |
617 | { |
618 | GSequenceIter *start; |
619 | |
620 | start = g_sequence_get_iter_at_pos (seq: items, pos: position); |
621 | g_sequence_remove_range (begin: start, end: point); |
622 | } |
623 | |
624 | n_added = g_variant_iter_init (iter: &iter, value: added); |
625 | while (g_variant_iter_loop (iter: &iter, format_string: "@a{sv}" , &item)) |
626 | g_sequence_insert_before (iter: point, data: g_dbus_menu_group_create_item (description: item)); |
627 | |
628 | if (g_sequence_is_empty (seq: items)) |
629 | { |
630 | g_hash_table_remove (hash_table: group->menus, GINT_TO_POINTER (menu_id)); |
631 | items = NULL; |
632 | } |
633 | |
634 | if ((proxy = g_hash_table_lookup (hash_table: group->proxies, GINT_TO_POINTER (menu_id)))) |
635 | g_dbus_menu_model_changed (proxy, items, position, removed, added: n_added); |
636 | } |
637 | |
638 | static GDBusMenuGroup * |
639 | (GDBusMenuPath *path, |
640 | guint group_id) |
641 | { |
642 | GDBusMenuGroup *group; |
643 | |
644 | group = g_hash_table_lookup (hash_table: path->groups, GINT_TO_POINTER (group_id)); |
645 | |
646 | if (group == NULL) |
647 | { |
648 | group = g_slice_new (GDBusMenuGroup); |
649 | group->path = g_dbus_menu_path_ref (path); |
650 | group->id = group_id; |
651 | group->proxies = g_hash_table_new (NULL, NULL); |
652 | group->menus = g_hash_table_new_full (NULL, NULL, NULL, value_destroy_func: (GDestroyNotify) g_sequence_free); |
653 | group->state = GROUP_OFFLINE; |
654 | group->active = 0; |
655 | group->ref_count = 0; |
656 | |
657 | g_hash_table_insert (hash_table: path->groups, GINT_TO_POINTER (group->id), value: group); |
658 | } |
659 | |
660 | return g_dbus_menu_group_ref (group); |
661 | } |
662 | |
663 | static GDBusMenuGroup * |
664 | (GMainContext *context, |
665 | GDBusConnection *connection, |
666 | const gchar *bus_name, |
667 | const gchar *object_path, |
668 | guint group_id) |
669 | { |
670 | GDBusMenuGroup *group; |
671 | GDBusMenuPath *path; |
672 | |
673 | path = g_dbus_menu_path_get (context, connection, bus_name, object_path); |
674 | group = g_dbus_menu_group_get_from_path (path, group_id); |
675 | g_dbus_menu_path_unref (path); |
676 | |
677 | return group; |
678 | } |
679 | |
680 | /* GDBusMenuModel {{{1 */ |
681 | |
682 | typedef GMenuModelClass ; |
683 | struct |
684 | { |
685 | GMenuModel ; |
686 | |
687 | GDBusMenuGroup *; |
688 | guint ; |
689 | |
690 | GSequence *; /* unowned */ |
691 | gboolean ; |
692 | }; |
693 | |
694 | G_DEFINE_TYPE (GDBusMenuModel, g_dbus_menu_model, G_TYPE_MENU_MODEL) |
695 | |
696 | static gboolean |
697 | (GMenuModel *model) |
698 | { |
699 | return TRUE; |
700 | } |
701 | |
702 | static gint |
703 | (GMenuModel *model) |
704 | { |
705 | GDBusMenuModel *proxy = G_DBUS_MENU_MODEL (model); |
706 | |
707 | if (!proxy->active) |
708 | { |
709 | g_dbus_menu_group_activate (group: proxy->group); |
710 | proxy->active = TRUE; |
711 | } |
712 | |
713 | return proxy->items ? g_sequence_get_length (seq: proxy->items) : 0; |
714 | } |
715 | |
716 | static void |
717 | (GMenuModel *model, |
718 | gint item_index, |
719 | GHashTable **table) |
720 | { |
721 | GDBusMenuModel *proxy = G_DBUS_MENU_MODEL (model); |
722 | GDBusMenuModelItem *item; |
723 | GSequenceIter *iter; |
724 | |
725 | g_return_if_fail (proxy->active); |
726 | g_return_if_fail (proxy->items); |
727 | |
728 | iter = g_sequence_get_iter_at_pos (seq: proxy->items, pos: item_index); |
729 | g_return_if_fail (iter); |
730 | |
731 | item = g_sequence_get (iter); |
732 | g_return_if_fail (item); |
733 | |
734 | *table = g_hash_table_ref (hash_table: item->attributes); |
735 | } |
736 | |
737 | static void |
738 | (GMenuModel *model, |
739 | gint item_index, |
740 | GHashTable **table) |
741 | { |
742 | GDBusMenuModel *proxy = G_DBUS_MENU_MODEL (model); |
743 | GDBusMenuModelItem *item; |
744 | GSequenceIter *iter; |
745 | |
746 | g_return_if_fail (proxy->active); |
747 | g_return_if_fail (proxy->items); |
748 | |
749 | iter = g_sequence_get_iter_at_pos (seq: proxy->items, pos: item_index); |
750 | g_return_if_fail (iter); |
751 | |
752 | item = g_sequence_get (iter); |
753 | g_return_if_fail (item); |
754 | |
755 | *table = g_hash_table_new_full (hash_func: g_str_hash, key_equal_func: g_str_equal, key_destroy_func: g_free, value_destroy_func: g_object_unref); |
756 | |
757 | { |
758 | GHashTableIter tmp; |
759 | gpointer key; |
760 | gpointer value; |
761 | |
762 | g_hash_table_iter_init (iter: &tmp, hash_table: item->links); |
763 | while (g_hash_table_iter_next (iter: &tmp, key: &key, value: &value)) |
764 | { |
765 | if (g_variant_is_of_type (value, G_VARIANT_TYPE ("(uu)" ))) |
766 | { |
767 | guint group_id, ; |
768 | GDBusMenuGroup *group; |
769 | GDBusMenuModel *link; |
770 | |
771 | g_variant_get (value, format_string: "(uu)" , &group_id, &menu_id); |
772 | |
773 | /* save the hash lookup in a relatively common case */ |
774 | if (proxy->group->id != group_id) |
775 | group = g_dbus_menu_group_get_from_path (path: proxy->group->path, group_id); |
776 | else |
777 | group = g_dbus_menu_group_ref (group: proxy->group); |
778 | |
779 | link = g_dbus_menu_model_get_from_group (group, menu_id); |
780 | |
781 | g_hash_table_insert (hash_table: *table, key: g_strdup (str: key), value: link); |
782 | |
783 | g_dbus_menu_group_unref (group); |
784 | } |
785 | } |
786 | } |
787 | } |
788 | |
789 | static void |
790 | (GObject *object) |
791 | { |
792 | GDBusMenuModel *proxy = G_DBUS_MENU_MODEL (object); |
793 | |
794 | if (proxy->active) |
795 | g_dbus_menu_group_deactivate (group: proxy->group); |
796 | |
797 | g_hash_table_remove (hash_table: proxy->group->proxies, GINT_TO_POINTER (proxy->id)); |
798 | g_dbus_menu_group_unref (group: proxy->group); |
799 | |
800 | G_OBJECT_CLASS (g_dbus_menu_model_parent_class) |
801 | ->finalize (object); |
802 | } |
803 | |
804 | static void |
805 | (GDBusMenuModel *proxy) |
806 | { |
807 | } |
808 | |
809 | static void |
810 | (GDBusMenuModelClass *class) |
811 | { |
812 | GObjectClass *object_class = G_OBJECT_CLASS (class); |
813 | |
814 | class->is_mutable = g_dbus_menu_model_is_mutable; |
815 | class->get_n_items = g_dbus_menu_model_get_n_items; |
816 | class->get_item_attributes = g_dbus_menu_model_get_item_attributes; |
817 | class->get_item_links = g_dbus_menu_model_get_item_links; |
818 | |
819 | object_class->finalize = g_dbus_menu_model_finalize; |
820 | } |
821 | |
822 | static void |
823 | (GDBusMenuModel *proxy, |
824 | GSequence *items, |
825 | gint position, |
826 | gint removed, |
827 | gint added) |
828 | { |
829 | proxy->items = items; |
830 | |
831 | if (proxy->active && (removed || added)) |
832 | g_menu_model_items_changed (G_MENU_MODEL (proxy), position, removed, added); |
833 | } |
834 | |
835 | static GDBusMenuModel * |
836 | (GDBusMenuGroup *group, |
837 | guint ) |
838 | { |
839 | GDBusMenuModel *proxy; |
840 | |
841 | proxy = g_hash_table_lookup (hash_table: group->proxies, GINT_TO_POINTER (menu_id)); |
842 | if (proxy) |
843 | g_object_ref (proxy); |
844 | |
845 | if (proxy == NULL) |
846 | { |
847 | proxy = g_object_new (G_TYPE_DBUS_MENU_MODEL, NULL); |
848 | proxy->items = g_hash_table_lookup (hash_table: group->menus, GINT_TO_POINTER (menu_id)); |
849 | g_hash_table_insert (hash_table: group->proxies, GINT_TO_POINTER (menu_id), value: proxy); |
850 | proxy->group = g_dbus_menu_group_ref (group); |
851 | proxy->id = menu_id; |
852 | } |
853 | |
854 | return proxy; |
855 | } |
856 | |
857 | /** |
858 | * g_dbus_menu_model_get: |
859 | * @connection: a #GDBusConnection |
860 | * @bus_name: (nullable): the bus name which exports the menu model |
861 | * or %NULL if @connection is not a message bus connection |
862 | * @object_path: the object path at which the menu model is exported |
863 | * |
864 | * Obtains a #GDBusMenuModel for the menu model which is exported |
865 | * at the given @bus_name and @object_path. |
866 | * |
867 | * The thread default main context is taken at the time of this call. |
868 | * All signals on the menu model (and any linked models) are reported |
869 | * with respect to this context. All calls on the returned menu model |
870 | * (and linked models) must also originate from this same context, with |
871 | * the thread default main context unchanged. |
872 | * |
873 | * Returns: (transfer full): a #GDBusMenuModel object. Free with |
874 | * g_object_unref(). |
875 | * |
876 | * Since: 2.32 |
877 | */ |
878 | GDBusMenuModel * |
879 | (GDBusConnection *connection, |
880 | const gchar *bus_name, |
881 | const gchar *object_path) |
882 | { |
883 | GDBusMenuGroup *group; |
884 | GDBusMenuModel *proxy; |
885 | GMainContext *context; |
886 | |
887 | g_return_val_if_fail (bus_name != NULL || g_dbus_connection_get_unique_name (connection) == NULL, NULL); |
888 | |
889 | context = g_main_context_get_thread_default (); |
890 | if (context == NULL) |
891 | context = g_main_context_default (); |
892 | |
893 | group = g_dbus_menu_group_get (context, connection, bus_name, object_path, group_id: 0); |
894 | proxy = g_dbus_menu_model_get_from_group (group, menu_id: 0); |
895 | g_dbus_menu_group_unref (group); |
896 | |
897 | return proxy; |
898 | } |
899 | |
900 | #if 0 |
901 | static void |
902 | dump_proxy (gpointer key, gpointer value, gpointer data) |
903 | { |
904 | GDBusMenuModel *proxy = value; |
905 | |
906 | g_print (" menu %d refcount %d active %d\n" , |
907 | proxy->id, G_OBJECT (proxy)->ref_count, proxy->active); |
908 | } |
909 | |
910 | static void |
911 | dump_group (gpointer key, gpointer value, gpointer data) |
912 | { |
913 | GDBusMenuGroup *group = value; |
914 | |
915 | g_print (" group %d refcount %d state %d active %d\n" , |
916 | group->id, group->ref_count, group->state, group->active); |
917 | |
918 | g_hash_table_foreach (group->proxies, dump_proxy, NULL); |
919 | } |
920 | |
921 | static void |
922 | dump_path (gpointer key, gpointer value, gpointer data) |
923 | { |
924 | PathIdentifier *pid = key; |
925 | GDBusMenuPath *path = value; |
926 | |
927 | g_print ("%s active %d\n" , pid->object_path, path->active); |
928 | g_hash_table_foreach (path->groups, dump_group, NULL); |
929 | } |
930 | |
931 | void |
932 | g_dbus_menu_model_dump (void) |
933 | { |
934 | g_hash_table_foreach (g_dbus_menu_paths, dump_path, NULL); |
935 | } |
936 | |
937 | #endif |
938 | |
939 | /* Epilogue {{{1 */ |
940 | /* vim:set foldmethod=marker: */ |
941 | |