Merge branch 'redo-a11y-name-computation' into 'main'

Reimplement a11y name computation

See merge request GNOME/gtk!6119
This commit is contained in:
Matthias Clasen 2023-06-19 17:49:22 +00:00
commit 72e5697804
7 changed files with 390 additions and 259 deletions

View File

@ -121,10 +121,7 @@ gtk_at_context_set_property (GObject *gobject,
switch (prop_id)
{
case PROP_ACCESSIBLE_ROLE:
if (!self->realized)
self->accessible_role = g_value_get_enum (value);
else
g_critical ("The accessible role cannot be set on a realized AT context");
gtk_at_context_set_accessible_role (self, g_value_get_enum (value));
break;
case PROP_ACCESSIBLE:
@ -1010,10 +1007,6 @@ gtk_at_context_get_accessible_relation (GtkATContext *self,
return gtk_accessible_attribute_set_get_value (self->relations, relation);
}
/* See the WAI-ARIA § 4.3, "Accessible Name and Description Computation",
* and https://www.w3.org/TR/accname-1.2/
*/
/* See ARIA 5.2.8.4, 5.2.8.5 and 5.2.8.6 for the prohibited, from author
* and from content parts, and the table in
* https://www.w3.org/WAI/ARIA/apg/practices/names-and-descriptions/
@ -1150,172 +1143,6 @@ gtk_accessible_role_get_naming (GtkAccessibleRole role)
return (GtkAccessibleNaming) (naming[role] & ~(NAME_FROM_AUTHOR|NAME_FROM_CONTENT));
}
static void
gtk_at_context_get_name_accumulate (GtkATContext *self,
GPtrArray *names,
gboolean recurse)
{
GtkAccessibleValue *value = NULL;
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);
g_ptr_array_add (names, (char *) gtk_string_accessible_value_get (value));
}
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);
for (GList *l = list; l != NULL; l = l->next)
{
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);
g_object_unref (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,
};
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 (names, (char *) gtk_string_accessible_value_get (value));
}
break;
default:
break;
}
/* 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 (gtk_boolean_accessible_value_get (value))
return;
}
if (names->len == 0)
{
if (GTK_IS_WIDGET (self->accessible))
{
const char *tooltip = gtk_widget_get_tooltip_text (GTK_WIDGET (self->accessible));
if (tooltip)
g_ptr_array_add (names, (char *) tooltip);
}
}
}
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->next)
{
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);
g_object_unref (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,
};
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;
}
if (labels->len == 0)
{
if (GTK_IS_WIDGET (self->accessible))
{
const char *tooltip = gtk_widget_get_tooltip_text (GTK_WIDGET (self->accessible));
if (tooltip)
g_ptr_array_add (labels, (char *) tooltip);
}
}
}
static gboolean
is_nested_button (GtkATContext *self)
{
@ -1366,24 +1193,185 @@ get_parent_context (GtkATContext *self)
return g_object_ref (self);
}
static inline gboolean
not_just_space (const char *text)
{
for (const char *p = text; *p; p = g_utf8_next_char (p))
{
if (!g_unichar_isspace (g_utf8_get_char (p)))
return TRUE;
}
/*< 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`
return FALSE;
}
static inline void
append_with_space (GString *str,
const char *text)
{
if (str->len > 0)
g_string_append (str, " ");
g_string_append (str, text);
}
/* See the WAI-ARIA § 4.3, "Accessible Name and Description Computation",
* and https://www.w3.org/TR/accname-1.2/
*/
char *
gtk_at_context_get_name (GtkATContext *self)
static void
gtk_at_context_get_text_accumulate (GtkATContext *self,
GPtrArray *nodes,
GString *res,
GtkAccessibleProperty property,
GtkAccessibleRelation relation,
gboolean is_ref,
gboolean is_child)
{
GtkAccessibleValue *value = NULL;
g_warn_if_fail (self->realized);
/* Step 2.A */
if (!is_ref)
{
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;
}
}
if (gtk_accessible_role_supports_name_from_author (self->accessible_role))
{
/* Step 2.B */
if (!is_ref && gtk_accessible_attribute_set_contains (self->relations, relation))
{
value = gtk_accessible_attribute_set_get_value (self->relations, relation);
GList *list = gtk_reference_list_accessible_value_get (value);
for (GList *l = list; l != NULL; l = l->next)
{
GtkAccessible *rel = GTK_ACCESSIBLE (l->data);
if (!g_ptr_array_find (nodes, rel, NULL))
{
GtkATContext *rel_context = gtk_accessible_get_at_context (rel);
g_ptr_array_add (nodes, rel);
gtk_at_context_get_text_accumulate (rel_context, nodes, res, property, relation, TRUE, FALSE);
g_object_unref (rel_context);
}
}
return;
}
/* Step 2.C */
if (gtk_accessible_attribute_set_contains (self->properties, property))
{
value = gtk_accessible_attribute_set_get_value (self->properties, property);
char *str = (char *) gtk_string_accessible_value_get (value);
if (str[0] != '\0')
{
append_with_space (res, gtk_string_accessible_value_get (value));
return;
}
}
}
/* Step 2.E */
if (self->accessible_role == GTK_ACCESSIBLE_ROLE_TEXT_BOX)
{
if (GTK_IS_EDITABLE (self->accessible))
{
const char *text = gtk_editable_get_text (GTK_EDITABLE (self->accessible));
if (text && not_just_space (text))
append_with_space (res, text);
}
return;
}
else if (gtk_accessible_role_is_range_subclass (self->accessible_role))
{
if (gtk_accessible_attribute_set_contains (self->properties, GTK_ACCESSIBLE_PROPERTY_VALUE_TEXT))
{
value = gtk_accessible_attribute_set_get_value (self->properties, GTK_ACCESSIBLE_PROPERTY_VALUE_TEXT);
append_with_space (res, gtk_string_accessible_value_get (value));
}
else if (gtk_accessible_attribute_set_contains (self->properties, GTK_ACCESSIBLE_PROPERTY_VALUE_NOW))
{
value = gtk_accessible_attribute_set_get_value (self->properties, GTK_ACCESSIBLE_PROPERTY_VALUE_NOW);
if (res->len > 0)
g_string_append (res, " ");
g_string_append_printf (res, "%g", gtk_number_accessible_value_get (value));
}
return;
}
/* Step 2.F */
if (gtk_accessible_role_supports_name_from_content (self->accessible_role) || is_ref || is_child)
{
if (GTK_IS_WIDGET (self->accessible))
{
gboolean has_child = FALSE;
for (GtkWidget *child = gtk_widget_get_first_child (GTK_WIDGET (self->accessible));
child != NULL;
child = gtk_widget_get_next_sibling (child))
{
GtkAccessible *rel = GTK_ACCESSIBLE (child);
GtkATContext *rel_context = gtk_accessible_get_at_context (rel);
gtk_at_context_get_text_accumulate (rel_context, nodes, res, property, relation, FALSE, TRUE);
has_child = TRUE;
}
if (has_child)
return;
}
}
/* Step 2.G */
if (GTK_IS_LABEL (self->accessible))
{
const char *text = gtk_label_get_text (GTK_LABEL (self->accessible));
if (text && not_just_space (text))
append_with_space (res, text);
return;
}
else if (GTK_IS_INSCRIPTION (self->accessible))
{
const char *text = gtk_inscription_get_text (GTK_INSCRIPTION (self->accessible));
if (text && not_just_space (text))
append_with_space (res, text);
return;
}
/* Step 2.I */
if (GTK_IS_WIDGET (self->accessible))
{
const char *text = gtk_widget_get_tooltip_text (GTK_WIDGET (self->accessible));
if (text && not_just_space (text))
append_with_space (res, text);
}
}
static char *
gtk_at_context_get_text (GtkATContext *self,
GtkAccessibleProperty property,
GtkAccessibleRelation relation)
{
GtkATContext *parent = NULL;
g_return_val_if_fail (GTK_IS_AT_CONTEXT (self), NULL);
g_warn_if_fail (self->realized);
/* Step 1 */
if (gtk_accessible_role_get_naming (self->accessible_role) == GTK_ACCESSIBLE_NAME_PROHIBITED)
return g_strdup ("");
@ -1405,33 +1393,35 @@ gtk_at_context_get_name (GtkATContext *self)
}
}
GPtrArray *names = g_ptr_array_new ();
gtk_at_context_get_name_accumulate (self, names, TRUE);
if (names->len == 0)
{
g_ptr_array_unref (names);
g_clear_object (&parent);
return g_strdup ("");
}
GPtrArray *nodes = g_ptr_array_new ();
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));
}
/* Step 2 */
gtk_at_context_get_text_accumulate (self, nodes, res, property, relation, FALSE, FALSE);
g_ptr_array_unref (names);
g_ptr_array_unref (nodes);
g_clear_object (&parent);
return g_string_free (res, FALSE);
}
/*< 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)
{
return gtk_at_context_get_text (self, GTK_ACCESSIBLE_PROPERTY_LABEL, GTK_ACCESSIBLE_RELATION_LABELLED_BY);
}
/*< private >
* gtk_at_context_get_description:
* @self: a `GtkATContext`
@ -1445,49 +1435,7 @@ gtk_at_context_get_name (GtkATContext *self)
char *
gtk_at_context_get_description (GtkATContext *self)
{
GtkATContext *parent = NULL;
g_return_val_if_fail (GTK_IS_AT_CONTEXT (self), NULL);
if (gtk_accessible_role_get_naming (self->accessible_role) == GTK_ACCESSIBLE_NAME_PROHIBITED)
return g_strdup ("");
/* We special case this here since it is a common pattern:
* We have a 'wrapper' object, like a GtkDropdown which
* contains a toggle button. The dropdown appears in the
* ui file and carries all the a11y attributes, but the
* focus ends up on the toggle button.
*/
if (is_nested_button (self))
{
parent = get_parent_context (self);
self = parent;
}
GPtrArray *names = g_ptr_array_new ();
gtk_at_context_get_description_accumulate (self, names, TRUE);
if (names->len == 0)
{
g_ptr_array_unref (names);
g_clear_object (&parent);
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);
g_clear_object (&parent);
return g_string_free (res, FALSE);
return gtk_at_context_get_text (self, GTK_ACCESSIBLE_PROPERTY_DESCRIPTION, GTK_ACCESSIBLE_RELATION_DESCRIBED_BY);
}
void

View File

@ -797,11 +797,6 @@ gtk_inscription_set_text (GtkInscription *self,
g_free (self->text);
self->text = g_strdup (text);
gtk_accessible_update_property (GTK_ACCESSIBLE (self),
GTK_ACCESSIBLE_PROPERTY_LABEL, self->text,
-1);
pango_layout_set_text (self->layout,
self->text ? self->text : "",
-1);

View File

@ -3067,10 +3067,6 @@ 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, self->text,
-1);
gtk_label_select_region_index (self, 0, 0);
}

View File

@ -2397,7 +2397,8 @@ gtk_widget_root_at_context (GtkWidget *self)
}
gtk_at_context_set_accessible_role (priv->at_context, role);
gtk_at_context_set_display (priv->at_context, gtk_root_get_display (priv->root));
if (priv->root)
gtk_at_context_set_display (priv->at_context, gtk_root_get_display (priv->root));
}
static void

View File

@ -45,11 +45,13 @@ label_properties (void)
g_object_ref_sink (label);
gtk_test_accessible_assert_property (label, GTK_ACCESSIBLE_PROPERTY_LABEL, "a");
gtk_label_set_selectable (GTK_LABEL (label), TRUE);
gtk_label_set_label (GTK_LABEL (label), "b");
gtk_test_accessible_assert_property (GTK_ACCESSIBLE (label), GTK_ACCESSIBLE_PROPERTY_HAS_POPUP, TRUE);
gtk_test_accessible_assert_property (label, GTK_ACCESSIBLE_PROPERTY_LABEL, "b");
gtk_label_set_selectable (GTK_LABEL (label), FALSE);
g_assert_false (gtk_test_accessible_has_property (GTK_ACCESSIBLE (label), GTK_ACCESSIBLE_PROPERTY_HAS_POPUP));
g_object_unref (label);
}

View File

@ -33,6 +33,9 @@ tests = [
{ 'name': 'window' },
]
internal_tests = [
{ 'name': 'names' },
]
is_debug = get_option('buildtype').startswith('debug')
@ -80,3 +83,31 @@ foreach t : tests
suite: ['a11y'] + test_extra_suites,
)
endforeach
foreach t : internal_tests
test_name = t.get('name')
test_srcs = ['@0@.c'.format(test_name)] + t.get('sources', [])
test_extra_cargs = t.get('c_args', [])
test_extra_ldflags = t.get('link_args', [])
test_extra_suites = t.get('suites', [])
test_timeout = 60
test_exe = executable(test_name,
sources: test_srcs,
c_args: test_cargs + test_extra_cargs + ['-DGTK_COMPILATION'],
link_args: test_extra_ldflags,
dependencies: libgtk_static_dep,
)
if test_extra_suites.contains('slow')
test_timeout = 90
endif
test(test_name, test_exe,
args: [ '--tap', '-k' ],
protocol: 'tap',
timeout: test_timeout,
env: test_env,
suite: ['a11y'] + test_extra_suites,
)
endforeach

158
testsuite/a11y/names.c Normal file
View File

@ -0,0 +1,158 @@
#include <gtk/gtk.h>
#include "gtk/gtkatcontextprivate.h"
#include "gtk/gtkwidgetprivate.h"
static void
test_name_content (void)
{
GtkWidget *label1, *label2, *box, *button;
char *name;
label1 = gtk_label_new ("a");
label2 = gtk_label_new ("b");
box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
button = gtk_button_new ();
gtk_box_append (GTK_BOX (box), label1);
gtk_box_append (GTK_BOX (box), label2);
gtk_button_set_child (GTK_BUTTON (button), box);
g_object_ref_sink (button);
/* gtk_at_context_get_name only works on realized contexts */
gtk_widget_realize_at_context (label1);
gtk_widget_realize_at_context (label2);
gtk_widget_realize_at_context (box);
gtk_widget_realize_at_context (button);
name = gtk_at_context_get_name (gtk_accessible_get_at_context (GTK_ACCESSIBLE (label1)));
g_assert_cmpstr (name, ==, "a");
g_free (name);
/* this is because generic doesn't allow naming */
name = gtk_at_context_get_name (gtk_accessible_get_at_context (GTK_ACCESSIBLE (box)));
g_assert_cmpstr (name, ==, "");
g_free (name);
name = gtk_at_context_get_name (gtk_accessible_get_at_context (GTK_ACCESSIBLE (button)));
g_assert_cmpstr (name, ==, "a b");
g_free (name);
gtk_widget_set_visible (label2, FALSE);
name = gtk_at_context_get_name (gtk_accessible_get_at_context (GTK_ACCESSIBLE (button)));
g_assert_cmpstr (name, ==, "a");
g_free (name);
g_object_unref (button);
}
static void
test_name_tooltip (void)
{
GtkWidget *image = gtk_image_new ();
char *name;
g_object_ref_sink (image);
gtk_widget_realize_at_context (image);
gtk_widget_set_tooltip_text (image, "tooltip");
name = gtk_at_context_get_name (gtk_accessible_get_at_context (GTK_ACCESSIBLE (image)));
g_assert_cmpstr (name, ==, "tooltip");
g_free (name);
g_object_unref (image);
}
static void
test_name_label (void)
{
GtkWidget *image = gtk_image_new ();
char *name;
char *desc;
g_object_ref_sink (image);
gtk_widget_realize_at_context (image);
gtk_widget_set_tooltip_text (image, "tooltip");
gtk_accessible_update_property (GTK_ACCESSIBLE (image),
GTK_ACCESSIBLE_PROPERTY_LABEL, "label",
-1);
name = gtk_at_context_get_name (gtk_accessible_get_at_context (GTK_ACCESSIBLE (image)));
desc = gtk_at_context_get_description (gtk_accessible_get_at_context (GTK_ACCESSIBLE (image)));
g_assert_cmpstr (name, ==, "label");
g_assert_cmpstr (desc, ==, "tooltip");
g_free (name);
g_free (desc);
g_object_unref (image);
}
static void
test_name_prohibited (void)
{
GtkWidget *widget;
char *name;
char *desc;
widget = g_object_new (GTK_TYPE_BUTTON,
"accessible-role", GTK_ACCESSIBLE_ROLE_TIME,
"label", "too late",
NULL);
g_object_ref_sink (widget);
gtk_widget_realize_at_context (widget);
name = gtk_at_context_get_name (gtk_accessible_get_at_context (GTK_ACCESSIBLE (widget)));
desc = gtk_at_context_get_description (gtk_accessible_get_at_context (GTK_ACCESSIBLE (widget)));
g_assert_cmpstr (name, ==, "");
g_assert_cmpstr (desc, ==, "");
g_free (name);
g_free (desc);
g_object_unref (widget);
}
static void
test_name_range (void)
{
GtkWidget *scale;
char *name;
scale = gtk_scale_new_with_range (GTK_ORIENTATION_HORIZONTAL, 0, 100, 10);
g_object_ref_sink (scale);
gtk_widget_realize_at_context (scale);
g_assert_true (gtk_accessible_get_accessible_role (GTK_ACCESSIBLE (scale)) == GTK_ACCESSIBLE_ROLE_SLIDER);
g_assert_true (gtk_at_context_get_accessible_role (gtk_accessible_get_at_context (GTK_ACCESSIBLE (scale))) == GTK_ACCESSIBLE_ROLE_SLIDER);
gtk_range_set_value (GTK_RANGE (scale), 50);
name = gtk_at_context_get_name (gtk_accessible_get_at_context (GTK_ACCESSIBLE (scale)));
g_assert_cmpstr (name, ==, "50");
g_free (name);
g_object_unref (scale);
}
int
main (int argc, char *argv[])
{
gtk_test_init (&argc, &argv, NULL);
g_test_add_func ("/a11y/name/content", test_name_content);
g_test_add_func ("/a11y/name/tooltip", test_name_tooltip);
g_test_add_func ("/a11y/name/label", test_name_label);
g_test_add_func ("/a11y/name/prohibited", test_name_prohibited);
g_test_add_func ("/a11y/name/range", test_name_range);
return g_test_run ();
}