inspector: Add an a11y overlay
authorMatthias Clasen <mclasen@redhat.com>
Fri, 9 Jun 2023 20:51:10 +0000 (16:51 -0400)
committerMatthias Clasen <mclasen@redhat.com>
Mon, 12 Jun 2023 11:31:49 +0000 (07:31 -0400)
Add an overlay that shows a11y issues.

For now, this checks for:
 - abstract roles being used
 - elements without labels
 - required attributes
 - required context

gtk/inspector/a11yoverlay.c [new file with mode: 0644]
gtk/inspector/a11yoverlay.h [new file with mode: 0644]
gtk/inspector/meson.build
gtk/inspector/visual.c
gtk/inspector/visual.ui

diff --git a/gtk/inspector/a11yoverlay.c b/gtk/inspector/a11yoverlay.c
new file mode 100644 (file)
index 0000000..b78fac2
--- /dev/null
@@ -0,0 +1,574 @@
+/*
+ * Copyright © 2023 Red Hat, Inc.
+ *
+ * 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/>.
+ *
+ * Authors: Matthias Clasen <mclasen@redhat.com>
+ */
+
+#include "config.h"
+
+#include "a11yoverlay.h"
+
+#include "gtkwidget.h"
+#include "gtkroot.h"
+#include "gtknative.h"
+#include "gtkwidgetprivate.h"
+#include "gtkatcontextprivate.h"
+#include "gtkaccessibleprivate.h"
+#include "gtktypebuiltins.h"
+
+struct _GtkA11yOverlay
+{
+  GtkInspectorOverlay parent_instance;
+
+  GdkRGBA recommend_color;
+  GdkRGBA error_color;
+
+  GArray *context;
+};
+
+struct _GtkA11yOverlayClass
+{
+  GtkInspectorOverlayClass parent_class;
+};
+
+G_DEFINE_TYPE (GtkA11yOverlay, gtk_a11y_overlay, GTK_TYPE_INSPECTOR_OVERLAY)
+
+typedef enum
+{
+  NAMING_DISCRETIONARY,
+  NAMING_REQUIRED,
+  NAMING_RECOMMENDED,
+  NAMING_CONDITIONAL,
+  NAMING_PROHIBITED
+} NamingNecessity;
+
+static NamingNecessity name_for_role[] = {
+  NAMING_DISCRETIONARY, // ALERT
+  NAMING_REQUIRED, // ALERT_DIALOG
+  NAMING_DISCRETIONARY, // BANNER
+  NAMING_CONDITIONAL, // BUTTON
+  NAMING_PROHIBITED, // CAPTION
+  NAMING_CONDITIONAL, // CELL
+  NAMING_CONDITIONAL, // CHECKBOX
+  NAMING_CONDITIONAL, // COLUMN_HEADER
+  NAMING_REQUIRED, // COMBO_BOX
+  NAMING_DISCRETIONARY, // COMMAND
+  NAMING_DISCRETIONARY, // COMPOSITE
+  NAMING_REQUIRED, // DIALOG
+  NAMING_DISCRETIONARY, // DOCUMENT
+  NAMING_RECOMMENDED, // FEED
+  NAMING_RECOMMENDED, // FORM
+  NAMING_PROHIBITED, // GENERIC
+  NAMING_REQUIRED, // GRID
+  NAMING_CONDITIONAL, // GRID_CELL
+  NAMING_DISCRETIONARY, // GROUP
+  NAMING_CONDITIONAL, // HEADING
+  NAMING_REQUIRED, // IMG
+  NAMING_DISCRETIONARY, // INPUT
+  NAMING_DISCRETIONARY, // LABEL
+  NAMING_DISCRETIONARY, // LANDMARK
+  NAMING_DISCRETIONARY, // LEGEND
+  NAMING_CONDITIONAL, // LINK
+  NAMING_DISCRETIONARY, // LIST
+  NAMING_REQUIRED, // LIST_BOX
+  NAMING_PROHIBITED, // LIST_ITEM
+  NAMING_DISCRETIONARY, // LOG
+  NAMING_DISCRETIONARY, // MAIN
+  NAMING_DISCRETIONARY, // MARQUEE
+  NAMING_RECOMMENDED, // MATH
+  NAMING_REQUIRED, // METER
+  NAMING_RECOMMENDED, // MENU
+  NAMING_RECOMMENDED, // MENU_BAR
+  NAMING_CONDITIONAL, // MENU_ITEM
+  NAMING_CONDITIONAL, // MENU_ITEM_CHECKBOX
+  NAMING_CONDITIONAL, // MENU_ITEM_RADIO
+  NAMING_RECOMMENDED, // NAVIGATION
+  NAMING_PROHIBITED, // NONE
+  NAMING_DISCRETIONARY, // NOTE
+  NAMING_CONDITIONAL, // OPTION
+  NAMING_PROHIBITED, // PRESENTATION
+  NAMING_REQUIRED, // PROGRESS_BAR
+  NAMING_CONDITIONAL, // RADIO
+  NAMING_REQUIRED, // RADIO_GROUP
+  NAMING_DISCRETIONARY, // RANGE
+  NAMING_REQUIRED, // REGION
+  NAMING_CONDITIONAL, // ROW
+  NAMING_PROHIBITED, // ROW_GROUP
+  NAMING_CONDITIONAL, // ROW_HEADER
+  NAMING_DISCRETIONARY, // SCROLLBAR
+  NAMING_RECOMMENDED, // SEARCH
+  NAMING_REQUIRED, // SEARCH_BOX
+  NAMING_DISCRETIONARY, // SECTION
+  NAMING_DISCRETIONARY, // SECTION_HEAD
+  NAMING_DISCRETIONARY, // SELECT
+  NAMING_DISCRETIONARY, // SEPARATOR
+  NAMING_REQUIRED, // SLIDER
+  NAMING_REQUIRED, // SPIN_BUTTON
+  NAMING_DISCRETIONARY, // STATUS
+  NAMING_DISCRETIONARY, // STRUCTURE
+  NAMING_CONDITIONAL, // SWITCH
+  NAMING_CONDITIONAL, // TAB
+  NAMING_REQUIRED, // TABLE
+  NAMING_RECOMMENDED, // TAB_LIST
+  NAMING_REQUIRED, // TAB_PANEL
+  NAMING_REQUIRED, // TEXT_BOX
+  NAMING_PROHIBITED, // TIME
+  NAMING_DISCRETIONARY, // TIMER
+  NAMING_RECOMMENDED, // TOOLBAR
+  NAMING_CONDITIONAL, // TOOLTIP
+  NAMING_REQUIRED, // TREE
+  NAMING_REQUIRED, // TREE_GRID
+  NAMING_CONDITIONAL, // TREE_ITEM
+  NAMING_DISCRETIONARY, // WIDGET
+  NAMING_DISCRETIONARY, // WINDOW
+  NAMING_CONDITIONAL, // TOGGLE_BUTTON
+};
+
+static GtkAccessibleRole abstract_roles[] = {
+  GTK_ACCESSIBLE_ROLE_COMMAND,
+  GTK_ACCESSIBLE_ROLE_COMPOSITE,
+  GTK_ACCESSIBLE_ROLE_INPUT,
+  GTK_ACCESSIBLE_ROLE_LANDMARK,
+  GTK_ACCESSIBLE_ROLE_RANGE,
+  GTK_ACCESSIBLE_ROLE_SECTION,
+  GTK_ACCESSIBLE_ROLE_SECTION_HEAD,
+  GTK_ACCESSIBLE_ROLE_SELECT,
+  GTK_ACCESSIBLE_ROLE_STRUCTURE,
+#if 0
+  /* FIXME: ARIA considers these abstract.
+   * But we are using them for widgets
+   */
+  GTK_ACCESSIBLE_ROLE_WIDGET,
+  GTK_ACCESSIBLE_ROLE_WINDOW
+#endif
+};
+
+typedef enum
+{
+  SEVERITY_GOOD,
+  SEVERITY_RECOMMENDATION,
+  SEVERITY_ERROR
+} FixSeverity;
+
+typedef enum
+{
+  ATTRIBUTE_STATE,
+  ATTRIBUTE_PROPERTY,
+  ATTRIBUTE_RELATION
+} AttributeType;
+
+static struct {
+  GtkAccessibleRole role;
+  AttributeType type;
+  int id;
+} required_attributes[] = {
+  { GTK_ACCESSIBLE_ROLE_CHECKBOX, ATTRIBUTE_STATE, GTK_ACCESSIBLE_STATE_CHECKED },
+  { GTK_ACCESSIBLE_ROLE_COMBO_BOX, ATTRIBUTE_STATE, GTK_ACCESSIBLE_STATE_EXPANDED },
+  { GTK_ACCESSIBLE_ROLE_COMBO_BOX, ATTRIBUTE_RELATION, GTK_ACCESSIBLE_RELATION_CONTROLS },
+  { GTK_ACCESSIBLE_ROLE_HEADING, ATTRIBUTE_PROPERTY, GTK_ACCESSIBLE_PROPERTY_LEVEL },
+  { GTK_ACCESSIBLE_ROLE_SCROLLBAR, ATTRIBUTE_RELATION, GTK_ACCESSIBLE_RELATION_CONTROLS },
+  { GTK_ACCESSIBLE_ROLE_SCROLLBAR, ATTRIBUTE_PROPERTY, GTK_ACCESSIBLE_PROPERTY_VALUE_NOW },
+  { GTK_ACCESSIBLE_ROLE_SWITCH, ATTRIBUTE_STATE, GTK_ACCESSIBLE_STATE_CHECKED },
+};
+
+static struct {
+  GtkAccessibleRole role;
+  GtkAccessibleRole context;
+} required_context[] = {
+  { GTK_ACCESSIBLE_ROLE_CAPTION, GTK_ACCESSIBLE_ROLE_GRID },
+  { GTK_ACCESSIBLE_ROLE_CAPTION, GTK_ACCESSIBLE_ROLE_TABLE },
+  { GTK_ACCESSIBLE_ROLE_CAPTION, GTK_ACCESSIBLE_ROLE_TREE_GRID },
+  { GTK_ACCESSIBLE_ROLE_CELL, GTK_ACCESSIBLE_ROLE_ROW },
+  { GTK_ACCESSIBLE_ROLE_COLUMN_HEADER, GTK_ACCESSIBLE_ROLE_ROW },
+  { GTK_ACCESSIBLE_ROLE_GRID_CELL, GTK_ACCESSIBLE_ROLE_ROW },
+  { GTK_ACCESSIBLE_ROLE_LIST_ITEM, GTK_ACCESSIBLE_ROLE_LIST },
+  { GTK_ACCESSIBLE_ROLE_MENU_ITEM, GTK_ACCESSIBLE_ROLE_GROUP },
+  { GTK_ACCESSIBLE_ROLE_MENU_ITEM, GTK_ACCESSIBLE_ROLE_MENU },
+  { GTK_ACCESSIBLE_ROLE_MENU_ITEM, GTK_ACCESSIBLE_ROLE_MENU_BAR },
+  { GTK_ACCESSIBLE_ROLE_MENU_ITEM_CHECKBOX, GTK_ACCESSIBLE_ROLE_GROUP },
+  { GTK_ACCESSIBLE_ROLE_MENU_ITEM_CHECKBOX, GTK_ACCESSIBLE_ROLE_MENU },
+  { GTK_ACCESSIBLE_ROLE_MENU_ITEM_CHECKBOX, GTK_ACCESSIBLE_ROLE_MENU_BAR },
+  { GTK_ACCESSIBLE_ROLE_MENU_ITEM_RADIO, GTK_ACCESSIBLE_ROLE_GROUP },
+  { GTK_ACCESSIBLE_ROLE_MENU_ITEM_RADIO, GTK_ACCESSIBLE_ROLE_MENU },
+  { GTK_ACCESSIBLE_ROLE_MENU_ITEM_RADIO, GTK_ACCESSIBLE_ROLE_MENU_BAR },
+  { GTK_ACCESSIBLE_ROLE_OPTION, GTK_ACCESSIBLE_ROLE_GROUP },
+  { GTK_ACCESSIBLE_ROLE_OPTION, GTK_ACCESSIBLE_ROLE_LIST_BOX },
+  { GTK_ACCESSIBLE_ROLE_ROW, GTK_ACCESSIBLE_ROLE_GRID },
+  { GTK_ACCESSIBLE_ROLE_ROW, GTK_ACCESSIBLE_ROLE_ROW_GROUP },
+  { GTK_ACCESSIBLE_ROLE_ROW, GTK_ACCESSIBLE_ROLE_TABLE },
+  { GTK_ACCESSIBLE_ROLE_ROW, GTK_ACCESSIBLE_ROLE_TREE_GRID },
+  { GTK_ACCESSIBLE_ROLE_ROW_GROUP, GTK_ACCESSIBLE_ROLE_GRID },
+  { GTK_ACCESSIBLE_ROLE_ROW_GROUP, GTK_ACCESSIBLE_ROLE_TABLE },
+  { GTK_ACCESSIBLE_ROLE_ROW_GROUP, GTK_ACCESSIBLE_ROLE_TREE_GRID },
+  { GTK_ACCESSIBLE_ROLE_ROW_HEADER, GTK_ACCESSIBLE_ROLE_ROW },
+  { GTK_ACCESSIBLE_ROLE_TAB, GTK_ACCESSIBLE_ROLE_TAB_LIST },
+  { GTK_ACCESSIBLE_ROLE_TREE_ITEM, GTK_ACCESSIBLE_ROLE_GROUP },
+  { GTK_ACCESSIBLE_ROLE_TREE_ITEM, GTK_ACCESSIBLE_ROLE_TREE },
+};
+
+static FixSeverity
+check_accessibility_errors (GtkWidget  *widget,
+                            GArray     *context_elements,
+                            char      **hint)
+{
+  GtkAccessibleRole role;
+  GtkATContext *context;
+  gboolean label_set;
+  const char *role_name;
+  GEnumClass *states;
+  GEnumClass *properties;
+  GEnumClass *relations;
+  gboolean has_context;
+
+  *hint = NULL;
+
+  role = gtk_accessible_get_accessible_role (GTK_ACCESSIBLE (widget));
+  role_name = gtk_accessible_role_to_name (role, NULL);
+
+  context = gtk_accessible_get_at_context (GTK_ACCESSIBLE (widget));
+  if (!gtk_at_context_is_realized (context))
+    gtk_at_context_realize (context);
+
+  /* Check for abstract roles */
+  for (unsigned int i = 0; i < G_N_ELEMENTS (abstract_roles); i++)
+    {
+      if (role == abstract_roles[i])
+        {
+          *hint = g_strdup_printf ("%s is an abstract role", role_name);
+
+          return SEVERITY_ERROR;
+        }
+    }
+
+  /* Check for name and description */
+  label_set = gtk_at_context_has_accessible_property (context, GTK_ACCESSIBLE_PROPERTY_LABEL) ||
+              gtk_at_context_has_accessible_relation (context, GTK_ACCESSIBLE_RELATION_LABELLED_BY);
+
+  switch (name_for_role[role])
+    {
+    case NAMING_DISCRETIONARY:
+      return SEVERITY_GOOD;
+
+    case NAMING_REQUIRED:
+      if (label_set)
+        {
+          return SEVERITY_GOOD;
+        }
+      else
+        {
+          *hint = g_strdup_printf ("%s must have label or labelled-by", role_name);
+
+          return SEVERITY_ERROR;
+        }
+      break;
+
+    case NAMING_PROHIBITED:
+      if (label_set)
+        {
+          *hint = g_strdup_printf ("%s can't have label or labelled-by", role_name);
+
+          return SEVERITY_ERROR;
+        }
+      else
+        {
+          return SEVERITY_GOOD;
+        }
+      break;
+
+    case NAMING_RECOMMENDED:
+      if (label_set)
+        {
+          return SEVERITY_GOOD;
+        }
+      else
+        {
+          *hint = g_strdup_printf ("label or labelled-by recommended for %s", role_name);
+
+          return SEVERITY_RECOMMENDATION;
+        }
+      break;
+
+    case NAMING_CONDITIONAL:
+      {
+        char *name = gtk_at_context_get_name (context);
+
+        if (strcmp (name, "") == 0)
+          {
+            g_free (name);
+            *hint = g_strdup_printf ("%s must have text content, label or labelled-by",
+                                     role_name);
+
+            return SEVERITY_ERROR;
+          }
+        else
+          {
+            return SEVERITY_GOOD;
+          }
+      }
+      break;
+
+    default:
+      g_assert_not_reached ();
+    }
+
+  /* Check for required attributes */
+  states = g_type_class_peek (GTK_TYPE_ACCESSIBLE_STATE);
+  properties = g_type_class_peek (GTK_TYPE_ACCESSIBLE_PROPERTY);
+  relations = g_type_class_peek (GTK_TYPE_ACCESSIBLE_RELATION);
+
+  for (unsigned int i = 0; i < G_N_ELEMENTS (required_attributes); i++)
+    {
+      if (role == required_attributes[i].role)
+        {
+          switch (required_attributes[i].type)
+            {
+            case ATTRIBUTE_STATE:
+              if (!gtk_at_context_has_accessible_state (context, required_attributes[i].id))
+                {
+                  *hint = g_strdup_printf ("%s must have state %s", role_name, g_enum_get_value (states, required_attributes[i].id)->value_nick);
+                  return SEVERITY_ERROR;
+                }
+              break;
+
+            case ATTRIBUTE_PROPERTY:
+              if (!gtk_at_context_has_accessible_property (context, required_attributes[i].id))
+                {
+                  *hint = g_strdup_printf ("%s must have property %s", role_name, g_enum_get_value (properties, required_attributes[i].id)->value_nick);
+                  return SEVERITY_ERROR;
+                }
+              break;
+
+            case ATTRIBUTE_RELATION:
+              if (!gtk_at_context_has_accessible_relation (context, required_attributes[i].id))
+                {
+                  *hint = g_strdup_printf ("%s must have relation %s", role_name, g_enum_get_value (relations, required_attributes[i].id)->value_nick);
+                  return SEVERITY_ERROR;
+                }
+              break;
+
+            default:
+              g_assert_not_reached ();
+            }
+        }
+    }
+
+  /* Check for required context */
+  has_context = TRUE;
+
+  for (unsigned int i = 0; i < G_N_ELEMENTS (required_context); i++)
+    {
+      if (required_context[i].role != role)
+        continue;
+
+      has_context = FALSE;
+
+      for (unsigned int j = 0; j < context_elements->len; j++)
+        {
+          GtkAccessibleRole elt = g_array_index (context_elements, GtkAccessibleRole, j);
+
+          if (required_context[i].context == elt)
+            {
+              has_context = TRUE;
+              break;
+            }
+        }
+
+      if (has_context)
+        break;
+    }
+
+  if (!has_context)
+    {
+      GString *s = g_string_new ("");
+
+      for (unsigned int i = 0; i < G_N_ELEMENTS (required_context); i++)
+        {
+          if (required_context[i].role != role)
+            continue;
+
+          if (s->len > 0)
+            g_string_append (s, ", ");
+
+          g_string_append (s, gtk_accessible_role_to_name (required_context[i].context, NULL));
+        }
+
+      *hint = g_strdup_printf ("%s requires context: %s", role_name, s->str);
+
+      g_string_free (s, TRUE);
+
+      return SEVERITY_ERROR;
+    }
+
+  return SEVERITY_GOOD;
+}
+
+static void
+recurse_child_widgets (GtkA11yOverlay *self,
+                       GtkWidget      *widget,
+                       GtkSnapshot    *snapshot)
+{
+  GtkWidget *child;
+  char *hint;
+  FixSeverity severity;
+  GtkAccessibleRole role;
+
+  if (!gtk_widget_get_mapped (widget))
+    return;
+
+  severity = check_accessibility_errors (widget, self->context, &hint);
+
+  if (severity != SEVERITY_GOOD)
+    {
+      int width, height;
+      GdkRGBA color;
+
+      width = gtk_widget_get_width (widget);
+      height = gtk_widget_get_height (widget);
+
+      if (severity == SEVERITY_ERROR)
+        color = self->error_color;
+      else
+        color = self->recommend_color;
+
+      gtk_snapshot_save (snapshot);
+      gtk_snapshot_push_debug (snapshot, "Widget a11y debugging");
+
+      gtk_snapshot_append_color (snapshot, &color,
+                                 &GRAPHENE_RECT_INIT (0, 0, width, height));
+
+      if (hint)
+        {
+          PangoLayout *layout;
+          PangoRectangle extents;
+          GdkRGBA black = { 0, 0, 0, 1 };
+          float widths[4] = { 1, 1, 1, 1 };
+          GdkRGBA colors[4] = {
+            { 0, 0, 0, 1 },
+            { 0, 0, 0, 1 },
+            { 0, 0, 0, 1 },
+            { 0, 0, 0, 1 },
+          };
+
+          gtk_snapshot_save (snapshot);
+
+          layout = gtk_widget_create_pango_layout (widget, hint);
+          pango_layout_set_width (layout, width * PANGO_SCALE);
+
+          pango_layout_get_pixel_extents (layout, NULL, &extents);
+
+          extents.x -= 5;
+          extents.y -= 5;
+          extents.width += 10;
+          extents.height += 10;
+
+          color.alpha = 0.8f;
+
+          gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (0.5 * (width - extents.width), 0.5 * (height - extents.height)));
+
+          gtk_snapshot_append_border (snapshot,
+                                       &GSK_ROUNDED_RECT_INIT (extents.x, extents.y,
+                                                               extents.width, extents.height),
+                                      widths, colors);
+          gtk_snapshot_append_color (snapshot, &color,
+                                     &GRAPHENE_RECT_INIT (extents.x, extents.y,
+                                                          extents.width, extents.height));
+
+          gtk_snapshot_append_layout (snapshot, layout, &black);
+          g_object_unref (layout);
+
+          gtk_snapshot_restore (snapshot);
+        }
+
+      gtk_snapshot_pop (snapshot);
+      gtk_snapshot_restore (snapshot);
+   }
+
+  g_free (hint);
+
+  /* Recurse into child widgets */
+
+  role = gtk_accessible_get_accessible_role (GTK_ACCESSIBLE (widget));
+  g_array_append_val (self->context, role);
+
+  for (child = gtk_widget_get_first_child (widget);
+       child != NULL;
+       child = gtk_widget_get_next_sibling (child))
+    {
+      gtk_snapshot_save (snapshot);
+      gtk_snapshot_transform (snapshot, child->priv->transform);
+
+      recurse_child_widgets (self, child, snapshot);
+
+      gtk_snapshot_restore (snapshot);
+    }
+
+  g_array_remove_index (self->context, self->context->len - 1);
+}
+
+static void
+gtk_a11y_overlay_snapshot (GtkInspectorOverlay *overlay,
+                           GtkSnapshot         *snapshot,
+                           GskRenderNode       *node,
+                           GtkWidget           *widget)
+{
+  GtkA11yOverlay *self = GTK_A11Y_OVERLAY (overlay);
+
+  g_assert (self->context->len == 0);
+
+  recurse_child_widgets (self, widget, snapshot);
+
+  g_assert (self->context->len == 0);
+}
+
+static void
+gtk_a11y_overlay_finalize (GObject *object)
+{
+  GtkA11yOverlay *self = GTK_A11Y_OVERLAY (object);
+
+  g_array_free (self->context, TRUE);
+
+  G_OBJECT_CLASS (gtk_a11y_overlay_parent_class)->finalize (object);
+}
+
+static void
+gtk_a11y_overlay_class_init (GtkA11yOverlayClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkInspectorOverlayClass *overlay_class = GTK_INSPECTOR_OVERLAY_CLASS (klass);
+
+  object_class->finalize = gtk_a11y_overlay_finalize;
+
+  overlay_class->snapshot = gtk_a11y_overlay_snapshot;
+}
+
+static void
+gtk_a11y_overlay_init (GtkA11yOverlay *self)
+{
+  self->recommend_color = (GdkRGBA) { 0.0, 0.5, 1.0, 0.2 };
+  self->error_color = (GdkRGBA) { 1.0, 0.0, 0.0, 0.2 };
+
+  self->context = g_array_new (FALSE, FALSE, sizeof (GtkAccessibleRole));
+}
+
+GtkInspectorOverlay *
+gtk_a11y_overlay_new (void)
+{
+  GtkA11yOverlay *self;
+
+  self = g_object_new (GTK_TYPE_A11Y_OVERLAY, NULL);
+
+  return GTK_INSPECTOR_OVERLAY (self);
+}
diff --git a/gtk/inspector/a11yoverlay.h b/gtk/inspector/a11yoverlay.h
new file mode 100644 (file)
index 0000000..d93cc83
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Copyright © 2023 Red Hat, Inc.
+ *
+ * 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/>.
+ *
+ * Authors: Matthias Clasen <mclasen@redhat.com>
+ */
+
+#pragma once
+
+#include "inspectoroverlay.h"
+
+G_BEGIN_DECLS
+
+#define GTK_TYPE_A11Y_OVERLAY             (gtk_a11y_overlay_get_type ())
+G_DECLARE_FINAL_TYPE (GtkA11yOverlay, gtk_a11y_overlay, GTK, A11Y_OVERLAY, GtkInspectorOverlay)
+
+GtkInspectorOverlay *   gtk_a11y_overlay_new               (void);
+
+G_END_DECLS
+
index 813028a26af4038f504d42606ee31061df606076..22e11bcef0d209f7ed123df573a1c461e2441d7f 100644 (file)
@@ -1,5 +1,6 @@
 inspector_sources = files(
   'a11y.c',
+  'a11yoverlay.c',
   'action-editor.c',
   'action-holder.c',
   'actions.c',
index f82122386520abccb1338f09b68d6b9fd901c413..92e4454c8acb47b4fd7a05c5169333f1af729aa4 100644 (file)
@@ -21,6 +21,7 @@
 #include "visual.h"
 
 #include "fpsoverlay.h"
+#include "a11yoverlay.h"
 #include "updatesoverlay.h"
 #include "layoutoverlay.h"
 #include "focusoverlay.h"
@@ -94,6 +95,7 @@ struct _GtkInspectorVisual
   GtkWidget *baselines_switch;
   GtkWidget *layout_switch;
   GtkWidget *focus_switch;
+  GtkWidget *a11y_switch;
 
   GtkWidget *misc_box;
   GtkWidget *touchscreen_switch;
@@ -103,6 +105,7 @@ struct _GtkInspectorVisual
   GtkInspectorOverlay *layout_overlay;
   GtkInspectorOverlay *focus_overlay;
   GtkInspectorOverlay *baseline_overlay;
+  GtkInspectorOverlay *a11y_overlay;
 
   GdkDisplay *display;
 };
@@ -282,6 +285,40 @@ fps_activate (GtkSwitch          *sw,
   redraw_everything ();
 }
 
+static void
+a11y_activate (GtkSwitch          *sw,
+               GParamSpec         *pspec,
+               GtkInspectorVisual *vis)
+{
+  GtkInspectorWindow *iw;
+  gboolean a11y;
+
+  a11y = gtk_switch_get_active (sw);
+  iw = GTK_INSPECTOR_WINDOW (gtk_widget_get_root (GTK_WIDGET (vis)));
+  if (iw == NULL)
+    return;
+
+  if (a11y)
+    {
+      if (vis->a11y_overlay == NULL)
+        {
+          vis->a11y_overlay = gtk_a11y_overlay_new ();
+          gtk_inspector_window_add_overlay (iw, vis->a11y_overlay);
+          g_object_unref (vis->a11y_overlay);
+        }
+    }
+  else
+    {
+      if (vis->a11y_overlay != NULL)
+        {
+          gtk_inspector_window_remove_overlay (iw, vis->a11y_overlay);
+          vis->a11y_overlay = NULL;
+        }
+    }
+
+  redraw_everything ();
+}
+
 static void
 updates_activate (GtkSwitch          *sw,
                   GParamSpec         *pspec,
@@ -1038,6 +1075,11 @@ row_activated (GtkListBox         *box,
       GtkSwitch *sw = GTK_SWITCH (vis->touchscreen_switch);
       gtk_switch_set_active (sw, !gtk_switch_get_active (sw));
     }
+  else if (gtk_widget_is_ancestor (vis->a11y_switch, GTK_WIDGET (row)))
+    {
+      GtkSwitch *sw = GTK_SWITCH (vis->a11y_switch);
+      gtk_switch_set_active (sw, !gtk_switch_get_active (sw));
+    }
 }
 
 static void
@@ -1102,6 +1144,11 @@ gtk_inspector_visual_unroot (GtkWidget *widget)
       gtk_inspector_window_remove_overlay (iw, vis->focus_overlay);
       vis->focus_overlay = NULL;
     }
+  if (vis->a11y_overlay)
+    {
+      gtk_inspector_window_remove_overlay (iw, vis->a11y_overlay);
+      vis->a11y_overlay = NULL;
+    }
 
   GTK_WIDGET_CLASS (gtk_inspector_visual_parent_class)->unroot (widget);
 }
@@ -1155,6 +1202,7 @@ gtk_inspector_visual_class_init (GtkInspectorVisualClass *klass)
   gtk_widget_class_bind_template_child (widget_class, GtkInspectorVisual, baselines_switch);
   gtk_widget_class_bind_template_child (widget_class, GtkInspectorVisual, layout_switch);
   gtk_widget_class_bind_template_child (widget_class, GtkInspectorVisual, focus_switch);
+  gtk_widget_class_bind_template_child (widget_class, GtkInspectorVisual, a11y_switch);
 
   gtk_widget_class_bind_template_callback (widget_class, fps_activate);
   gtk_widget_class_bind_template_callback (widget_class, updates_activate);
@@ -1163,6 +1211,7 @@ gtk_inspector_visual_class_init (GtkInspectorVisualClass *klass)
   gtk_widget_class_bind_template_callback (widget_class, baselines_activate);
   gtk_widget_class_bind_template_callback (widget_class, layout_activate);
   gtk_widget_class_bind_template_callback (widget_class, focus_activate);
+  gtk_widget_class_bind_template_callback (widget_class, a11y_activate);
   gtk_widget_class_bind_template_callback (widget_class, inspect_inspector);
 
   gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
index b10dd71173c2ccff4ca5465b4c75ae884aeebae3..2030f635d7f92514537486304dc63f720e610a4f 100644 (file)
                         </child>
                       </object>
                     </child>
+                    <child>
+                      <object class="GtkListBoxRow">
+                        <child>
+                          <object class="GtkBox">
+                            <property name="spacing">40</property>
+                            <child>
+                              <object class="GtkLabel" id="a11y_label">
+                                <property name="label" translatable="yes">Show Accessibility warnings</property>
+                                <property name="halign">start</property>
+                                <property name="valign">baseline</property>
+                                <property name="xalign">0.0</property>
+                              </object>
+                            </child>
+                            <child>
+                              <object class="GtkSwitch" id="a11y_switch">
+                                <property name="halign">end</property>
+                                <property name="valign">center</property>
+                                <property name="hexpand">1</property>
+                                <signal name="notify::active" handler="a11y_activate"/>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
                   </object>
                 </child>
                 <child>