/*
* 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 .
*
* Authors: Matthias Clasen
*/
#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
{
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 (GtkATContext *context,
GtkAccessibleRole role,
GArray *context_elements,
char **hint)
{
gboolean label_set;
const char *role_name;
GEnumClass *states;
GEnumClass *properties;
GEnumClass *relations;
gboolean has_context;
*hint = NULL;
role_name = gtk_accessible_role_to_name (role, NULL);
if (!gtk_at_context_is_realized (context))
gtk_at_context_realize (context);
/* Check for abstract roles */
if (gtk_accessible_role_is_abstract (role))
{
*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 (gtk_accessible_role_get_naming (role))
{
case GTK_ACCESSIBLE_NAME_ALLOWED:
return SEVERITY_GOOD;
case GTK_ACCESSIBLE_NAME_REQUIRED:
if (label_set)
{
return SEVERITY_GOOD;
}
else
{
if (gtk_accessible_role_supports_name_from_author (role))
{
char *name = gtk_at_context_get_name (context);
if (strcmp (name, "") == 0)
{
g_free (name);
*hint = g_strdup_printf ("%s must have text content or label", role_name);
return SEVERITY_ERROR;
}
else
{
return SEVERITY_GOOD;
}
}
else
{
*hint = g_strdup_printf ("%s must have label", role_name);
return SEVERITY_ERROR;
}
}
break;
case GTK_ACCESSIBLE_NAME_PROHIBITED:
if (label_set)
{
*hint = g_strdup_printf ("%s can't have label", role_name);
return SEVERITY_ERROR;
}
else
{
return SEVERITY_GOOD;
}
break;
case GTK_ACCESSIBLE_NAME_RECOMMENDED:
if (label_set)
{
return SEVERITY_GOOD;
}
else
{
*hint = g_strdup_printf ("label recommended for %s", role_name);
return SEVERITY_RECOMMENDATION;
}
break;
case GTK_ACCESSIBLE_NAME_NOT_RECOMMENDED:
if (!label_set)
{
return SEVERITY_GOOD;
}
else
{
*hint = g_strdup_printf ("label not recommended for %s", role_name);
return SEVERITY_RECOMMENDATION;
}
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 FixSeverity
check_widget_accessibility_errors (GtkWidget *widget,
GArray *context_elements,
char **hint)
{
GtkAccessibleRole role;
GtkATContext *context;
FixSeverity ret;
role = gtk_accessible_get_accessible_role (GTK_ACCESSIBLE (widget));
context = gtk_accessible_get_at_context (GTK_ACCESSIBLE (widget));
ret = check_accessibility_errors (context, role, context_elements, hint);
g_object_unref (context);
return ret;
}
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_widget_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);
}