diff --git a/docs/reference/gtk/section-accessibility.md b/docs/reference/gtk/section-accessibility.md index ecd3dacd26..5576066e2f 100644 --- a/docs/reference/gtk/section-accessibility.md +++ b/docs/reference/gtk/section-accessibility.md @@ -220,3 +220,71 @@ which acts as a proxy to the specific platform's accessibility API: Additionally, an ad hoc accessibility backend is available for the GTK testsuite, to ensure reproducibility of issues in the CI pipeline. + +## Authoring practices {#authoring-practices} + +The authoring practices are aimed at application developers, as well as +developers of GUI elements based on GTK. + +Functionally, #GtkAccessible roles, states, properties, and relations are +analogous to a CSS for assistive technologies. For screen reader users, for +instance, the various accessible attributes control the rendering of their +non-visual experience. Incorrect roles and attributes may result in a +completely inaccessible user interface. + +### A role is a promise + +The following code: + +```c +gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_BUTTON); +``` + +is a promise that the widget being created will provide the same keyboard +interactions expected for a button. An accessible role of a button will not +turn automatically any widget into a #GtkButton; but if your widget behaves +like a button, using the %GTK_ACCESSIBLE_ROLE_BUTTON will allow any +assistive technology to handle it like they would a #GtkButton. + +### Attributes can both hide and enhance + +Accessible attributes can be used to override the content of a UI element, +for instance: + +```c +gtk_label_set_text (GTK_LABEL (label), "Some text"); +gtk_accessible_update_property (GTK_ACCESSIBLE (label), + GTK_ACCESSIBLE_PROPERTY_LABEL, + "Assistive technologies users will perceive " + "this text, not the contents of the label", + -1); +``` + +In the example above, the "label" property will override the contents of the +label widget. + +The attributes can also enhance the UI: + +```c +gtk_button_set_label (GTK_BUTTON (button), "Download"); +gtk_box_append (GTK_BOX (button), button); + +gtk_label_set_text (GTK_LABEL (label), "Final report.pdf"); +gtk_box_append (GTK_BOX (box), label); + +gtk_accessible_update_relation (GTK_ACCESSIBLE (button), + GTK_ACCESSIBLE_RELATION_LABELLED_BY, + g_list_append (NULL, label), + -1); +``` + +In the example above, an assistive technology will read the button's +accessible label as "Download Final report.pdf". + +The power of hiding and enhancing can be a double-edged sword, as it can +lead to inadvertently overriding the accessible semantics of existing +widgets. + +## Design patterns and custom widgets + +... diff --git a/gtk/a11y/gtkatspicontext.c b/gtk/a11y/gtkatspicontext.c index 8d378685a9..9c801bf0f3 100644 --- a/gtk/a11y/gtkatspicontext.c +++ b/gtk/a11y/gtkatspicontext.c @@ -593,22 +593,14 @@ handle_accessible_get_property (GDBusConnection *connection, if (g_strcmp0 (property_name, "Name") == 0) { - if (GTK_IS_WIDGET (accessible)) - res = g_variant_new_string (gtk_widget_get_name (GTK_WIDGET (accessible))); - else if (GTK_IS_STACK_PAGE (accessible)) - { - const char *name = gtk_stack_page_get_name (GTK_STACK_PAGE (accessible)); - if (name == NULL) - name = G_OBJECT_TYPE_NAME (accessible); - res = g_variant_new_string (name); - } - else - res = g_variant_new_string (G_OBJECT_TYPE_NAME (accessible)); + char *label = gtk_at_context_get_name (GTK_AT_CONTEXT (self)); + res = g_variant_new_string (label ? label : ""); + g_free (label); } else if (g_strcmp0 (property_name, "Description") == 0) { - char *label = gtk_at_context_get_label (GTK_AT_CONTEXT (self)); - res = g_variant_new_string (label); + char *label = gtk_at_context_get_description (GTK_AT_CONTEXT (self)); + res = g_variant_new_string (label ? label : ""); g_free (label); } else if (g_strcmp0 (property_name, "Locale") == 0) @@ -953,7 +945,14 @@ gtk_at_spi_context_state_change (GtkATContext *ctx, if (changed_properties & GTK_ACCESSIBLE_PROPERTY_CHANGE_LABEL) { - char *label = gtk_at_context_get_label (GTK_AT_CONTEXT (self)); + char *label = gtk_at_context_get_name (GTK_AT_CONTEXT (self)); + GVariant *v = g_variant_new_take_string (label); + emit_property_changed (self, "accessible-name", v); + } + + if (changed_properties & GTK_ACCESSIBLE_PROPERTY_CHANGE_DESCRIPTION) + { + char *label = gtk_at_context_get_description (GTK_AT_CONTEXT (self)); GVariant *v = g_variant_new_take_string (label); emit_property_changed (self, "accessible-description", v); } diff --git a/gtk/gtkatcontext.c b/gtk/gtkatcontext.c index 1925f5bf80..8a62e2821d 100644 --- a/gtk/gtkatcontext.c +++ b/gtk/gtkatcontext.c @@ -739,47 +739,34 @@ gtk_at_context_get_accessible_relation (GtkATContext *self, 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) +/* See the WAI-ARIA ยง 4.3, "Accessible Name and Description Computation" */ +static void +gtk_at_context_get_name_accumulate (GtkATContext *self, + GPtrArray *names, + gboolean recurse) { - 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)); + g_ptr_array_add (names, (char *) gtk_string_accessible_value_get (value)); } - if (gtk_accessible_attribute_set_contains (self->relations, GTK_ACCESSIBLE_RELATION_LABELLED_BY)) + if (recurse && 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); GList *list = gtk_reference_list_accessible_value_get (value); - GtkAccessible *rel = GTK_ACCESSIBLE (list->data); - GtkATContext *rel_context = gtk_accessible_get_at_context (rel); - return gtk_at_context_get_label (rel_context); + for (GList *l = list; l != NULL; l = l->data) + { + GtkAccessible *rel = GTK_ACCESSIBLE (l->data); + GtkATContext *rel_context = gtk_accessible_get_at_context (rel); + + gtk_at_context_get_name_accumulate (rel_context, names, FALSE); + } } GtkAccessibleRole role = gtk_at_context_get_accessible_role (self); @@ -793,6 +780,7 @@ gtk_at_context_get_label (GtkATContext *self) GTK_ACCESSIBLE_PROPERTY_VALUE_NOW, }; + value = NULL; for (int i = 0; i < G_N_ELEMENTS (range_attrs); i++) { if (gtk_accessible_attribute_set_contains (self->properties, range_attrs[i])) @@ -803,7 +791,7 @@ gtk_at_context_get_label (GtkATContext *self) } if (value != NULL) - return g_strdup (gtk_string_accessible_value_get (value)); + g_ptr_array_add (names, (char *) gtk_string_accessible_value_get (value)); } break; @@ -811,13 +799,172 @@ gtk_at_context_get_label (GtkATContext *self) break; } - GEnumClass *enum_class = g_type_class_peek (GTK_TYPE_ACCESSIBLE_ROLE); - GEnumValue *enum_value = g_enum_get_value (enum_class, role); + /* If there is no label or labelled-by attribute, hidden elements + * have no name + */ + 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 (enum_value != NULL) - return g_strdup (enum_value->value_nick); + if (gtk_boolean_accessible_value_get (value)) + return; + } - return g_strdup ("widget"); + /* This fallback is in place only for unlabelled elements */ + if (names->len != 0) + return; + + if (self->accessible) + g_ptr_array_add (names, (char *)G_OBJECT_TYPE_NAME (self->accessible)); +} + +static void +gtk_at_context_get_description_accumulate (GtkATContext *self, + GPtrArray *labels, + gboolean recurse) +{ + GtkAccessibleValue *value = NULL; + + if (gtk_accessible_attribute_set_contains (self->properties, GTK_ACCESSIBLE_PROPERTY_DESCRIPTION)) + { + value = gtk_accessible_attribute_set_get_value (self->properties, GTK_ACCESSIBLE_PROPERTY_DESCRIPTION); + + g_ptr_array_add (labels, (char *) gtk_string_accessible_value_get (value)); + } + + if (recurse && gtk_accessible_attribute_set_contains (self->relations, GTK_ACCESSIBLE_RELATION_DESCRIBED_BY)) + { + value = gtk_accessible_attribute_set_get_value (self->relations, GTK_ACCESSIBLE_RELATION_DESCRIBED_BY); + + GList *list = gtk_reference_list_accessible_value_get (value); + + for (GList *l = list; l != NULL; l = l->data) + { + GtkAccessible *rel = GTK_ACCESSIBLE (l->data); + GtkATContext *rel_context = gtk_accessible_get_at_context (rel); + + gtk_at_context_get_description_accumulate (rel_context, labels, FALSE); + } + } + + 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, + }; + + value = NULL; + 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) + g_ptr_array_add (labels, (char *) gtk_string_accessible_value_get (value)); + } + break; + + default: + break; + } + + /* If there is no description or described-by attribute, hidden elements + * have no description + */ + 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; + } +} + +/*< private > + * gtk_at_context_get_name: + * @self: a #GtkATContext + * + * Retrieves the accessible name 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_name (GtkATContext *self) +{ + g_return_val_if_fail (GTK_IS_AT_CONTEXT (self), NULL); + + GPtrArray *names = g_ptr_array_new (); + + gtk_at_context_get_name_accumulate (self, names, TRUE); + + if (names->len == 0) + { + g_ptr_array_unref (names); + return g_strdup (""); + } + + GString *res = g_string_new (""); + g_string_append (res, g_ptr_array_index (names, 0)); + + for (guint i = 1; i < names->len; i++) + { + g_string_append (res, " "); + g_string_append (res, g_ptr_array_index (names, i)); + } + + g_ptr_array_unref (names); + + return g_string_free (res, FALSE); +} + +/*< private > + * gtk_at_context_get_description: + * @self: a #GtkATContext + * + * Retrieves the accessible description 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_description (GtkATContext *self) +{ + g_return_val_if_fail (GTK_IS_AT_CONTEXT (self), NULL); + + GPtrArray *names = g_ptr_array_new (); + + gtk_at_context_get_description_accumulate (self, names, TRUE); + + if (names->len == 0) + { + g_ptr_array_unref (names); + return g_strdup (""); + } + + GString *res = g_string_new (""); + g_string_append (res, g_ptr_array_index (names, 0)); + + for (guint i = 1; i < names->len; i++) + { + g_string_append (res, " "); + g_string_append (res, g_ptr_array_index (names, i)); + } + + g_ptr_array_unref (names); + + return g_string_free (res, FALSE); } void diff --git a/gtk/gtkatcontextprivate.h b/gtk/gtkatcontextprivate.h index d1495538c1..edc7fa3c94 100644 --- a/gtk/gtkatcontextprivate.h +++ b/gtk/gtkatcontextprivate.h @@ -152,7 +152,8 @@ gboolean gtk_at_context_has_accessible_relation (GtkATContext GtkAccessibleValue * gtk_at_context_get_accessible_relation (GtkATContext *self, GtkAccessibleRelation relation); -char * gtk_at_context_get_label (GtkATContext *self); +char * gtk_at_context_get_name (GtkATContext *self); +char * gtk_at_context_get_description (GtkATContext *self); void gtk_at_context_platform_changed (GtkATContext *self, GtkAccessiblePlatformChange change); diff --git a/gtk/gtklabel.c b/gtk/gtklabel.c index d23f0253a9..b6c1e43e49 100644 --- a/gtk/gtklabel.c +++ b/gtk/gtklabel.c @@ -1696,6 +1696,10 @@ gtk_label_set_text_internal (GtkLabel *self, g_free (self->text); self->text = str; + gtk_accessible_update_property (GTK_ACCESSIBLE (self), + GTK_ACCESSIBLE_PROPERTY_LABEL, str, + -1); + gtk_label_select_region_index (self, 0, 0); }