a11y: Reimplement name computation
authorMatthias Clasen <mclasen@redhat.com>
Sun, 18 Jun 2023 22:56:14 +0000 (18:56 -0400)
committerMatthias Clasen <mclasen@redhat.com>
Mon, 19 Jun 2023 16:38:51 +0000 (12:38 -0400)
Reimplement the name computation to follow the spec
(https://www.w3.org/TR/accname-1.2) more closely.

Also, unify the functions for name and description,
since their only difference is which property/relation
they use.

gtk/gtkatcontext.c

index bdd1597ae2a5f3e314aab3f5ac2d78589b7f49e1..6e1703f9b509b6099c76654220f8ab1e13420f41 100644 (file)
@@ -1007,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/
@@ -1147,124 +1143,158 @@ 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)
+static gboolean
+is_nested_button (GtkATContext *self)
 {
-  GtkAccessibleValue *value = NULL;
+  GtkAccessible *accessible;
+  GtkWidget *widget, *parent;
 
-  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);
+  accessible = gtk_at_context_get_accessible (self);
 
-      g_ptr_array_add (names, (char *) gtk_string_accessible_value_get (value));
-    }
+  if (!GTK_IS_WIDGET (accessible))
+    return FALSE;
 
-  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);
+  widget = GTK_WIDGET (accessible);
+  parent = gtk_widget_get_parent (widget);
 
-      GList *list = gtk_reference_list_accessible_value_get (value);
+  if ((GTK_IS_TOGGLE_BUTTON (widget) && GTK_IS_DROP_DOWN (parent)) ||
+      (GTK_IS_TOGGLE_BUTTON (widget) && GTK_IS_MENU_BUTTON (parent)) ||
+      (GTK_IS_BUTTON (widget) && GTK_IS_COLOR_DIALOG_BUTTON (parent)) ||
+      (GTK_IS_BUTTON (widget) && GTK_IS_FONT_DIALOG_BUTTON (parent))
+#ifdef G_OS_UNIX
+      || (GTK_IS_PRINTER_OPTION_WIDGET (parent) &&
+          (GTK_IS_CHECK_BUTTON (widget) ||
+           GTK_IS_DROP_DOWN (widget) ||
+           GTK_IS_ENTRY (widget) ||
+           GTK_IS_IMAGE (widget) ||
+           GTK_IS_LABEL (widget) ||
+           GTK_IS_BUTTON (widget)))
+#endif
+      )
+    return TRUE;
 
-      for (GList *l = list; l != NULL; l = l->next)
-        {
-          GtkAccessible *rel = GTK_ACCESSIBLE (l->data);
-          GtkATContext *rel_context = gtk_accessible_get_at_context (rel);
+  return FALSE;
+}
 
-          gtk_at_context_get_name_accumulate (rel_context, names, FALSE);
+static GtkATContext *
+get_parent_context (GtkATContext *self)
+{
+  GtkAccessible *accessible, *parent;
 
-          g_object_unref (rel_context);
-        }
+  accessible = gtk_at_context_get_accessible (self);
+  parent = gtk_accessible_get_accessible_parent (accessible);
+  if (parent)
+    {
+      GtkATContext *context = gtk_accessible_get_at_context (parent);
+      g_object_unref (parent);
+      return context;
     }
 
-  GtkAccessibleRole role = gtk_at_context_get_accessible_role (self);
+  return g_object_ref (self);
+}
 
-  switch ((int) role)
+static inline gboolean
+not_just_space (const char *text)
+{
+  for (const char *p = text; *p; p = g_utf8_next_char (p))
     {
-    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 (!g_unichar_isspace (g_utf8_get_char (p)))
+        return TRUE;
     }
 
-  /* 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;
-    }
+  return FALSE;
+}
 
-  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 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/
+ */
+
 static void
-gtk_at_context_get_description_accumulate (GtkATContext *self,
-                                           GPtrArray    *labels,
-                                           gboolean      recurse)
+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;
 
-  if (gtk_accessible_attribute_set_contains (self->properties, GTK_ACCESSIBLE_PROPERTY_DESCRIPTION))
+  /* Step 2.A */
+  if (!is_ref)
     {
-      value = gtk_accessible_attribute_set_get_value (self->properties, GTK_ACCESSIBLE_PROPERTY_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);
 
-      g_ptr_array_add (labels, (char *) gtk_string_accessible_value_get (value));
+          if (gtk_boolean_accessible_value_get (value))
+            return;
+        }
     }
 
-  if (recurse && gtk_accessible_attribute_set_contains (self->relations, GTK_ACCESSIBLE_RELATION_DESCRIBED_BY))
+  if (gtk_accessible_role_supports_name_from_author (self->accessible_role))
     {
-      value = gtk_accessible_attribute_set_get_value (self->relations, GTK_ACCESSIBLE_RELATION_DESCRIBED_BY);
+      /* 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);
+          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);
+          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);
 
-          gtk_at_context_get_description_accumulate (rel_context, labels, FALSE);
+                  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);
+                  g_object_unref (rel_context);
+                }
+            }
+
+          return;
         }
-    }
 
-  GtkAccessibleRole role = gtk_at_context_get_accessible_role (self);
+      /* 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;
+            }
+        }
+    }
 
-  switch ((int) role)
+  /* Step 2.E */
+  switch ((int) self->accessible_role)
     {
+    case 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;
+
     case GTK_ACCESSIBLE_ROLE_RANGE:
       {
         int range_attrs[] = {
@@ -1283,104 +1313,71 @@ gtk_at_context_get_description_accumulate (GtkATContext *self,
           }
 
         if (value != NULL)
-          g_ptr_array_add (labels, (char *) gtk_string_accessible_value_get (value));
+          append_with_space (res, gtk_string_accessible_value_get (value));
       }
-      break;
+      return;
 
     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)
+  /* Step 2.F */
+  if (gtk_accessible_role_supports_name_from_content (self->accessible_role) || is_ref || is_child)
     {
       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);
-        }
-    }
-}
+          gboolean has_child = FALSE;
 
-static gboolean
-is_nested_button (GtkATContext *self)
-{
-  GtkAccessible *accessible;
-  GtkWidget *widget, *parent;
+          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);
 
-  accessible = gtk_at_context_get_accessible (self);
+              gtk_at_context_get_text_accumulate (rel_context, nodes, res, property, relation, FALSE, TRUE);
 
-  if (!GTK_IS_WIDGET (accessible))
-    return FALSE;
+              has_child = TRUE;
+            }
 
-  widget = GTK_WIDGET (accessible);
-  parent = gtk_widget_get_parent (widget);
-
-  if ((GTK_IS_TOGGLE_BUTTON (widget) && GTK_IS_DROP_DOWN (parent)) ||
-      (GTK_IS_TOGGLE_BUTTON (widget) && GTK_IS_MENU_BUTTON (parent)) ||
-      (GTK_IS_BUTTON (widget) && GTK_IS_COLOR_DIALOG_BUTTON (parent)) ||
-      (GTK_IS_BUTTON (widget) && GTK_IS_FONT_DIALOG_BUTTON (parent))
-#ifdef G_OS_UNIX
-      || (GTK_IS_PRINTER_OPTION_WIDGET (parent) &&
-          (GTK_IS_CHECK_BUTTON (widget) ||
-           GTK_IS_DROP_DOWN (widget) ||
-           GTK_IS_ENTRY (widget) ||
-           GTK_IS_IMAGE (widget) ||
-           GTK_IS_LABEL (widget) ||
-           GTK_IS_BUTTON (widget)))
-#endif
-      )
-    return TRUE;
-
-  return FALSE;
-}
-
-static GtkATContext *
-get_parent_context (GtkATContext *self)
-{
-  GtkAccessible *accessible, *parent;
+           if (has_child)
+             return;
+        }
+    }
 
-  accessible = gtk_at_context_get_accessible (self);
-  parent = gtk_accessible_get_accessible_parent (accessible);
-  if (parent)
+  /* Step 2.G */
+  if (GTK_IS_LABEL (self->accessible))
     {
-      GtkATContext *context = gtk_accessible_get_at_context (parent);
-      g_object_unref (parent);
-      return context;
+      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;
     }
 
-  return g_object_ref (self);
+  /* 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);
+    }
 }
 
-
-/*< 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)
+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);
-
+  /* Step 1 */
   if (gtk_accessible_role_get_naming (self->accessible_role) == GTK_ACCESSIBLE_NAME_PROHIBITED)
     return g_strdup ("");
 
@@ -1402,33 +1399,37 @@ 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)
+{
+  g_return_val_if_fail (GTK_IS_AT_CONTEXT (self), NULL);
+
+  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`
@@ -1442,49 +1443,9 @@ 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