1 | /* gtkatspicache.c: AT-SPI object cache |
2 | * |
3 | * Copyright 2020 holder |
4 | * |
5 | * SPDX-License-Identifier: LGPL-2.1-or-later |
6 | * |
7 | * This library is free software; you can redistribute it and/or |
8 | * modify it under the terms of the GNU Lesser General Public |
9 | * License as published by the Free Software Foundation; either |
10 | * version 2.1 of the License, or (at your option) any later version. |
11 | * |
12 | * This library is distributed in the hope that it will be useful, |
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
15 | * Lesser General Public License for more details. |
16 | * |
17 | * You should have received a copy of the GNU Lesser General Public |
18 | * License along with this library; if not, see <http://www.gnu.org/licenses/>. |
19 | */ |
20 | |
21 | #include "config.h" |
22 | |
23 | #include "gtkatspicacheprivate.h" |
24 | |
25 | #include "gtkatspicontextprivate.h" |
26 | #include "gtkatspirootprivate.h" |
27 | #include "gtkatspiutilsprivate.h" |
28 | #include "gtkdebug.h" |
29 | |
30 | #include "a11y/atspi/atspi-accessible.h" |
31 | #include "a11y/atspi/atspi-application.h" |
32 | #include "a11y/atspi/atspi-cache.h" |
33 | |
34 | /* Cached item: |
35 | * |
36 | * (so): object ref |
37 | * (so): application ref |
38 | * (so): parent ref |
39 | * - parent.role == application ? desktop ref : null ref |
40 | * i: index in parent, or -1 for transient widgets/menu items |
41 | * i: child count, or -1 for defunct/menus |
42 | * as: interfaces |
43 | * s: name |
44 | * u: role |
45 | * s: description |
46 | * au: state set |
47 | */ |
48 | #define ITEM_SIGNATURE "(so)(so)(so)iiassusau" |
49 | #define GET_ITEMS_SIGNATURE "a(" ITEM_SIGNATURE ")" |
50 | |
51 | struct _GtkAtSpiCache |
52 | { |
53 | GObject parent_instance; |
54 | |
55 | char *cache_path; |
56 | GDBusConnection *connection; |
57 | |
58 | /* HashTable<str, GtkAtSpiContext> */ |
59 | GHashTable *contexts_by_path; |
60 | |
61 | /* HashTable<GtkAtSpiContext, str> */ |
62 | GHashTable *contexts_to_path; |
63 | |
64 | /* Re-entrancy guard */ |
65 | gboolean in_get_items; |
66 | |
67 | GtkAtSpiRoot *root; |
68 | }; |
69 | |
70 | enum |
71 | { |
72 | PROP_CACHE_PATH = 1, |
73 | PROP_CONNECTION, |
74 | |
75 | N_PROPS |
76 | }; |
77 | |
78 | static GParamSpec *obj_props[N_PROPS]; |
79 | |
80 | G_DEFINE_TYPE (GtkAtSpiCache, gtk_at_spi_cache, G_TYPE_OBJECT) |
81 | |
82 | static void |
83 | gtk_at_spi_cache_finalize (GObject *gobject) |
84 | { |
85 | GtkAtSpiCache *self = GTK_AT_SPI_CACHE (ptr: gobject); |
86 | |
87 | g_clear_pointer (&self->contexts_to_path, g_hash_table_unref); |
88 | g_clear_pointer (&self->contexts_by_path, g_hash_table_unref); |
89 | g_clear_object (&self->connection); |
90 | g_free (mem: self->cache_path); |
91 | |
92 | G_OBJECT_CLASS (gtk_at_spi_cache_parent_class)->finalize (gobject); |
93 | } |
94 | |
95 | static void |
96 | gtk_at_spi_cache_set_property (GObject *gobject, |
97 | guint prop_id, |
98 | const GValue *value, |
99 | GParamSpec *pspec) |
100 | { |
101 | GtkAtSpiCache *self = GTK_AT_SPI_CACHE (ptr: gobject); |
102 | |
103 | switch (prop_id) |
104 | { |
105 | case PROP_CACHE_PATH: |
106 | g_free (mem: self->cache_path); |
107 | self->cache_path = g_value_dup_string (value); |
108 | break; |
109 | |
110 | case PROP_CONNECTION: |
111 | g_clear_object (&self->connection); |
112 | self->connection = g_value_dup_object (value); |
113 | break; |
114 | |
115 | default: |
116 | G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); |
117 | } |
118 | } |
119 | |
120 | static void |
121 | collect_object (GtkAtSpiCache *self, |
122 | GVariantBuilder *builder, |
123 | GtkAtSpiContext *context) |
124 | { |
125 | g_variant_builder_add (builder, format_string: "@(so)" , gtk_at_spi_context_to_ref (self: context)); |
126 | |
127 | GtkAtSpiRoot *root = gtk_at_spi_context_get_root (self: context); |
128 | g_variant_builder_add (builder, format_string: "@(so)" , gtk_at_spi_root_to_ref (self: root)); |
129 | |
130 | g_variant_builder_add (builder, format_string: "@(so)" , gtk_at_spi_context_get_parent_ref (self: context)); |
131 | |
132 | g_variant_builder_add (builder, format_string: "i" , gtk_at_spi_context_get_index_in_parent (self: context)); |
133 | g_variant_builder_add (builder, format_string: "i" , gtk_at_spi_context_get_child_count (self: context)); |
134 | |
135 | g_variant_builder_add (builder, format_string: "@as" , gtk_at_spi_context_get_interfaces (self: context)); |
136 | |
137 | char *name = gtk_at_context_get_name (self: GTK_AT_CONTEXT (ptr: context)); |
138 | g_variant_builder_add (builder, format_string: "s" , name ? name : "" ); |
139 | g_free (mem: name); |
140 | |
141 | guint atspi_role = gtk_atspi_role_for_context (context: GTK_AT_CONTEXT (ptr: context)); |
142 | g_variant_builder_add (builder, format_string: "u" , atspi_role); |
143 | |
144 | char *description = gtk_at_context_get_description (self: GTK_AT_CONTEXT (ptr: context)); |
145 | g_variant_builder_add (builder, format_string: "s" , description ? description : "" ); |
146 | g_free (mem: description); |
147 | |
148 | g_variant_builder_add (builder, format_string: "@au" , gtk_at_spi_context_get_states (self: context)); |
149 | } |
150 | |
151 | static void |
152 | collect_root (GtkAtSpiCache *self, |
153 | GVariantBuilder *builder) |
154 | { |
155 | g_variant_builder_add (builder, format_string: "@(so)" , gtk_at_spi_root_to_ref (self: self->root)); |
156 | g_variant_builder_add (builder, format_string: "@(so)" , gtk_at_spi_root_to_ref (self: self->root)); |
157 | |
158 | g_variant_builder_add (builder, format_string: "@(so)" , gtk_at_spi_null_ref ()); |
159 | |
160 | g_variant_builder_add (builder, format_string: "i" , -1); |
161 | g_variant_builder_add (builder, format_string: "i" , 0); |
162 | |
163 | GVariantBuilder interfaces = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("as" )); |
164 | |
165 | g_variant_builder_add (builder: &interfaces, format_string: "s" , atspi_accessible_interface.name); |
166 | g_variant_builder_add (builder: &interfaces, format_string: "s" , atspi_application_interface.name); |
167 | g_variant_builder_add (builder, format_string: "@as" , g_variant_builder_end (builder: &interfaces)); |
168 | |
169 | g_variant_builder_add (builder, format_string: "s" , g_get_prgname () ? g_get_prgname () : "Unnamed" ); |
170 | |
171 | g_variant_builder_add (builder, format_string: "u" , ATSPI_ROLE_APPLICATION); |
172 | |
173 | g_variant_builder_add (builder, format_string: "s" , g_get_application_name () ? g_get_application_name () : "No description" ); |
174 | |
175 | GVariantBuilder states = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("au" )); |
176 | g_variant_builder_add (builder: &states, format_string: "u" , 0); |
177 | g_variant_builder_add (builder: &states, format_string: "u" , 0); |
178 | g_variant_builder_add (builder, format_string: "@au" , g_variant_builder_end (builder: &states)); |
179 | } |
180 | |
181 | static void |
182 | collect_cached_objects (GtkAtSpiCache *self, |
183 | GVariantBuilder *builder) |
184 | { |
185 | GHashTable *collection = g_hash_table_new (NULL, NULL); |
186 | GHashTableIter iter; |
187 | gpointer key_p, value_p; |
188 | |
189 | /* Serializing the contexts might re-enter, and end up modifying the hash |
190 | * table, so we take a snapshot here and return the items we have at the |
191 | * moment of the GetItems() call |
192 | */ |
193 | g_hash_table_iter_init (iter: &iter, hash_table: self->contexts_by_path); |
194 | while (g_hash_table_iter_next (iter: &iter, key: &key_p, value: &value_p)) |
195 | g_hash_table_add (hash_table: collection, key: value_p); |
196 | |
197 | g_variant_builder_open (builder, G_VARIANT_TYPE ("(" ITEM_SIGNATURE ")" )); |
198 | collect_root (self, builder); |
199 | g_variant_builder_close (builder); |
200 | |
201 | g_hash_table_iter_init (iter: &iter, hash_table: collection); |
202 | while (g_hash_table_iter_next (iter: &iter, key: &key_p, value: &value_p)) |
203 | { |
204 | g_variant_builder_open (builder, G_VARIANT_TYPE ("(" ITEM_SIGNATURE ")" )); |
205 | |
206 | GtkAtSpiContext *context = value_p; |
207 | |
208 | collect_object (self, builder, context); |
209 | |
210 | g_variant_builder_close (builder); |
211 | } |
212 | |
213 | g_hash_table_unref (hash_table: collection); |
214 | } |
215 | |
216 | static void |
217 | emit_add_accessible (GtkAtSpiCache *self, |
218 | GtkAtSpiContext *context) |
219 | { |
220 | GtkATContext *at_context = GTK_AT_CONTEXT (ptr: context); |
221 | |
222 | /* If the context is hidden, we don't need to update the cache */ |
223 | if (gtk_at_context_has_accessible_state (self: at_context, state: GTK_ACCESSIBLE_STATE_HIDDEN)) |
224 | { |
225 | GtkAccessibleValue *is_hidden = |
226 | gtk_at_context_get_accessible_state (self: at_context, state: GTK_ACCESSIBLE_STATE_HIDDEN); |
227 | |
228 | if (gtk_boolean_accessible_value_get (value: is_hidden)) |
229 | return; |
230 | } |
231 | |
232 | GVariantBuilder builder = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("(" ITEM_SIGNATURE ")" )); |
233 | |
234 | collect_object (self, builder: &builder, context); |
235 | |
236 | g_dbus_connection_emit_signal (connection: self->connection, |
237 | NULL, |
238 | object_path: self->cache_path, |
239 | interface_name: "org.a11y.atspi.Cache" , |
240 | signal_name: "AddAccessible" , |
241 | parameters: g_variant_new (format_string: "(@(" ITEM_SIGNATURE "))" , |
242 | g_variant_builder_end (builder: &builder)), |
243 | NULL); |
244 | } |
245 | |
246 | static void |
247 | emit_remove_accessible (GtkAtSpiCache *self, |
248 | GtkAtSpiContext *context) |
249 | { |
250 | GtkATContext *at_context = GTK_AT_CONTEXT (ptr: context); |
251 | |
252 | /* If the context is hidden, we don't need to update the cache */ |
253 | if (gtk_at_context_has_accessible_state (self: at_context, state: GTK_ACCESSIBLE_STATE_HIDDEN)) |
254 | { |
255 | GtkAccessibleValue *is_hidden = |
256 | gtk_at_context_get_accessible_state (self: at_context, state: GTK_ACCESSIBLE_STATE_HIDDEN); |
257 | |
258 | if (gtk_boolean_accessible_value_get (value: is_hidden)) |
259 | return; |
260 | } |
261 | |
262 | GVariant *ref = gtk_at_spi_context_to_ref (self: context); |
263 | |
264 | g_dbus_connection_emit_signal (connection: self->connection, |
265 | NULL, |
266 | object_path: self->cache_path, |
267 | interface_name: "org.a11y.atspi.Cache" , |
268 | signal_name: "RemoveAccessible" , |
269 | parameters: g_variant_new (format_string: "(@(so))" , ref), |
270 | NULL); |
271 | } |
272 | |
273 | static void |
274 | handle_cache_method (GDBusConnection *connection, |
275 | const gchar *sender, |
276 | const gchar *object_path, |
277 | const gchar *interface_name, |
278 | const gchar *method_name, |
279 | GVariant *parameters, |
280 | GDBusMethodInvocation *invocation, |
281 | gpointer user_data) |
282 | { |
283 | GtkAtSpiCache *self = user_data; |
284 | |
285 | GTK_NOTE (A11Y, |
286 | g_message ("[Cache] Method '%s' on interface '%s' for object '%s' from '%s'\n" , |
287 | method_name, interface_name, object_path, sender)); |
288 | |
289 | |
290 | if (g_strcmp0 (str1: method_name, str2: "GetItems" ) == 0) |
291 | { |
292 | GVariantBuilder builder = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("(" GET_ITEMS_SIGNATURE ")" )); |
293 | GVariant *items; |
294 | |
295 | /* Prevent the emission os signals while collecting accessible |
296 | * objects as the result of walking the accessible tree |
297 | */ |
298 | self->in_get_items = TRUE; |
299 | |
300 | g_variant_builder_open (builder: &builder, G_VARIANT_TYPE (GET_ITEMS_SIGNATURE)); |
301 | collect_cached_objects (self, builder: &builder); |
302 | g_variant_builder_close (builder: &builder); |
303 | items = g_variant_builder_end (builder: &builder); |
304 | |
305 | self->in_get_items = FALSE; |
306 | |
307 | GTK_NOTE (A11Y, |
308 | g_message ("Returning %lu items\n" , g_variant_n_children (items))); |
309 | |
310 | g_dbus_method_invocation_return_value (invocation, parameters: items); |
311 | } |
312 | } |
313 | |
314 | static GVariant * |
315 | handle_cache_get_property (GDBusConnection *connection, |
316 | const gchar *sender, |
317 | const gchar *object_path, |
318 | const gchar *interface_name, |
319 | const gchar *property_name, |
320 | GError **error, |
321 | gpointer user_data) |
322 | { |
323 | GVariant *res = NULL; |
324 | |
325 | g_set_error (err: error, G_IO_ERROR, code: G_IO_ERROR_NOT_SUPPORTED, |
326 | format: "Unknown property '%s'" , property_name); |
327 | |
328 | return res; |
329 | } |
330 | |
331 | |
332 | static const GDBusInterfaceVTable cache_vtable = { |
333 | handle_cache_method, |
334 | handle_cache_get_property, |
335 | NULL, |
336 | }; |
337 | |
338 | static void |
339 | gtk_at_spi_cache_constructed (GObject *gobject) |
340 | { |
341 | GtkAtSpiCache *self = GTK_AT_SPI_CACHE (ptr: gobject); |
342 | |
343 | g_assert (self->connection); |
344 | g_assert (self->cache_path); |
345 | |
346 | g_dbus_connection_register_object (self->connection, |
347 | self->cache_path, |
348 | (GDBusInterfaceInfo *) &atspi_cache_interface, |
349 | &cache_vtable, |
350 | self, |
351 | NULL, |
352 | NULL); |
353 | |
354 | GTK_NOTE (A11Y, g_message ("Cache registered at %s" , self->cache_path)); |
355 | |
356 | G_OBJECT_CLASS (gtk_at_spi_cache_parent_class)->constructed (gobject); |
357 | } |
358 | |
359 | static void |
360 | gtk_at_spi_cache_class_init (GtkAtSpiCacheClass *klass) |
361 | { |
362 | GObjectClass *gobject_class = G_OBJECT_CLASS (klass); |
363 | |
364 | gobject_class->constructed = gtk_at_spi_cache_constructed; |
365 | gobject_class->set_property = gtk_at_spi_cache_set_property; |
366 | gobject_class->finalize = gtk_at_spi_cache_finalize; |
367 | |
368 | obj_props[PROP_CACHE_PATH] = |
369 | g_param_spec_string (name: "cache-path" , NULL, NULL, |
370 | NULL, |
371 | flags: G_PARAM_WRITABLE | |
372 | G_PARAM_CONSTRUCT_ONLY | |
373 | G_PARAM_STATIC_STRINGS); |
374 | |
375 | obj_props[PROP_CONNECTION] = |
376 | g_param_spec_object (name: "connection" , NULL, NULL, |
377 | G_TYPE_DBUS_CONNECTION, |
378 | flags: G_PARAM_WRITABLE | |
379 | G_PARAM_CONSTRUCT_ONLY | |
380 | G_PARAM_STATIC_STRINGS); |
381 | |
382 | g_object_class_install_properties (oclass: gobject_class, n_pspecs: N_PROPS, pspecs: obj_props); |
383 | } |
384 | |
385 | static void |
386 | gtk_at_spi_cache_init (GtkAtSpiCache *self) |
387 | { |
388 | self->contexts_by_path = g_hash_table_new_full (hash_func: g_str_hash, key_equal_func: g_str_equal, |
389 | key_destroy_func: g_free, |
390 | NULL); |
391 | self->contexts_to_path = g_hash_table_new (NULL, NULL); |
392 | } |
393 | |
394 | GtkAtSpiCache * |
395 | gtk_at_spi_cache_new (GDBusConnection *connection, |
396 | const char *cache_path, |
397 | GtkAtSpiRoot *root) |
398 | { |
399 | GtkAtSpiCache *cache; |
400 | |
401 | g_return_val_if_fail (G_IS_DBUS_CONNECTION (connection), NULL); |
402 | g_return_val_if_fail (cache_path != NULL, NULL); |
403 | |
404 | cache = g_object_new (GTK_TYPE_AT_SPI_CACHE, |
405 | first_property_name: "connection" , connection, |
406 | "cache-path" , cache_path, |
407 | NULL); |
408 | cache->root = root; |
409 | |
410 | return cache; |
411 | } |
412 | |
413 | void |
414 | gtk_at_spi_cache_add_context (GtkAtSpiCache *self, |
415 | GtkAtSpiContext *context) |
416 | { |
417 | g_return_if_fail (GTK_IS_AT_SPI_CACHE (self)); |
418 | g_return_if_fail (GTK_IS_AT_SPI_CONTEXT (context)); |
419 | |
420 | const char *path = gtk_at_spi_context_get_context_path (self: context); |
421 | if (path == NULL) |
422 | return; |
423 | |
424 | if (g_hash_table_contains (hash_table: self->contexts_by_path, key: path)) |
425 | return; |
426 | |
427 | char *path_key = g_strdup (str: path); |
428 | g_hash_table_insert (hash_table: self->contexts_by_path, key: path_key, value: context); |
429 | g_hash_table_insert (hash_table: self->contexts_to_path, key: context, value: path_key); |
430 | |
431 | GTK_NOTE (A11Y, g_message ("Adding context '%s' to cache" , path_key)); |
432 | |
433 | /* GetItems is safe from re-entrancy, but we still don't want to |
434 | * emit an unnecessary signal while we're collecting ATContexts |
435 | */ |
436 | if (!self->in_get_items) |
437 | emit_add_accessible (self, context); |
438 | } |
439 | |
440 | void |
441 | gtk_at_spi_cache_remove_context (GtkAtSpiCache *self, |
442 | GtkAtSpiContext *context) |
443 | { |
444 | g_return_if_fail (GTK_IS_AT_SPI_CACHE (self)); |
445 | g_return_if_fail (GTK_IS_AT_SPI_CONTEXT (context)); |
446 | |
447 | const char *path = gtk_at_spi_context_get_context_path (self: context); |
448 | if (!g_hash_table_contains (hash_table: self->contexts_by_path, key: path)) |
449 | return; |
450 | |
451 | emit_remove_accessible (self, context); |
452 | |
453 | /* The order is important: the value in contexts_by_path is the |
454 | * key in contexts_to_path |
455 | */ |
456 | g_hash_table_remove (hash_table: self->contexts_to_path, key: context); |
457 | g_hash_table_remove (hash_table: self->contexts_by_path, key: path); |
458 | |
459 | GTK_NOTE (A11Y, g_message ("Removing context '%s' from cache" , path)); |
460 | } |
461 | |