463 lines
14 KiB
C
463 lines
14 KiB
C
|
/* gtkatspicache.c: AT-SPI object cache
|
||
|
*
|
||
|
* Copyright 2020 holder
|
||
|
*
|
||
|
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||
|
*
|
||
|
* This library is free software; you can redistribute it and/or
|
||
|
* modify it under the terms of the GNU Lesser General Public
|
||
|
* License as published by the Free Software Foundation; either
|
||
|
* version 2.1 of the License, or (at your option) any later version.
|
||
|
*
|
||
|
* This library is distributed in the hope that it will be useful,
|
||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||
|
* Lesser General Public License for more details.
|
||
|
*
|
||
|
* You should have received a copy of the GNU Lesser General Public
|
||
|
* License along with this library; if not, see <http://www.gnu.org/licenses/>.
|
||
|
*/
|
||
|
|
||
|
#include "config.h"
|
||
|
|
||
|
#include "gtkatspicacheprivate.h"
|
||
|
|
||
|
#include "gtkatspicontextprivate.h"
|
||
|
#include "gtkatspirootprivate.h"
|
||
|
#include "gtkatspiutilsprivate.h"
|
||
|
#include "gtkdebug.h"
|
||
|
#include "gtkprivate.h"
|
||
|
|
||
|
#include "a11y/atspi/atspi-accessible.h"
|
||
|
#include "a11y/atspi/atspi-application.h"
|
||
|
#include "a11y/atspi/atspi-cache.h"
|
||
|
|
||
|
/* Cached item:
|
||
|
*
|
||
|
* (so): object ref
|
||
|
* (so): application ref
|
||
|
* (so): parent ref
|
||
|
* - parent.role == application ? desktop ref : null ref
|
||
|
* i: index in parent, or -1 for transient widgets/menu items
|
||
|
* i: child count, or -1 for defunct/menus
|
||
|
* as: interfaces
|
||
|
* s: name
|
||
|
* u: role
|
||
|
* s: description
|
||
|
* au: state set
|
||
|
*/
|
||
|
#define ITEM_SIGNATURE "(so)(so)(so)iiassusau"
|
||
|
#define GET_ITEMS_SIGNATURE "a(" ITEM_SIGNATURE ")"
|
||
|
|
||
|
struct _GtkAtSpiCache
|
||
|
{
|
||
|
GObject parent_instance;
|
||
|
|
||
|
char *cache_path;
|
||
|
GDBusConnection *connection;
|
||
|
|
||
|
/* HashTable<str, GtkAtSpiContext> */
|
||
|
GHashTable *contexts_by_path;
|
||
|
|
||
|
/* HashTable<GtkAtSpiContext, str> */
|
||
|
GHashTable *contexts_to_path;
|
||
|
|
||
|
/* Re-entrancy guard */
|
||
|
gboolean in_get_items;
|
||
|
|
||
|
GtkAtSpiRoot *root;
|
||
|
};
|
||
|
|
||
|
enum
|
||
|
{
|
||
|
PROP_CACHE_PATH = 1,
|
||
|
PROP_CONNECTION,
|
||
|
|
||
|
N_PROPS
|
||
|
};
|
||
|
|
||
|
static GParamSpec *obj_props[N_PROPS];
|
||
|
|
||
|
G_DEFINE_TYPE (GtkAtSpiCache, gtk_at_spi_cache, G_TYPE_OBJECT)
|
||
|
|
||
|
static void
|
||
|
gtk_at_spi_cache_finalize (GObject *gobject)
|
||
|
{
|
||
|
GtkAtSpiCache *self = GTK_AT_SPI_CACHE (gobject);
|
||
|
|
||
|
g_clear_pointer (&self->contexts_to_path, g_hash_table_unref);
|
||
|
g_clear_pointer (&self->contexts_by_path, g_hash_table_unref);
|
||
|
g_clear_object (&self->connection);
|
||
|
g_free (self->cache_path);
|
||
|
|
||
|
G_OBJECT_CLASS (gtk_at_spi_cache_parent_class)->finalize (gobject);
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
gtk_at_spi_cache_set_property (GObject *gobject,
|
||
|
guint prop_id,
|
||
|
const GValue *value,
|
||
|
GParamSpec *pspec)
|
||
|
{
|
||
|
GtkAtSpiCache *self = GTK_AT_SPI_CACHE (gobject);
|
||
|
|
||
|
switch (prop_id)
|
||
|
{
|
||
|
case PROP_CACHE_PATH:
|
||
|
g_free (self->cache_path);
|
||
|
self->cache_path = g_value_dup_string (value);
|
||
|
break;
|
||
|
|
||
|
case PROP_CONNECTION:
|
||
|
g_clear_object (&self->connection);
|
||
|
self->connection = g_value_dup_object (value);
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
collect_object (GtkAtSpiCache *self,
|
||
|
GVariantBuilder *builder,
|
||
|
GtkAtSpiContext *context)
|
||
|
{
|
||
|
g_variant_builder_add (builder, "@(so)", gtk_at_spi_context_to_ref (context));
|
||
|
|
||
|
GtkAtSpiRoot *root = gtk_at_spi_context_get_root (context);
|
||
|
g_variant_builder_add (builder, "@(so)", gtk_at_spi_root_to_ref (root));
|
||
|
|
||
|
g_variant_builder_add (builder, "@(so)", gtk_at_spi_context_get_parent_ref (context));
|
||
|
|
||
|
g_variant_builder_add (builder, "i", gtk_at_spi_context_get_index_in_parent (context));
|
||
|
g_variant_builder_add (builder, "i", gtk_at_spi_context_get_child_count (context));
|
||
|
|
||
|
g_variant_builder_add (builder, "@as", gtk_at_spi_context_get_interfaces (context));
|
||
|
|
||
|
char *name = gtk_at_context_get_name (GTK_AT_CONTEXT (context));
|
||
|
g_variant_builder_add (builder, "s", name ? name : "");
|
||
|
g_free (name);
|
||
|
|
||
|
guint atspi_role = gtk_atspi_role_for_context (GTK_AT_CONTEXT (context));
|
||
|
g_variant_builder_add (builder, "u", atspi_role);
|
||
|
|
||
|
char *description = gtk_at_context_get_description (GTK_AT_CONTEXT (context));
|
||
|
g_variant_builder_add (builder, "s", description ? description : "");
|
||
|
g_free (description);
|
||
|
|
||
|
g_variant_builder_add (builder, "@au", gtk_at_spi_context_get_states (context));
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
collect_root (GtkAtSpiCache *self,
|
||
|
GVariantBuilder *builder)
|
||
|
{
|
||
|
g_variant_builder_add (builder, "@(so)", gtk_at_spi_root_to_ref (self->root));
|
||
|
g_variant_builder_add (builder, "@(so)", gtk_at_spi_root_to_ref (self->root));
|
||
|
|
||
|
g_variant_builder_add (builder, "@(so)", gtk_at_spi_null_ref ());
|
||
|
|
||
|
g_variant_builder_add (builder, "i", -1);
|
||
|
g_variant_builder_add (builder, "i", 0);
|
||
|
|
||
|
GVariantBuilder interfaces = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("as"));
|
||
|
|
||
|
g_variant_builder_add (&interfaces, "s", atspi_accessible_interface.name);
|
||
|
g_variant_builder_add (&interfaces, "s", atspi_application_interface.name);
|
||
|
g_variant_builder_add (builder, "@as", g_variant_builder_end (&interfaces));
|
||
|
|
||
|
g_variant_builder_add (builder, "s", g_get_prgname () ? g_get_prgname () : "Unnamed");
|
||
|
|
||
|
g_variant_builder_add (builder, "u", ATSPI_ROLE_APPLICATION);
|
||
|
|
||
|
g_variant_builder_add (builder, "s", g_get_application_name () ? g_get_application_name () : "No description");
|
||
|
|
||
|
GVariantBuilder states = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("au"));
|
||
|
g_variant_builder_add (&states, "u", 0);
|
||
|
g_variant_builder_add (&states, "u", 0);
|
||
|
g_variant_builder_add (builder, "@au", g_variant_builder_end (&states));
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
collect_cached_objects (GtkAtSpiCache *self,
|
||
|
GVariantBuilder *builder)
|
||
|
{
|
||
|
GHashTable *collection = g_hash_table_new (NULL, NULL);
|
||
|
GHashTableIter iter;
|
||
|
gpointer key_p, value_p;
|
||
|
|
||
|
/* Serializing the contexts might re-enter, and end up modifying the hash
|
||
|
* table, so we take a snapshot here and return the items we have at the
|
||
|
* moment of the GetItems() call
|
||
|
*/
|
||
|
g_hash_table_iter_init (&iter, self->contexts_by_path);
|
||
|
while (g_hash_table_iter_next (&iter, &key_p, &value_p))
|
||
|
g_hash_table_add (collection, value_p);
|
||
|
|
||
|
g_variant_builder_open (builder, G_VARIANT_TYPE ("(" ITEM_SIGNATURE ")"));
|
||
|
collect_root (self, builder);
|
||
|
g_variant_builder_close (builder);
|
||
|
|
||
|
g_hash_table_iter_init (&iter, collection);
|
||
|
while (g_hash_table_iter_next (&iter, &key_p, &value_p))
|
||
|
{
|
||
|
g_variant_builder_open (builder, G_VARIANT_TYPE ("(" ITEM_SIGNATURE ")"));
|
||
|
|
||
|
GtkAtSpiContext *context = value_p;
|
||
|
|
||
|
collect_object (self, builder, context);
|
||
|
|
||
|
g_variant_builder_close (builder);
|
||
|
}
|
||
|
|
||
|
g_hash_table_unref (collection);
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
emit_add_accessible (GtkAtSpiCache *self,
|
||
|
GtkAtSpiContext *context)
|
||
|
{
|
||
|
GtkATContext *at_context = GTK_AT_CONTEXT (context);
|
||
|
|
||
|
/* If the context is hidden, we don't need to update the cache */
|
||
|
if (gtk_at_context_has_accessible_state (at_context, GTK_ACCESSIBLE_STATE_HIDDEN))
|
||
|
{
|
||
|
GtkAccessibleValue *is_hidden =
|
||
|
gtk_at_context_get_accessible_state (at_context, GTK_ACCESSIBLE_STATE_HIDDEN);
|
||
|
|
||
|
if (gtk_boolean_accessible_value_get (is_hidden))
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
GVariantBuilder builder = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("(" ITEM_SIGNATURE ")"));
|
||
|
|
||
|
collect_object (self, &builder, context);
|
||
|
|
||
|
g_dbus_connection_emit_signal (self->connection,
|
||
|
NULL,
|
||
|
self->cache_path,
|
||
|
"org.a11y.atspi.Cache",
|
||
|
"AddAccessible",
|
||
|
g_variant_new ("(@(" ITEM_SIGNATURE "))",
|
||
|
g_variant_builder_end (&builder)),
|
||
|
NULL);
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
emit_remove_accessible (GtkAtSpiCache *self,
|
||
|
GtkAtSpiContext *context)
|
||
|
{
|
||
|
GtkATContext *at_context = GTK_AT_CONTEXT (context);
|
||
|
|
||
|
/* If the context is hidden, we don't need to update the cache */
|
||
|
if (gtk_at_context_has_accessible_state (at_context, GTK_ACCESSIBLE_STATE_HIDDEN))
|
||
|
{
|
||
|
GtkAccessibleValue *is_hidden =
|
||
|
gtk_at_context_get_accessible_state (at_context, GTK_ACCESSIBLE_STATE_HIDDEN);
|
||
|
|
||
|
if (gtk_boolean_accessible_value_get (is_hidden))
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
GVariant *ref = gtk_at_spi_context_to_ref (context);
|
||
|
|
||
|
g_dbus_connection_emit_signal (self->connection,
|
||
|
NULL,
|
||
|
self->cache_path,
|
||
|
"org.a11y.atspi.Cache",
|
||
|
"RemoveAccessible",
|
||
|
g_variant_new ("(@(so))", ref),
|
||
|
NULL);
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
handle_cache_method (GDBusConnection *connection,
|
||
|
const gchar *sender,
|
||
|
const gchar *object_path,
|
||
|
const gchar *interface_name,
|
||
|
const gchar *method_name,
|
||
|
GVariant *parameters,
|
||
|
GDBusMethodInvocation *invocation,
|
||
|
gpointer user_data)
|
||
|
{
|
||
|
GtkAtSpiCache *self = user_data;
|
||
|
|
||
|
GTK_DEBUG (A11Y, "[Cache] Method '%s' on interface '%s' for object '%s' from '%s'",
|
||
|
method_name, interface_name, object_path, sender);
|
||
|
|
||
|
|
||
|
if (g_strcmp0 (method_name, "GetItems") == 0)
|
||
|
{
|
||
|
GVariantBuilder builder = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("(" GET_ITEMS_SIGNATURE ")"));
|
||
|
GVariant *items;
|
||
|
|
||
|
/* Prevent the emission os signals while collecting accessible
|
||
|
* objects as the result of walking the accessible tree
|
||
|
*/
|
||
|
self->in_get_items = TRUE;
|
||
|
|
||
|
g_variant_builder_open (&builder, G_VARIANT_TYPE (GET_ITEMS_SIGNATURE));
|
||
|
collect_cached_objects (self, &builder);
|
||
|
g_variant_builder_close (&builder);
|
||
|
items = g_variant_builder_end (&builder);
|
||
|
|
||
|
self->in_get_items = FALSE;
|
||
|
|
||
|
GTK_DEBUG (A11Y, "Returning %lu items", g_variant_n_children (items));
|
||
|
|
||
|
g_dbus_method_invocation_return_value (invocation, items);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static GVariant *
|
||
|
handle_cache_get_property (GDBusConnection *connection,
|
||
|
const gchar *sender,
|
||
|
const gchar *object_path,
|
||
|
const gchar *interface_name,
|
||
|
const gchar *property_name,
|
||
|
GError **error,
|
||
|
gpointer user_data)
|
||
|
{
|
||
|
GVariant *res = NULL;
|
||
|
|
||
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
|
||
|
"Unknown property '%s'", property_name);
|
||
|
|
||
|
return res;
|
||
|
}
|
||
|
|
||
|
|
||
|
static const GDBusInterfaceVTable cache_vtable = {
|
||
|
handle_cache_method,
|
||
|
handle_cache_get_property,
|
||
|
NULL,
|
||
|
};
|
||
|
|
||
|
static void
|
||
|
gtk_at_spi_cache_constructed (GObject *gobject)
|
||
|
{
|
||
|
GtkAtSpiCache *self = GTK_AT_SPI_CACHE (gobject);
|
||
|
|
||
|
g_assert (self->connection);
|
||
|
g_assert (self->cache_path);
|
||
|
|
||
|
g_dbus_connection_register_object (self->connection,
|
||
|
self->cache_path,
|
||
|
(GDBusInterfaceInfo *) &atspi_cache_interface,
|
||
|
&cache_vtable,
|
||
|
self,
|
||
|
NULL,
|
||
|
NULL);
|
||
|
|
||
|
GTK_DEBUG (A11Y, "Cache registered at %s", self->cache_path);
|
||
|
|
||
|
G_OBJECT_CLASS (gtk_at_spi_cache_parent_class)->constructed (gobject);
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
gtk_at_spi_cache_class_init (GtkAtSpiCacheClass *klass)
|
||
|
{
|
||
|
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
|
||
|
|
||
|
gobject_class->constructed = gtk_at_spi_cache_constructed;
|
||
|
gobject_class->set_property = gtk_at_spi_cache_set_property;
|
||
|
gobject_class->finalize = gtk_at_spi_cache_finalize;
|
||
|
|
||
|
obj_props[PROP_CACHE_PATH] =
|
||
|
g_param_spec_string ("cache-path", NULL, NULL,
|
||
|
NULL,
|
||
|
G_PARAM_WRITABLE |
|
||
|
G_PARAM_CONSTRUCT_ONLY |
|
||
|
G_PARAM_STATIC_STRINGS);
|
||
|
|
||
|
obj_props[PROP_CONNECTION] =
|
||
|
g_param_spec_object ("connection", NULL, NULL,
|
||
|
G_TYPE_DBUS_CONNECTION,
|
||
|
G_PARAM_WRITABLE |
|
||
|
G_PARAM_CONSTRUCT_ONLY |
|
||
|
G_PARAM_STATIC_STRINGS);
|
||
|
|
||
|
g_object_class_install_properties (gobject_class, N_PROPS, obj_props);
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
gtk_at_spi_cache_init (GtkAtSpiCache *self)
|
||
|
{
|
||
|
self->contexts_by_path = g_hash_table_new_full (g_str_hash, g_str_equal,
|
||
|
g_free,
|
||
|
NULL);
|
||
|
self->contexts_to_path = g_hash_table_new (NULL, NULL);
|
||
|
}
|
||
|
|
||
|
GtkAtSpiCache *
|
||
|
gtk_at_spi_cache_new (GDBusConnection *connection,
|
||
|
const char *cache_path,
|
||
|
GtkAtSpiRoot *root)
|
||
|
{
|
||
|
GtkAtSpiCache *cache;
|
||
|
|
||
|
g_return_val_if_fail (G_IS_DBUS_CONNECTION (connection), NULL);
|
||
|
g_return_val_if_fail (cache_path != NULL, NULL);
|
||
|
|
||
|
cache = g_object_new (GTK_TYPE_AT_SPI_CACHE,
|
||
|
"connection", connection,
|
||
|
"cache-path", cache_path,
|
||
|
NULL);
|
||
|
cache->root = root;
|
||
|
|
||
|
return cache;
|
||
|
}
|
||
|
|
||
|
void
|
||
|
gtk_at_spi_cache_add_context (GtkAtSpiCache *self,
|
||
|
GtkAtSpiContext *context)
|
||
|
{
|
||
|
g_return_if_fail (GTK_IS_AT_SPI_CACHE (self));
|
||
|
g_return_if_fail (GTK_IS_AT_SPI_CONTEXT (context));
|
||
|
|
||
|
const char *path = gtk_at_spi_context_get_context_path (context);
|
||
|
if (path == NULL)
|
||
|
return;
|
||
|
|
||
|
if (g_hash_table_contains (self->contexts_by_path, path))
|
||
|
return;
|
||
|
|
||
|
char *path_key = g_strdup (path);
|
||
|
g_hash_table_insert (self->contexts_by_path, path_key, context);
|
||
|
g_hash_table_insert (self->contexts_to_path, context, path_key);
|
||
|
|
||
|
GTK_DEBUG (A11Y, "Adding context '%s' to cache", path_key);
|
||
|
|
||
|
/* GetItems is safe from re-entrancy, but we still don't want to
|
||
|
* emit an unnecessary signal while we're collecting ATContexts
|
||
|
*/
|
||
|
if (!self->in_get_items)
|
||
|
emit_add_accessible (self, context);
|
||
|
}
|
||
|
|
||
|
void
|
||
|
gtk_at_spi_cache_remove_context (GtkAtSpiCache *self,
|
||
|
GtkAtSpiContext *context)
|
||
|
{
|
||
|
g_return_if_fail (GTK_IS_AT_SPI_CACHE (self));
|
||
|
g_return_if_fail (GTK_IS_AT_SPI_CONTEXT (context));
|
||
|
|
||
|
const char *path = gtk_at_spi_context_get_context_path (context);
|
||
|
if (path == NULL)
|
||
|
return;
|
||
|
|
||
|
if (!g_hash_table_contains (self->contexts_by_path, path))
|
||
|
return;
|
||
|
|
||
|
emit_remove_accessible (self, context);
|
||
|
|
||
|
/* The order is important: the value in contexts_by_path is the
|
||
|
* key in contexts_to_path
|
||
|
*/
|
||
|
g_hash_table_remove (self->contexts_to_path, context);
|
||
|
g_hash_table_remove (self->contexts_by_path, path);
|
||
|
|
||
|
GTK_DEBUG (A11Y, "Removing context '%s' from cache", path);
|
||
|
}
|