/* gtkatcontext.c: Assistive technology context * * Copyright 2020 GNOME Foundation * * 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 . */ /** * SECTION:gtkatcontext * @Title: GtkATContext * @Short_description: An object communicating to Assistive Technologies * * GtkATContext is an abstract class provided by GTK to communicate to * platform-specific assistive technologies API. * * Each platform supported by GTK implements a #GtkATContext subclass, and * is responsible for updating the accessible state in response to state * changes in #GtkAccessible. */ #include "config.h" #include "gtkatcontextprivate.h" #include "gtkaccessiblevalueprivate.h" #include "gtkdebug.h" #include "gtktestatcontextprivate.h" #include "gtktypebuiltins.h" #if defined(GDK_WINDOWING_X11) || defined(GDK_WINDOWING_WAYLAND) #include "a11y/gtkatspicontextprivate.h" #endif G_DEFINE_ABSTRACT_TYPE (GtkATContext, gtk_at_context, G_TYPE_OBJECT) enum { PROP_ACCESSIBLE_ROLE = 1, PROP_ACCESSIBLE, PROP_DISPLAY, N_PROPS }; enum { STATE_CHANGE, LAST_SIGNAL }; static GParamSpec *obj_props[N_PROPS]; static guint obj_signals[LAST_SIGNAL]; static void gtk_at_context_finalize (GObject *gobject) { GtkATContext *self = GTK_AT_CONTEXT (gobject); gtk_accessible_attribute_set_unref (self->properties); gtk_accessible_attribute_set_unref (self->relations); gtk_accessible_attribute_set_unref (self->states); G_OBJECT_CLASS (gtk_at_context_parent_class)->finalize (gobject); } static void gtk_at_context_set_property (GObject *gobject, guint prop_id, const GValue *value, GParamSpec *pspec) { GtkATContext *self = GTK_AT_CONTEXT (gobject); switch (prop_id) { case PROP_ACCESSIBLE_ROLE: self->accessible_role = g_value_get_enum (value); break; case PROP_ACCESSIBLE: self->accessible = g_value_get_object (value); break; case PROP_DISPLAY: self->display = g_value_get_object (value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); } } static void gtk_at_context_get_property (GObject *gobject, guint prop_id, GValue *value, GParamSpec *pspec) { GtkATContext *self = GTK_AT_CONTEXT (gobject); switch (prop_id) { case PROP_ACCESSIBLE_ROLE: g_value_set_enum (value, self->accessible_role); break; case PROP_ACCESSIBLE: g_value_set_object (value, self->accessible); break; case PROP_DISPLAY: g_value_set_object (value, self->display); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); } } static void gtk_at_context_real_state_change (GtkATContext *self, GtkAccessibleStateChange changed_states, GtkAccessiblePropertyChange changed_properties, GtkAccessibleRelationChange changed_relations, GtkAccessibleAttributeSet *states, GtkAccessibleAttributeSet *properties, GtkAccessibleAttributeSet *relations) { } static void gtk_at_context_class_init (GtkATContextClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS (klass); gobject_class->set_property = gtk_at_context_set_property; gobject_class->get_property = gtk_at_context_get_property; gobject_class->finalize = gtk_at_context_finalize; klass->state_change = gtk_at_context_real_state_change; /** * GtkATContext:accessible-role: * * The accessible role used by the AT context. * * Depending on the given role, different states and properties can be * set or retrieved. */ obj_props[PROP_ACCESSIBLE_ROLE] = g_param_spec_enum ("accessible-role", "Accessible Role", "The accessible role of the AT context", GTK_TYPE_ACCESSIBLE_ROLE, GTK_ACCESSIBLE_ROLE_NONE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); /** * GtkATContext:accessible: * * The #GtkAccessible that created the #GtkATContext instance. */ obj_props[PROP_ACCESSIBLE] = g_param_spec_object ("accessible", "Accessible", "The accessible implementation", GTK_TYPE_ACCESSIBLE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); /** * GtkATContext:display: * * The #GdkDisplay for the #GtkATContext. */ obj_props[PROP_DISPLAY] = g_param_spec_object ("display", "Display", "The display connection", GDK_TYPE_DISPLAY, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); /** * GtkATContext::state-change: * @self: the #GtkATContext * * Emitted when the attributes of the accessible for the * #GtkATContext instance change. */ obj_signals[STATE_CHANGE] = g_signal_new ("state-change", G_TYPE_FROM_CLASS (gobject_class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); g_object_class_install_properties (gobject_class, N_PROPS, obj_props); } #define N_PROPERTIES (GTK_ACCESSIBLE_PROPERTY_VALUE_TEXT + 1) #define N_RELATIONS (GTK_ACCESSIBLE_RELATION_SET_SIZE + 1) #define N_STATES (GTK_ACCESSIBLE_STATE_SELECTED + 1) static const char *property_attrs[] = { [GTK_ACCESSIBLE_PROPERTY_AUTOCOMPLETE] = "autocomplete", [GTK_ACCESSIBLE_PROPERTY_DESCRIPTION] = "description", [GTK_ACCESSIBLE_PROPERTY_HAS_POPUP] = "haspopup", [GTK_ACCESSIBLE_PROPERTY_KEY_SHORTCUTS] = "keyshortcuts", [GTK_ACCESSIBLE_PROPERTY_LABEL] = "label", [GTK_ACCESSIBLE_PROPERTY_LEVEL] = "level", [GTK_ACCESSIBLE_PROPERTY_MODAL] = "modal", [GTK_ACCESSIBLE_PROPERTY_MULTI_LINE] = "multiline", [GTK_ACCESSIBLE_PROPERTY_MULTI_SELECTABLE] = "multiselectable", [GTK_ACCESSIBLE_PROPERTY_ORIENTATION] = "orientation", [GTK_ACCESSIBLE_PROPERTY_PLACEHOLDER] = "placeholder", [GTK_ACCESSIBLE_PROPERTY_READ_ONLY] = "readonly", [GTK_ACCESSIBLE_PROPERTY_REQUIRED] = "required", [GTK_ACCESSIBLE_PROPERTY_ROLE_DESCRIPTION] = "roledescription", [GTK_ACCESSIBLE_PROPERTY_SORT] = "sort", [GTK_ACCESSIBLE_PROPERTY_VALUE_MAX] = "valuemax", [GTK_ACCESSIBLE_PROPERTY_VALUE_MIN] = "valuemin", [GTK_ACCESSIBLE_PROPERTY_VALUE_NOW] = "valuenow", [GTK_ACCESSIBLE_PROPERTY_VALUE_TEXT] = "valuetext", }; /*< private > * gtk_accessible_property_get_attribute_name: * @property: a #GtkAccessibleProperty * * Retrieves the name of a #GtkAccessibleProperty. * * Returns: (transfer none): the name of the accessible property */ const char * gtk_accessible_property_get_attribute_name (GtkAccessibleProperty property) { g_return_val_if_fail (property >= GTK_ACCESSIBLE_PROPERTY_AUTOCOMPLETE && property <= GTK_ACCESSIBLE_PROPERTY_VALUE_TEXT, ""); return property_attrs[property]; } static const char *relation_attrs[] = { [GTK_ACCESSIBLE_RELATION_ACTIVE_DESCENDANT] = "activedescendant", [GTK_ACCESSIBLE_RELATION_COL_COUNT] = "colcount", [GTK_ACCESSIBLE_RELATION_COL_INDEX] = "colindex", [GTK_ACCESSIBLE_RELATION_COL_INDEX_TEXT] = "colindextext", [GTK_ACCESSIBLE_RELATION_COL_SPAN] = "colspan", [GTK_ACCESSIBLE_RELATION_CONTROLS] = "controls", [GTK_ACCESSIBLE_RELATION_DESCRIBED_BY] = "describedby", [GTK_ACCESSIBLE_RELATION_DETAILS] = "details", [GTK_ACCESSIBLE_RELATION_ERROR_MESSAGE] = "errormessage", [GTK_ACCESSIBLE_RELATION_FLOW_TO] = "flowto", [GTK_ACCESSIBLE_RELATION_LABELLED_BY] = "labelledby", [GTK_ACCESSIBLE_RELATION_OWNS] = "owns", [GTK_ACCESSIBLE_RELATION_POS_IN_SET] = "posinset", [GTK_ACCESSIBLE_RELATION_ROW_COUNT] = "rowcount", [GTK_ACCESSIBLE_RELATION_ROW_INDEX] = "rowindex", [GTK_ACCESSIBLE_RELATION_ROW_INDEX_TEXT] = "rowindextext", [GTK_ACCESSIBLE_RELATION_ROW_SPAN] = "rowspan", [GTK_ACCESSIBLE_RELATION_SET_SIZE] = "setsize", }; /*< private > * gtk_accessible_relation_get_attribute_name: * @relation: a #GtkAccessibleRelation * * Retrieves the name of a #GtkAccessibleRelation. * * Returns: (transfer none): the name of the accessible relation */ const char * gtk_accessible_relation_get_attribute_name (GtkAccessibleRelation relation) { g_return_val_if_fail (relation >= GTK_ACCESSIBLE_RELATION_ACTIVE_DESCENDANT && relation <= GTK_ACCESSIBLE_RELATION_SET_SIZE, ""); return relation_attrs[relation]; } static const char *state_attrs[] = { [GTK_ACCESSIBLE_STATE_BUSY] = "busy", [GTK_ACCESSIBLE_STATE_CHECKED] = "checked", [GTK_ACCESSIBLE_STATE_DISABLED] = "disabled", [GTK_ACCESSIBLE_STATE_EXPANDED] = "expanded", [GTK_ACCESSIBLE_STATE_HIDDEN] = "hidden", [GTK_ACCESSIBLE_STATE_INVALID] = "invalid", [GTK_ACCESSIBLE_STATE_PRESSED] = "pressed", [GTK_ACCESSIBLE_STATE_SELECTED] = "selected", }; /*< private > * gtk_accessible_state_get_attribute_name: * @state: a #GtkAccessibleState * * Retrieves the name of a #GtkAccessibleState. * * Returns: (transfer none): the name of the accessible state */ const char * gtk_accessible_state_get_attribute_name (GtkAccessibleState state) { g_return_val_if_fail (state >= GTK_ACCESSIBLE_STATE_BUSY && state <= GTK_ACCESSIBLE_STATE_SELECTED, ""); return state_attrs[state]; } static void gtk_at_context_init (GtkATContext *self) { self->accessible_role = GTK_ACCESSIBLE_ROLE_NONE; self->properties = gtk_accessible_attribute_set_new (N_PROPERTIES, property_attrs, (GtkAccessibleAttributeDefaultFunc) gtk_accessible_value_get_default_for_property); self->relations = gtk_accessible_attribute_set_new (N_RELATIONS, relation_attrs, (GtkAccessibleAttributeDefaultFunc) gtk_accessible_value_get_default_for_relation); self->states = gtk_accessible_attribute_set_new (N_STATES, state_attrs, (GtkAccessibleAttributeDefaultFunc) gtk_accessible_value_get_default_for_state); } /** * gtk_at_context_get_accessible: * @self: a #GtkATContext * * Retrieves the #GtkAccessible using this context. * * Returns: (transfer none): a #GtkAccessible */ GtkAccessible * gtk_at_context_get_accessible (GtkATContext *self) { g_return_val_if_fail (GTK_IS_AT_CONTEXT (self), NULL); return self->accessible; } /** * gtk_at_context_get_accessible_role: * @self: a #GtkATContext * * Retrieves the accessible role of this context. * * Returns: a #GtkAccessibleRole */ GtkAccessibleRole gtk_at_context_get_accessible_role (GtkATContext *self) { g_return_val_if_fail (GTK_IS_AT_CONTEXT (self), GTK_ACCESSIBLE_ROLE_NONE); return self->accessible_role; } /*< private > * gtk_at_context_get_display: * @self: a #GtkATContext * * Retrieves the #GdkDisplay used to create the context. * * Returns: (transfer none): a #GdkDisplay */ GdkDisplay * gtk_at_context_get_display (GtkATContext *self) { g_return_val_if_fail (GTK_IS_AT_CONTEXT (self), NULL); return self->display; } static const struct { const char *name; GtkATContext * (* create_context) (GtkAccessibleRole accessible_role, GtkAccessible *accessible, GdkDisplay *display); } a11y_backends[] = { #if defined(GDK_WINDOWING_WAYLAND) { "AT-SPI (Wayland)", gtk_at_spi_create_context }, #endif #if defined(GDK_WINDOWING_X11) { "AT-SPI (X11)", gtk_at_spi_create_context }, #endif { NULL, NULL }, }; /** * gtk_at_context_create: (constructor) * @accessible_role: the accessible role used by the #GtkATContext * @accessible: the #GtkAccessible implementation using the #GtkATContext * @display: the #GdkDisplay used by the #GtkATContext * * Creates a new #GtkATContext instance for the given accessible role, * accessible instance, and display connection. * * The #GtkATContext implementation being instantiated will depend on the * platform. * * Returns: (nullable) (transfer full): the #GtkATContext */ GtkATContext * gtk_at_context_create (GtkAccessibleRole accessible_role, GtkAccessible *accessible, GdkDisplay *display) { static const char *gtk_test_accessible; static const char *gtk_no_a11y; if (G_UNLIKELY (gtk_test_accessible == NULL)) { const char *env = g_getenv ("GTK_TEST_ACCESSIBLE"); if (env != NULL && *env !='\0') gtk_test_accessible = "1"; else gtk_test_accessible = "0"; } if (G_UNLIKELY (gtk_no_a11y == NULL)) { const char *env = g_getenv ("GTK_NO_A11Y"); if (env != NULL && *env != '\0') gtk_no_a11y = "1"; else gtk_no_a11y = "0"; } /* Shortcut everything if we're running with the test AT context */ if (gtk_test_accessible[0] == '1') return gtk_test_at_context_new (accessible_role, accessible); if (gtk_no_a11y[0] == '1') return NULL; GtkATContext *res = NULL; for (guint i = 0; i < G_N_ELEMENTS (a11y_backends); i++) { if (a11y_backends[i].name == NULL) break; GTK_NOTE (A11Y, g_message ("Trying %s a11y backend", a11y_backends[i].name)); if (a11y_backends[i].create_context != NULL) { res = a11y_backends[i].create_context (accessible_role, accessible, display); if (res != NULL) break; } } /* Fall back to the test context, so we can get debugging data */ if (res == NULL) res = g_object_new (GTK_TYPE_TEST_AT_CONTEXT, "accessible_role", accessible_role, "accessible", accessible, "display", display, NULL); /* FIXME: Add GIOExtension for AT contexts */ return res; } /*< private > * gtk_at_context_update: * @self: a #GtkATContext * * Notifies the AT connected to this #GtkATContext that the accessible * state and its properties have changed. */ void gtk_at_context_update (GtkATContext *self) { g_return_if_fail (GTK_IS_AT_CONTEXT (self)); /* There's no point in notifying of state changes if there weren't any */ if (self->updated_properties == 0 && self->updated_relations == 0 && self->updated_states == 0) return; GtkAccessibleStateChange changed_states = gtk_accessible_attribute_set_get_changed (self->states); GtkAccessiblePropertyChange changed_properties = gtk_accessible_attribute_set_get_changed (self->properties); GtkAccessibleRelationChange changed_relations = gtk_accessible_attribute_set_get_changed (self->relations); GTK_AT_CONTEXT_GET_CLASS (self)->state_change (self, changed_states, changed_properties, changed_relations, self->states, self->properties, self->relations); g_signal_emit (self, obj_signals[STATE_CHANGE], 0); self->updated_properties = 0; self->updated_relations = 0; self->updated_states = 0; } /*< private > * gtk_at_context_set_accessible_state: * @self: a #GtkATContext * @state: a #GtkAccessibleState * @value: (nullable): #GtkAccessibleValue * * Sets the @value for the given @state of a #GtkATContext. * * If @value is %NULL, the state is unset. * * This function will accumulate state changes until gtk_at_context_update() * is called. */ void gtk_at_context_set_accessible_state (GtkATContext *self, GtkAccessibleState state, GtkAccessibleValue *value) { g_return_if_fail (GTK_IS_AT_CONTEXT (self)); gboolean res = FALSE; if (value != NULL) res = gtk_accessible_attribute_set_add (self->states, state, value); else res = gtk_accessible_attribute_set_remove (self->states, state); if (res) self->updated_states |= (1 << state); } /*< private > * gtk_at_context_has_accessible_state: * @self: a #GtkATContext * @state: a #GtkAccessibleState * * Checks whether a #GtkATContext has the given @state set. * * Returns: %TRUE, if the accessible state is set */ gboolean gtk_at_context_has_accessible_state (GtkATContext *self, GtkAccessibleState state) { g_return_val_if_fail (GTK_IS_AT_CONTEXT (self), FALSE); return gtk_accessible_attribute_set_contains (self->states, state); } /*< private > * gtk_at_context_get_accessible_state: * @self: a #GtkATContext * @state: a #GtkAccessibleState * * Retrieves the value for the accessible state of a #GtkATContext. * * Returns: (transfer none): the value for the given state */ GtkAccessibleValue * gtk_at_context_get_accessible_state (GtkATContext *self, GtkAccessibleState state) { g_return_val_if_fail (GTK_IS_AT_CONTEXT (self), NULL); return gtk_accessible_attribute_set_get_value (self->states, state); } /*< private > * gtk_at_context_set_accessible_property: * @self: a #GtkATContext * @property: a #GtkAccessibleProperty * @value: (nullable): #GtkAccessibleValue * * Sets the @value for the given @property of a #GtkATContext. * * If @value is %NULL, the property is unset. * * This function will accumulate property changes until gtk_at_context_update() * is called. */ void gtk_at_context_set_accessible_property (GtkATContext *self, GtkAccessibleProperty property, GtkAccessibleValue *value) { g_return_if_fail (GTK_IS_AT_CONTEXT (self)); gboolean res = FALSE; if (value != NULL) res = gtk_accessible_attribute_set_add (self->properties, property, value); else res = gtk_accessible_attribute_set_remove (self->properties, property); if (res) self->updated_properties |= (1 << property); } /*< private > * gtk_at_context_has_accessible_property: * @self: a #GtkATContext * @property: a #GtkAccessibleProperty * * Checks whether a #GtkATContext has the given @property set. * * Returns: %TRUE, if the accessible property is set */ gboolean gtk_at_context_has_accessible_property (GtkATContext *self, GtkAccessibleProperty property) { g_return_val_if_fail (GTK_IS_AT_CONTEXT (self), FALSE); return gtk_accessible_attribute_set_contains (self->properties, property); } /*< private > * gtk_at_context_get_accessible_property: * @self: a #GtkATContext * @property: a #GtkAccessibleProperty * * Retrieves the value for the accessible property of a #GtkATContext. * * Returns: (transfer none): the value for the given property */ GtkAccessibleValue * gtk_at_context_get_accessible_property (GtkATContext *self, GtkAccessibleProperty property) { g_return_val_if_fail (GTK_IS_AT_CONTEXT (self), NULL); return gtk_accessible_attribute_set_get_value (self->properties, property); } /*< private > * gtk_at_context_set_accessible_relation: * @self: a #GtkATContext * @relation: a #GtkAccessibleRelation * @value: (nullable): #GtkAccessibleValue * * Sets the @value for the given @relation of a #GtkATContext. * * If @value is %NULL, the relation is unset. * * This function will accumulate relation changes until gtk_at_context_update() * is called. */ void gtk_at_context_set_accessible_relation (GtkATContext *self, GtkAccessibleRelation relation, GtkAccessibleValue *value) { g_return_if_fail (GTK_IS_AT_CONTEXT (self)); gboolean res = FALSE; if (value != NULL) res = gtk_accessible_attribute_set_add (self->relations, relation, value); else res = gtk_accessible_attribute_set_remove (self->relations, relation); if (res) self->updated_relations |= (1 << relation); } /*< private > * gtk_at_context_has_accessible_relation: * @self: a #GtkATContext * @relation: a #GtkAccessibleRelation * * Checks whether a #GtkATContext has the given @relation set. * * Returns: %TRUE, if the accessible relation is set */ gboolean gtk_at_context_has_accessible_relation (GtkATContext *self, GtkAccessibleRelation relation) { g_return_val_if_fail (GTK_IS_AT_CONTEXT (self), FALSE); return gtk_accessible_attribute_set_contains (self->relations, relation); } /*< private > * gtk_at_context_get_accessible_relation: * @self: a #GtkATContext * @relation: a #GtkAccessibleRelation * * Retrieves the value for the accessible relation of a #GtkATContext. * * Returns: (transfer none): the value for the given relation */ GtkAccessibleValue * gtk_at_context_get_accessible_relation (GtkATContext *self, GtkAccessibleRelation relation) { g_return_val_if_fail (GTK_IS_AT_CONTEXT (self), NULL); return gtk_accessible_attribute_set_get_value (self->relations, relation); } /*< private > * gtk_at_context_get_label: * @self: a #GtkATContext * * Retrieves the accessible label of the #GtkATContext. * * This is a convenience function meant to be used by #GtkATContext implementations. * * Returns: (transfer full): the label of the #GtkATContext */ char * gtk_at_context_get_label (GtkATContext *self) { g_return_val_if_fail (GTK_IS_AT_CONTEXT (self), NULL); GtkAccessibleValue *value = NULL; if (gtk_accessible_attribute_set_contains (self->states, GTK_ACCESSIBLE_STATE_HIDDEN)) { value = gtk_accessible_attribute_set_get_value (self->states, GTK_ACCESSIBLE_STATE_HIDDEN); if (gtk_boolean_accessible_value_get (value)) return g_strdup (""); } if (gtk_accessible_attribute_set_contains (self->properties, GTK_ACCESSIBLE_PROPERTY_LABEL)) { value = gtk_accessible_attribute_set_get_value (self->properties, GTK_ACCESSIBLE_PROPERTY_LABEL); return g_strdup (gtk_string_accessible_value_get (value)); } if (gtk_accessible_attribute_set_contains (self->relations, GTK_ACCESSIBLE_RELATION_LABELLED_BY)) { value = gtk_accessible_attribute_set_get_value (self->relations, GTK_ACCESSIBLE_RELATION_LABELLED_BY); GtkAccessible *rel = gtk_reference_accessible_value_get (value); GtkATContext *rel_context = gtk_accessible_get_at_context (rel); return gtk_at_context_get_label (rel_context); } GtkAccessibleRole role = gtk_at_context_get_accessible_role (self); switch ((int) role) { case GTK_ACCESSIBLE_ROLE_RANGE: { int range_attrs[] = { GTK_ACCESSIBLE_PROPERTY_VALUE_TEXT, GTK_ACCESSIBLE_PROPERTY_VALUE_NOW, }; for (int i = 0; i < G_N_ELEMENTS (range_attrs); i++) { if (gtk_accessible_attribute_set_contains (self->properties, range_attrs[i])) { value = gtk_accessible_attribute_set_get_value (self->properties, range_attrs[i]); break; } } if (value != NULL) return g_strdup (gtk_string_accessible_value_get (value)); } break; default: break; } GEnumClass *enum_class = g_type_class_peek (GTK_TYPE_ACCESSIBLE_ROLE); GEnumValue *enum_value = g_enum_get_value (enum_class, role); if (enum_value != NULL) return g_strdup (enum_value->value_nick); return g_strdup ("widget"); }