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
51struct _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
70enum
71{
72 PROP_CACHE_PATH = 1,
73 PROP_CONNECTION,
74
75 N_PROPS
76};
77
78static GParamSpec *obj_props[N_PROPS];
79
80G_DEFINE_TYPE (GtkAtSpiCache, gtk_at_spi_cache, G_TYPE_OBJECT)
81
82static void
83gtk_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
95static void
96gtk_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
120static void
121collect_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
151static void
152collect_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
181static void
182collect_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
216static void
217emit_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
246static void
247emit_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
273static void
274handle_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
314static GVariant *
315handle_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
332static const GDBusInterfaceVTable cache_vtable = {
333 handle_cache_method,
334 handle_cache_get_property,
335 NULL,
336};
337
338static void
339gtk_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
359static void
360gtk_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
385static void
386gtk_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
394GtkAtSpiCache *
395gtk_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
413void
414gtk_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
440void
441gtk_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

source code of gtk/gtk/a11y/gtkatspicache.c