904 lines
37 KiB
C
904 lines
37 KiB
C
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
#include "adapter_native.h"
|
|
|
|
static gchar *cthulhu_capture_text(AtspiAccessible *source, gchar **beforeCaret, gint *startOffset);
|
|
static void cthulhu_capture_document_metrics(AtspiAccessible *source, cthulhu_snapshot *snapshot);
|
|
static gchar *cthulhu_table_cell_label(AtspiAccessible *source, guint depth, guint *remaining);
|
|
|
|
// Collect only the selected row. Walking an entire table on every arrow key
|
|
// would block input in large application tables.
|
|
gchar *cthulhu_table_context(AtspiAccessible *source, gint *rows, gint *columns,
|
|
gchar **tableName, gchar **tablePath,
|
|
gint *rowIndex, gint *columnIndex, gboolean *guiTable) {
|
|
AtspiAccessible *tableAccessible = NULL;
|
|
gint row = -1;
|
|
gint column = -1;
|
|
if (atspi_accessible_get_role(source, NULL) == ATSPI_ROLE_TABLE) {
|
|
tableAccessible = g_object_ref(source);
|
|
} else if (atspi_accessible_get_role(source, NULL) == ATSPI_ROLE_TABLE_CELL) {
|
|
AtspiTableCell *cell = atspi_accessible_get_table_cell(source);
|
|
if (cell != NULL) {
|
|
tableAccessible = atspi_table_cell_get_table(cell, NULL);
|
|
atspi_table_cell_get_position(cell, &row, &column, NULL);
|
|
g_object_unref(cell);
|
|
}
|
|
if (tableAccessible == NULL) {
|
|
AtspiAccessible *parent = atspi_accessible_get_parent(source, NULL);
|
|
if (parent != NULL && atspi_accessible_get_role(parent, NULL) == ATSPI_ROLE_TABLE) {
|
|
tableAccessible = parent;
|
|
gint index = atspi_accessible_get_index_in_parent(source, NULL);
|
|
AtspiTable *parentTable = atspi_accessible_get_table_iface(parent);
|
|
if (parentTable != NULL) {
|
|
row = atspi_table_get_row_at_index(parentTable, index, NULL);
|
|
column = atspi_table_get_column_at_index(parentTable, index, NULL);
|
|
g_object_unref(parentTable);
|
|
}
|
|
} else if (parent != NULL) {
|
|
g_object_unref(parent);
|
|
}
|
|
}
|
|
}
|
|
if (tableAccessible == NULL) return NULL;
|
|
*guiTable = TRUE;
|
|
AtspiAccessible *ancestor = g_object_ref(tableAccessible);
|
|
for (gint depth = 0; depth < 16 && ancestor != NULL; depth++) {
|
|
AtspiRole role = atspi_accessible_get_role(ancestor, NULL);
|
|
if (role == ATSPI_ROLE_DOCUMENT_WEB || role == ATSPI_ROLE_DOCUMENT_FRAME ||
|
|
role == ATSPI_ROLE_DOCUMENT_TEXT || role == ATSPI_ROLE_DOCUMENT_SPREADSHEET) {
|
|
*guiTable = FALSE;
|
|
g_object_unref(ancestor);
|
|
ancestor = NULL;
|
|
break;
|
|
}
|
|
AtspiAccessible *parent = atspi_accessible_get_parent(ancestor, NULL);
|
|
g_object_unref(ancestor);
|
|
ancestor = parent;
|
|
}
|
|
if (ancestor != NULL) g_object_unref(ancestor);
|
|
AtspiTable *table = atspi_accessible_get_table_iface(tableAccessible);
|
|
if (table == NULL) {
|
|
g_object_unref(tableAccessible);
|
|
return NULL;
|
|
}
|
|
*rows = atspi_table_get_n_rows(table, NULL);
|
|
*columns = atspi_table_get_n_columns(table, NULL);
|
|
*tableName = atspi_accessible_get_name(tableAccessible, NULL);
|
|
*tablePath = g_strdup(cthulhu_accessible_path(tableAccessible));
|
|
*rowIndex = row;
|
|
*columnIndex = column;
|
|
GString *result = g_string_new(NULL);
|
|
if (row >= 0 && row < *rows && *columns > 0 && *columns <= 32) {
|
|
guint remaining = 32;
|
|
for (gint column = 0; column < *columns; column++) {
|
|
AtspiAccessible *item = atspi_table_get_accessible_at(table, row, column, NULL);
|
|
if (item == NULL) continue;
|
|
gchar *name = atspi_accessible_get_name(item, NULL);
|
|
if ((name == NULL || *name == '\0') && remaining != 0 &&
|
|
atspi_accessible_get_role(item, NULL) == ATSPI_ROLE_TABLE_CELL) {
|
|
g_free(name);
|
|
name = cthulhu_table_cell_label(item, 0, &remaining);
|
|
}
|
|
if (name != NULL && *name != '\0' && result->len + strlen(name) < 4096) {
|
|
if (result->len > 0) g_string_append(result, ", ");
|
|
g_string_append(result, name);
|
|
}
|
|
g_free(name);
|
|
g_object_unref(item);
|
|
}
|
|
}
|
|
g_object_unref(table);
|
|
g_object_unref(tableAccessible);
|
|
return g_string_free(result, FALSE);
|
|
}
|
|
|
|
static gboolean cthulhu_capture_sound_rect(AtspiAccessible *source, gint *x, gint *y,
|
|
gint *width, gint *height) {
|
|
AtspiStateSet *states = atspi_accessible_get_state_set(source);
|
|
if (states == NULL) return FALSE;
|
|
gboolean available = atspi_state_set_contains(states, ATSPI_STATE_VISIBLE) &&
|
|
atspi_state_set_contains(states, ATSPI_STATE_SHOWING);
|
|
g_object_unref(states);
|
|
if (!available) return FALSE;
|
|
AtspiComponent *component = atspi_accessible_get_component_iface(source);
|
|
if (component == NULL) return FALSE;
|
|
GError *error = NULL;
|
|
AtspiRect *rect = atspi_component_get_extents(component, ATSPI_COORD_TYPE_SCREEN, &error);
|
|
g_object_unref(component);
|
|
if (error != NULL) {
|
|
g_clear_error(&error);
|
|
if (rect != NULL) g_free(rect);
|
|
return FALSE;
|
|
}
|
|
if (rect == NULL || rect->width <= 0 || rect->height <= 0 ||
|
|
rect->x == G_MININT || rect->y == G_MININT) {
|
|
if (rect != NULL) g_free(rect);
|
|
return FALSE;
|
|
}
|
|
*x = rect->x;
|
|
*y = rect->y;
|
|
*width = rect->width;
|
|
*height = rect->height;
|
|
g_free(rect);
|
|
return TRUE;
|
|
}
|
|
|
|
void cthulhu_capture_sound_geometry(AtspiAccessible *source, cthulhu_snapshot *snapshot) {
|
|
snapshot->objectGeometryKnown = cthulhu_capture_sound_rect(
|
|
source, &snapshot->objectX, &snapshot->objectY,
|
|
&snapshot->objectWidth, &snapshot->objectHeight);
|
|
if (!snapshot->objectGeometryKnown) return;
|
|
|
|
AtspiAccessible *current = g_object_ref(source);
|
|
for (gint depth = 0; current != NULL && depth < 256; depth++) {
|
|
GError *error = NULL;
|
|
AtspiRole role = atspi_accessible_get_role(current, &error);
|
|
if (error != NULL) {
|
|
g_clear_error(&error);
|
|
g_object_unref(current);
|
|
return;
|
|
}
|
|
if (role == ATSPI_ROLE_DIALOG || role == ATSPI_ROLE_FRAME || role == ATSPI_ROLE_WINDOW) {
|
|
snapshot->referenceGeometryKnown = cthulhu_capture_sound_rect(
|
|
current, &snapshot->referenceX, &snapshot->referenceY,
|
|
&snapshot->referenceWidth, &snapshot->referenceHeight);
|
|
g_object_unref(current);
|
|
return;
|
|
}
|
|
AtspiAccessible *parent = atspi_accessible_get_parent(current, &error);
|
|
g_object_unref(current);
|
|
if (error != NULL) {
|
|
g_clear_error(&error);
|
|
if (parent != NULL) g_object_unref(parent);
|
|
return;
|
|
}
|
|
current = parent;
|
|
}
|
|
if (current != NULL) g_object_unref(current);
|
|
}
|
|
|
|
static void cthulhu_capture_value(AtspiAccessible *source, cthulhu_snapshot *snapshot) {
|
|
GError *error = NULL;
|
|
AtspiRole role = atspi_accessible_get_role(source, &error);
|
|
if (error != NULL) {
|
|
g_error_free(error);
|
|
return;
|
|
}
|
|
switch (role) {
|
|
case ATSPI_ROLE_COMBO_BOX: {
|
|
// Many applications expose the selected item only through Selection.
|
|
// Prefer an already supplied description so well-behaved controls do
|
|
// not pay another synchronous AT-SPI round trip on every focus event.
|
|
if (snapshot->description != NULL && *snapshot->description != '\0') {
|
|
return;
|
|
}
|
|
AtspiSelection *selection = atspi_accessible_get_selection_iface(source);
|
|
if (selection == NULL) return;
|
|
AtspiAccessible *selected = atspi_selection_get_selected_child(selection, 0, &error);
|
|
g_object_unref(selection);
|
|
if (error != NULL) {
|
|
g_clear_error(&error);
|
|
if (selected != NULL) g_object_unref(selected);
|
|
return;
|
|
}
|
|
if (selected == NULL) return;
|
|
snapshot->valueText = atspi_accessible_get_name(selected, &error);
|
|
g_object_unref(selected);
|
|
if (error != NULL) {
|
|
g_clear_error(&error);
|
|
snapshot->valueText = NULL;
|
|
}
|
|
return;
|
|
}
|
|
case ATSPI_ROLE_DIAL:
|
|
case ATSPI_ROLE_PROGRESS_BAR:
|
|
case ATSPI_ROLE_SCROLL_BAR:
|
|
case ATSPI_ROLE_SLIDER:
|
|
break;
|
|
case ATSPI_ROLE_SPIN_BUTTON:
|
|
if (snapshot->text != NULL && *snapshot->text != '\0') {
|
|
return;
|
|
}
|
|
break;
|
|
case ATSPI_ROLE_SPLIT_PANE:
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
AtspiValue *value = atspi_accessible_get_value_iface(source);
|
|
if (value == NULL) {
|
|
return;
|
|
}
|
|
|
|
error = NULL;
|
|
snapshot->valueText = atspi_value_get_text(value, &error);
|
|
if (error != NULL) {
|
|
g_error_free(error);
|
|
error = NULL;
|
|
}
|
|
|
|
snapshot->currentValue = atspi_value_get_current_value(value, &error);
|
|
if (error != NULL) {
|
|
g_error_free(error);
|
|
error = NULL;
|
|
g_object_unref(value);
|
|
return;
|
|
}
|
|
snapshot->minimumValue = atspi_value_get_minimum_value(value, &error);
|
|
if (error != NULL) {
|
|
g_error_free(error);
|
|
error = NULL;
|
|
g_object_unref(value);
|
|
return;
|
|
}
|
|
snapshot->maximumValue = atspi_value_get_maximum_value(value, &error);
|
|
if (error == NULL) {
|
|
snapshot->valueKnown = TRUE;
|
|
} else {
|
|
g_clear_error(&error);
|
|
}
|
|
snapshot->minimumIncrement = atspi_value_get_minimum_increment(value, &error);
|
|
if (error != NULL) {
|
|
g_clear_error(&error);
|
|
snapshot->minimumIncrement = (snapshot->maximumValue - snapshot->minimumValue) / 100.0;
|
|
}
|
|
g_object_unref(value);
|
|
}
|
|
|
|
// GTK tree rows can be unnamed while their renderer cells hold the visible
|
|
// label (including file names in Thunar's details view). Bound the lookup so
|
|
// a large or broken table cannot stall the event callback.
|
|
static gchar *cthulhu_table_cell_label(AtspiAccessible *source, guint depth, guint *remaining) {
|
|
if (source == NULL || *remaining == 0) return NULL;
|
|
(*remaining)--;
|
|
if (depth != 0) {
|
|
GError *error = NULL;
|
|
gchar *name = atspi_accessible_get_name(source, &error);
|
|
if (error != NULL) g_clear_error(&error);
|
|
if (name != NULL && *g_strstrip(name) != '\0') return name;
|
|
g_free(name);
|
|
}
|
|
if (depth >= 2) return NULL;
|
|
GError *error = NULL;
|
|
gint count = atspi_accessible_get_child_count(source, &error);
|
|
if (error != NULL) {
|
|
g_clear_error(&error);
|
|
return NULL;
|
|
}
|
|
for (gint index = 0; index < count && index < 12 && *remaining != 0; index++) {
|
|
AtspiAccessible *child = atspi_accessible_get_child_at_index(source, index, &error);
|
|
if (error != NULL) {
|
|
g_clear_error(&error);
|
|
continue;
|
|
}
|
|
if (child == NULL) continue;
|
|
gchar *name = cthulhu_table_cell_label(child, depth + 1, remaining);
|
|
g_object_unref(child);
|
|
if (name != NULL) return name;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// Document cells can contain several text/link children. Collect their labels
|
|
// within a fixed budget, rather than the GUI row helper's first-label shortcut.
|
|
static gchar *cthulhu_document_cell_label(AtspiAccessible *source, guint depth, guint *remaining) {
|
|
if (!source || !*remaining || depth > 8) return NULL;
|
|
(*remaining)--;
|
|
gchar *name = atspi_accessible_get_name(source, NULL);
|
|
if (name && *g_strstrip(name)) return name;
|
|
g_free(name);
|
|
GString *text = g_string_new(NULL);
|
|
gint count = atspi_accessible_get_child_count(source, NULL);
|
|
for (gint i = 0; i < count && *remaining; i++) {
|
|
AtspiAccessible *child = atspi_accessible_get_child_at_index(source, i, NULL);
|
|
if (!child) continue;
|
|
gchar *label = cthulhu_document_cell_label(child, depth + 1, remaining);
|
|
g_object_unref(child);
|
|
if (label && *label) {
|
|
if (text->len) g_string_append_c(text, ' ');
|
|
g_string_append(text, label);
|
|
}
|
|
g_free(label);
|
|
}
|
|
return g_string_free(text, FALSE);
|
|
}
|
|
|
|
// Header arrays own their accessible references. Copy labels before release.
|
|
static gchar *cthulhu_table_headers(GPtrArray *headers) {
|
|
if (!headers) return NULL;
|
|
GString *text = g_string_new(NULL);
|
|
guint remaining = 32;
|
|
for (guint i = 0; i < headers->len && remaining; i++) {
|
|
gchar *label = cthulhu_document_cell_label(g_ptr_array_index(headers, i), 0, &remaining);
|
|
if (label && *label) {
|
|
if (text->len) g_string_append(text, ", ");
|
|
g_string_append(text, label);
|
|
}
|
|
g_free(label);
|
|
}
|
|
g_ptr_array_unref(headers);
|
|
return g_string_free(text, FALSE);
|
|
}
|
|
|
|
cthulhu_snapshot cthulhu_capture_snapshot(AtspiAccessible *source, gboolean captureText,
|
|
gboolean captureCaretCharacter,
|
|
gboolean captureDocumentMetadata,
|
|
gboolean captureSoundGeometry,
|
|
gboolean captureApplicationName) {
|
|
cthulhu_snapshot snapshot = {0};
|
|
if (source == NULL) {
|
|
return snapshot;
|
|
}
|
|
snapshot.path = ATSPI_OBJECT(source)->path;
|
|
if (ATSPI_OBJECT(source)->app != NULL) {
|
|
snapshot.busName = ATSPI_OBJECT(source)->app->bus_name;
|
|
if (captureApplicationName) {
|
|
GError *applicationError = NULL;
|
|
AtspiAccessible *application = atspi_accessible_get_application(source, &applicationError);
|
|
if (applicationError != NULL) {
|
|
g_clear_error(&applicationError);
|
|
}
|
|
if (application != NULL) {
|
|
snapshot.applicationName = atspi_accessible_get_name(application, &applicationError);
|
|
if (applicationError != NULL) {
|
|
g_clear_error(&applicationError);
|
|
}
|
|
g_object_unref(application);
|
|
}
|
|
}
|
|
}
|
|
AtspiStateSet *states = atspi_accessible_get_state_set(source);
|
|
if (states != NULL) {
|
|
snapshot.statesKnown = TRUE;
|
|
snapshot.visible = atspi_state_set_contains(states, ATSPI_STATE_VISIBLE);
|
|
snapshot.showing = atspi_state_set_contains(states, ATSPI_STATE_SHOWING);
|
|
snapshot.active = atspi_state_set_contains(states, ATSPI_STATE_ACTIVE);
|
|
snapshot.autocomplete = atspi_state_set_contains(states, ATSPI_STATE_SUPPORTS_AUTOCOMPLETION) && atspi_state_set_contains(states, ATSPI_STATE_SINGLE_LINE);
|
|
snapshot.focusable = atspi_state_set_contains(states, ATSPI_STATE_FOCUSABLE);
|
|
snapshot.focused = atspi_state_set_contains(states, ATSPI_STATE_FOCUSED);
|
|
snapshot.hasPopup = atspi_state_set_contains(states, ATSPI_STATE_HAS_POPUP);
|
|
snapshot.sensitiveKnown = TRUE;
|
|
snapshot.checked = atspi_state_set_contains(states, ATSPI_STATE_CHECKED) || atspi_state_set_contains(states, ATSPI_STATE_PRESSED);
|
|
snapshot.defunct = atspi_state_set_contains(states, ATSPI_STATE_DEFUNCT);
|
|
snapshot.editable = atspi_state_set_contains(states, ATSPI_STATE_EDITABLE);
|
|
snapshot.expandable = atspi_state_set_contains(states, ATSPI_STATE_EXPANDABLE) || atspi_state_set_contains(states, ATSPI_STATE_EXPANDED);
|
|
snapshot.expanded = atspi_state_set_contains(states, ATSPI_STATE_EXPANDED);
|
|
snapshot.indeterminate = atspi_state_set_contains(states, ATSPI_STATE_INDETERMINATE);
|
|
snapshot.invalid = atspi_state_set_contains(states, ATSPI_STATE_INVALID_ENTRY) || atspi_state_set_contains(states, ATSPI_STATE_INVALID);
|
|
snapshot.multiselectable = atspi_state_set_contains(states, ATSPI_STATE_MULTISELECTABLE);
|
|
snapshot.readOnly = atspi_state_set_contains(states, ATSPI_STATE_READ_ONLY);
|
|
snapshot.required = atspi_state_set_contains(states, ATSPI_STATE_REQUIRED);
|
|
snapshot.selected = atspi_state_set_contains(states, ATSPI_STATE_SELECTED);
|
|
snapshot.sensitive = atspi_state_set_contains(states, ATSPI_STATE_SENSITIVE);
|
|
snapshot.visited = atspi_state_set_contains(states, ATSPI_STATE_VISITED);
|
|
g_object_unref(states);
|
|
}
|
|
GError *error = NULL;
|
|
snapshot.name = atspi_accessible_get_name(source, &error);
|
|
if (error != NULL) {
|
|
g_error_free(error);
|
|
error = NULL;
|
|
}
|
|
snapshot.role = atspi_accessible_get_role_name(source, &error);
|
|
if (error != NULL) {
|
|
g_error_free(error);
|
|
error = NULL;
|
|
}
|
|
if (!snapshot.defunct && (g_strcmp0(snapshot.role, "list") == 0 ||
|
|
g_strcmp0(snapshot.role, "list box") == 0)) {
|
|
snapshot.childCount = atspi_accessible_get_child_count(source, &error);
|
|
snapshot.childCountKnown = error == NULL && snapshot.childCount >= 0;
|
|
g_clear_error(&error);
|
|
}
|
|
if (!snapshot.defunct && !captureDocumentMetadata && g_strcmp0(snapshot.role, "table cell") == 0 &&
|
|
(snapshot.name == NULL || *snapshot.name == '\0')) {
|
|
guint remaining = 20;
|
|
gchar *label = cthulhu_table_cell_label(source, 0, &remaining);
|
|
if (label != NULL) {
|
|
g_free(snapshot.name);
|
|
snapshot.name = label;
|
|
}
|
|
}
|
|
snapshot.description = atspi_accessible_get_description(source, &error);
|
|
if (error != NULL) {
|
|
g_error_free(error);
|
|
}
|
|
if (captureDocumentMetadata) {
|
|
cthulhu_capture_document_metrics(source, &snapshot);
|
|
}
|
|
if (captureSoundGeometry) {
|
|
cthulhu_capture_sound_geometry(source, &snapshot);
|
|
}
|
|
// Copy only semantic attribute fields used by navigation; arbitrary
|
|
// browser attributes can include URLs and unrelated document data.
|
|
error = NULL;
|
|
GHashTable *attributes = NULL;
|
|
gboolean captureSetPosition = !snapshot.defunct && cthulhu_has_set_position(snapshot.role);
|
|
if (captureDocumentMetadata || g_strcmp0(snapshot.role, "heading") == 0 || captureSetPosition) {
|
|
attributes = atspi_accessible_get_attributes(source, &error);
|
|
}
|
|
if (error != NULL) {
|
|
g_clear_error(&error);
|
|
}
|
|
if (captureSetPosition) cthulhu_capture_set_position(source, attributes, &snapshot);
|
|
if (attributes != NULL) {
|
|
if (captureDocumentMetadata) {
|
|
snapshot.explicitName = g_strcmp0(g_hash_table_lookup(attributes, "explicit-name"), "true") == 0;
|
|
const gchar *xmlRoles = g_hash_table_lookup(attributes, "xml-roles");
|
|
if (xmlRoles != NULL) {
|
|
snapshot.xmlRoles = g_strdup(xmlRoles);
|
|
}
|
|
const gchar *htmlTag = g_hash_table_lookup(attributes, "tag");
|
|
if (htmlTag != NULL) snapshot.htmlTag = g_strdup(htmlTag);
|
|
const gchar *elementID = g_hash_table_lookup(attributes, "id");
|
|
if (elementID != NULL) {
|
|
snapshot.elementID = g_strdup(elementID);
|
|
}
|
|
const gchar *containerLive = g_hash_table_lookup(attributes, "container-live");
|
|
if (containerLive != NULL && (g_strcmp0(containerLive, "off") == 0 ||
|
|
g_strcmp0(containerLive, "polite") == 0 ||
|
|
g_strcmp0(containerLive, "assertive") == 0)) {
|
|
snapshot.liveRegion = TRUE;
|
|
}
|
|
const gchar *layoutGuess = g_hash_table_lookup(attributes, "layout-guess");
|
|
if (g_strcmp0(layoutGuess, "true") == 0) {
|
|
snapshot.layoutTable = TRUE;
|
|
}
|
|
}
|
|
if (g_strcmp0(snapshot.role, "heading") == 0) {
|
|
const gchar *level = g_hash_table_lookup(attributes, "level");
|
|
if (level != NULL && *level >= '1' && *level <= '9') {
|
|
gchar *end = NULL;
|
|
gint64 number = g_ascii_strtoll(level, &end, 10);
|
|
if (end != NULL && *end == '\0' && number <= G_MAXINT) {
|
|
snapshot.headingLevel = (gint)number;
|
|
}
|
|
}
|
|
}
|
|
if (attributes != NULL) g_hash_table_unref(attributes);
|
|
}
|
|
if (captureDocumentMetadata && (g_strcmp0(snapshot.role, "table") == 0 || g_strcmp0(snapshot.role, "tree table") == 0)) {
|
|
AtspiTable *table = atspi_accessible_get_table_iface(source);
|
|
if (table != NULL) {
|
|
error = NULL;
|
|
snapshot.tableRowCount = atspi_table_get_n_rows(table, &error);
|
|
if (error != NULL) {
|
|
g_clear_error(&error);
|
|
}
|
|
snapshot.tableColumnCount = atspi_table_get_n_columns(table, NULL);
|
|
g_object_unref(table);
|
|
}
|
|
}
|
|
if (captureDocumentMetadata && (g_strcmp0(snapshot.role, "table cell") == 0 ||
|
|
g_strcmp0(snapshot.role, "column header") == 0 || g_strcmp0(snapshot.role, "row header") == 0)) {
|
|
if (!snapshot.name || !*snapshot.name) {
|
|
guint remaining = 64;
|
|
gchar *label = cthulhu_document_cell_label(source, 0, &remaining);
|
|
g_free(snapshot.name);
|
|
snapshot.name = label;
|
|
}
|
|
AtspiTableCell *cell = atspi_accessible_get_table_cell(source);
|
|
if (cell) {
|
|
error = NULL;
|
|
snapshot.tableRowIndex = snapshot.tableColumnIndex = -1;
|
|
atspi_table_cell_get_row_column_span(cell, &snapshot.tableRowIndex, &snapshot.tableColumnIndex,
|
|
&snapshot.tableRowSpan, &snapshot.tableColumnSpan, &error);
|
|
snapshot.tablePositionKnown = !error && snapshot.tableRowIndex >= 0 && snapshot.tableColumnIndex >= 0 &&
|
|
snapshot.tableRowSpan > 0 && snapshot.tableColumnSpan > 0;
|
|
g_clear_error(&error);
|
|
snapshot.tableRowHeaders = cthulhu_table_headers(atspi_table_cell_get_row_header_cells(cell, NULL));
|
|
snapshot.tableColumnHeaders = cthulhu_table_headers(atspi_table_cell_get_column_header_cells(cell, NULL));
|
|
g_object_unref(cell);
|
|
}
|
|
}
|
|
AtspiAction *action = captureDocumentMetadata ? atspi_accessible_get_action_iface(source) : NULL;
|
|
if (action != NULL) {
|
|
error = NULL;
|
|
gint actionCount = atspi_action_get_n_actions(action, &error);
|
|
if (error != NULL) {
|
|
g_clear_error(&error);
|
|
}
|
|
for (gint index = 0; index < actionCount; index++) {
|
|
error = NULL;
|
|
gchar *actionName = atspi_action_get_name(action, index, &error);
|
|
if (error != NULL) {
|
|
g_clear_error(&error);
|
|
}
|
|
if (g_strcmp0(actionName, "click") == 0) {
|
|
snapshot.supportsClick = TRUE;
|
|
}
|
|
g_free(actionName);
|
|
}
|
|
g_object_unref(action);
|
|
}
|
|
// Text and caret capture make synchronous AT-SPI calls. Focus events for
|
|
// ordinary controls do not use that text in presentation, so avoid stalling
|
|
// the GLib event loop for each Tab stop. Caret events still need line and
|
|
// character text even when they originate in static web content.
|
|
if (captureText && g_strcmp0(snapshot.role, "password text") != 0 &&
|
|
(snapshot.editable || g_strcmp0(snapshot.role, "terminal") == 0 || captureCaretCharacter)) {
|
|
snapshot.text = cthulhu_capture_text(source, &snapshot.textBeforeCaret, &snapshot.textStartOffset);
|
|
}
|
|
if (captureCaretCharacter && g_strcmp0(snapshot.role, "password text") != 0) {
|
|
snapshot.caretCharacter = cthulhu_capture_caret_unit(source, ATSPI_TEXT_GRANULARITY_CHAR);
|
|
}
|
|
cthulhu_capture_value(source, &snapshot);
|
|
return snapshot;
|
|
}
|
|
|
|
static gchar *cthulhu_capture_text(AtspiAccessible *source, gchar **beforeCaret, gint *startOffset) {
|
|
*beforeCaret = NULL;
|
|
AtspiText *text = atspi_accessible_get_text(source);
|
|
if (text == NULL) {
|
|
return NULL;
|
|
}
|
|
GError *error = NULL;
|
|
gint caretOffset = atspi_text_get_caret_offset(text, &error);
|
|
if (error != NULL) {
|
|
g_error_free(error);
|
|
g_object_unref(text);
|
|
return NULL;
|
|
}
|
|
AtspiTextRange *range = atspi_text_get_string_at_offset(
|
|
text, caretOffset, ATSPI_TEXT_GRANULARITY_LINE, &error);
|
|
// AXText.get_line_at_offset retries Chromium's invalid end-of-text
|
|
// range. A valid empty range is a real blank line and must stay empty.
|
|
if (error == NULL && range != NULL && caretOffset > 0) {
|
|
gboolean retry = range->start_offset > caretOffset;
|
|
if (range->start_offset == -1 && range->end_offset == -1) {
|
|
gint count = atspi_text_get_character_count(text, &error);
|
|
retry = error == NULL && caretOffset == count;
|
|
}
|
|
if (retry) {
|
|
g_boxed_free(atspi_text_range_get_type(), range);
|
|
range = atspi_text_get_string_at_offset(
|
|
text, caretOffset - 1, ATSPI_TEXT_GRANULARITY_LINE, &error);
|
|
}
|
|
}
|
|
g_object_unref(text);
|
|
if (error != NULL) {
|
|
g_error_free(error);
|
|
if (range != NULL) g_boxed_free(atspi_text_range_get_type(), range);
|
|
return NULL;
|
|
}
|
|
if (range == NULL) {
|
|
return NULL;
|
|
}
|
|
gchar *content = g_strdup(range->content != NULL ? range->content : "");
|
|
*startOffset = range->start_offset;
|
|
gint relativeCaret = caretOffset - range->start_offset;
|
|
glong characterCount = g_utf8_strlen(content, -1);
|
|
if (relativeCaret < 0) {
|
|
relativeCaret = 0;
|
|
} else if (relativeCaret > characterCount) {
|
|
relativeCaret = (gint)characterCount;
|
|
}
|
|
*beforeCaret = g_utf8_substring(content, 0, relativeCaret);
|
|
g_free(range->content);
|
|
g_free(range);
|
|
return content;
|
|
}
|
|
|
|
static gchar *cthulhu_label_text(AtspiAccessible *source) {
|
|
AtspiText *text = atspi_accessible_get_text(source);
|
|
if (text != NULL) {
|
|
GError *error = NULL;
|
|
gint characterCount = atspi_text_get_character_count(text, &error);
|
|
if (error != NULL) {
|
|
g_clear_error(&error);
|
|
g_object_unref(text);
|
|
return NULL;
|
|
}
|
|
gchar *content = atspi_text_get_text(text, 0, characterCount, &error);
|
|
g_object_unref(text);
|
|
if (error != NULL) {
|
|
g_clear_error(&error);
|
|
return NULL;
|
|
}
|
|
return content != NULL ? content : g_strdup("");
|
|
}
|
|
|
|
GError *error = NULL;
|
|
gchar *name = atspi_accessible_get_name(source, &error);
|
|
if (error != NULL) g_error_free(error);
|
|
return name != NULL ? name : g_strdup("");
|
|
}
|
|
|
|
static void cthulhu_append_label_texts(AtspiAccessible *source, GString *contents,
|
|
int *visited) {
|
|
if (source == NULL || *visited >= 100 || contents->len >= 4096) return;
|
|
(*visited)++;
|
|
GError *error = NULL;
|
|
AtspiRole role = atspi_accessible_get_role(source, &error);
|
|
if (error != NULL) g_clear_error(&error);
|
|
if (role == ATSPI_ROLE_LABEL) {
|
|
gchar *text = cthulhu_label_text(source);
|
|
if (text != NULL && *text != '\0') {
|
|
if (contents->len != 0) g_string_append_c(contents, ' ');
|
|
g_string_append(contents, text);
|
|
}
|
|
g_free(text);
|
|
}
|
|
error = NULL;
|
|
gint childCount = atspi_accessible_get_child_count(source, &error);
|
|
if (error != NULL) {
|
|
g_clear_error(&error);
|
|
return;
|
|
}
|
|
for (gint index = 0; index < childCount && *visited < 100 && contents->len < 4096; index++) {
|
|
error = NULL;
|
|
AtspiAccessible *child = atspi_accessible_get_child_at_index(source, index, &error);
|
|
if (error != NULL) {
|
|
g_clear_error(&error);
|
|
continue;
|
|
}
|
|
cthulhu_append_label_texts(child, contents, visited);
|
|
if (child != NULL) g_object_unref(child);
|
|
}
|
|
}
|
|
|
|
gchar *cthulhu_collect_notification_text(AtspiAccessible *source) {
|
|
if (source == NULL) return g_strdup("");
|
|
GString *contents = g_string_new("");
|
|
int visited = 0;
|
|
cthulhu_append_label_texts(source, contents, &visited);
|
|
return g_string_free(contents, FALSE);
|
|
}
|
|
|
|
// YAD exposes prompts as labels beside buttons or a choice list. Keep the
|
|
// capture within that small shape; ordinary frames contain unrelated labels.
|
|
static gboolean cthulhu_append_prompt_labels(AtspiAccessible *source, GString *contents,
|
|
gint *visited, gint *buttons, gint *labels, gint *choices) {
|
|
if (source == NULL || *visited >= 100 || contents->len >= 4096) return FALSE;
|
|
(*visited)++;
|
|
GError *error = NULL;
|
|
AtspiRole role = atspi_accessible_get_role(source, &error);
|
|
if (error != NULL) {
|
|
g_clear_error(&error);
|
|
return FALSE;
|
|
}
|
|
if (role == ATSPI_ROLE_BUTTON) {
|
|
(*buttons)++;
|
|
return *buttons <= 4;
|
|
}
|
|
if (role == ATSPI_ROLE_LABEL || role == ATSPI_ROLE_STATIC) {
|
|
AtspiStateSet *states = atspi_accessible_get_state_set(source);
|
|
gboolean showing = states != NULL && atspi_state_set_contains(states, ATSPI_STATE_SHOWING);
|
|
if (states != NULL) g_object_unref(states);
|
|
if (!showing) return TRUE;
|
|
gchar *label = cthulhu_label_text(source);
|
|
if (label != NULL && *label != '\0') {
|
|
if (contents->len != 0) g_string_append_c(contents, ' ');
|
|
g_string_append(contents, label);
|
|
(*labels)++;
|
|
}
|
|
g_free(label);
|
|
return TRUE;
|
|
}
|
|
if (role == ATSPI_ROLE_ICON || role == ATSPI_ROLE_SCROLL_BAR) return TRUE;
|
|
if (role == ATSPI_ROLE_TREE_TABLE || role == ATSPI_ROLE_TREE ||
|
|
role == ATSPI_ROLE_TABLE || role == ATSPI_ROLE_LIST) {
|
|
(*choices)++;
|
|
return *choices <= 1;
|
|
}
|
|
if (role != ATSPI_ROLE_FRAME && role != ATSPI_ROLE_DIALOG &&
|
|
role != ATSPI_ROLE_FILLER && role != ATSPI_ROLE_PANEL &&
|
|
role != ATSPI_ROLE_SCROLL_PANE) return FALSE;
|
|
gint childCount = atspi_accessible_get_child_count(source, &error);
|
|
if (error != NULL) {
|
|
g_clear_error(&error);
|
|
return FALSE;
|
|
}
|
|
for (gint index = 0; index < childCount; index++) {
|
|
AtspiAccessible *child = atspi_accessible_get_child_at_index(source, index, &error);
|
|
if (error != NULL || child == NULL) {
|
|
g_clear_error(&error);
|
|
if (child != NULL) g_object_unref(child);
|
|
return FALSE;
|
|
}
|
|
gboolean valid = cthulhu_append_prompt_labels(child, contents, visited, buttons, labels, choices);
|
|
g_object_unref(child);
|
|
if (!valid) return FALSE;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
gchar *cthulhu_collect_simple_prompt_text(AtspiAccessible *source) {
|
|
if (source == NULL) return NULL;
|
|
GString *contents = g_string_new("");
|
|
gint visited = 0, buttons = 0, labels = 0, choices = 0;
|
|
gboolean valid = cthulhu_append_prompt_labels(source, contents, &visited, &buttons, &labels, &choices);
|
|
if (!valid || labels == 0 || (buttons == 0 && choices == 0)) {
|
|
g_string_free(contents, TRUE);
|
|
return NULL;
|
|
}
|
|
return g_string_free(contents, FALSE);
|
|
}
|
|
|
|
// Follow the same label/ancestor conventions used by GTK radio groups. A
|
|
// named panel or filler is the group's question, not the option's own name.
|
|
gchar *cthulhu_radio_group_name(AtspiAccessible *source) {
|
|
GArray *relations = atspi_accessible_get_relation_set(source, NULL);
|
|
if (relations != NULL) {
|
|
for (guint i = 0; i < relations->len; i++) {
|
|
AtspiRelation *relation = g_array_index(relations, AtspiRelation *, i);
|
|
if (atspi_relation_get_relation_type(relation) == ATSPI_RELATION_LABELLED_BY &&
|
|
atspi_relation_get_n_targets(relation) > 0) {
|
|
AtspiAccessible *label = atspi_relation_get_target(relation, 0);
|
|
gchar *name = label != NULL ? cthulhu_label_text(label) : NULL;
|
|
gchar *option = atspi_accessible_get_name(source, NULL);
|
|
gboolean ownLabel = g_strcmp0(name, option) == 0;
|
|
g_free(option);
|
|
if (label != NULL) g_object_unref(label);
|
|
for (guint j = 0; j < relations->len; j++)
|
|
g_object_unref(g_array_index(relations, AtspiRelation *, j));
|
|
g_array_unref(relations);
|
|
if (name != NULL && *name != '\0' && !ownLabel) return name;
|
|
g_free(name);
|
|
relations = NULL;
|
|
break;
|
|
}
|
|
}
|
|
if (relations != NULL) {
|
|
for (guint i = 0; i < relations->len; i++)
|
|
g_object_unref(g_array_index(relations, AtspiRelation *, i));
|
|
g_array_unref(relations);
|
|
}
|
|
}
|
|
AtspiAccessible *current = atspi_accessible_get_parent(source, NULL);
|
|
for (int depth = 0; current != NULL && depth < 16; depth++) {
|
|
AtspiRole role = atspi_accessible_get_role(current, NULL);
|
|
if (role == ATSPI_ROLE_FRAME || role == ATSPI_ROLE_DIALOG || role == ATSPI_ROLE_APPLICATION) {
|
|
g_object_unref(current);
|
|
return NULL;
|
|
}
|
|
if (role == ATSPI_ROLE_PANEL || role == ATSPI_ROLE_FILLER) {
|
|
gchar *name = atspi_accessible_get_name(current, NULL);
|
|
if (name != NULL && *name != '\0') {
|
|
g_object_unref(current);
|
|
return name;
|
|
}
|
|
g_free(name);
|
|
}
|
|
AtspiAccessible *parent = atspi_accessible_get_parent(current, NULL);
|
|
g_object_unref(current);
|
|
current = parent;
|
|
}
|
|
if (current != NULL) g_object_unref(current);
|
|
return NULL;
|
|
}
|
|
|
|
static void cthulhu_append_live_text(AtspiAccessible *source, GString *contents, int *visited) {
|
|
if (source == NULL || *visited >= 100 || contents->len >= 4096) return;
|
|
(*visited)++;
|
|
GError *error = NULL;
|
|
gint childCount = atspi_accessible_get_child_count(source, &error);
|
|
if (error != NULL) {
|
|
g_clear_error(&error);
|
|
childCount = 0;
|
|
}
|
|
guint before = contents->len;
|
|
for (gint index = 0; index < childCount && *visited < 100 && contents->len < 4096; index++) {
|
|
AtspiAccessible *child = atspi_accessible_get_child_at_index(source, index, &error);
|
|
if (error != NULL) g_clear_error(&error);
|
|
if (child != NULL) {
|
|
cthulhu_append_live_text(child, contents, visited);
|
|
g_object_unref(child);
|
|
}
|
|
}
|
|
if (contents->len == before) {
|
|
gchar *text = cthulhu_label_text(source);
|
|
if (text != NULL && *text != '\0') {
|
|
if (contents->len != 0) g_string_append_c(contents, ' ');
|
|
g_string_append(contents, text);
|
|
}
|
|
g_free(text);
|
|
}
|
|
}
|
|
|
|
gchar *cthulhu_live_content(AtspiAccessible *source) {
|
|
GString *contents = g_string_new("");
|
|
int visited = 0;
|
|
cthulhu_append_live_text(source, contents, &visited);
|
|
return g_string_free(contents, FALSE);
|
|
}
|
|
|
|
gchar *cthulhu_capture_caret_unit(AtspiAccessible *source, AtspiTextGranularity unit) {
|
|
AtspiText *text = atspi_accessible_get_text(source);
|
|
if (text == NULL) {
|
|
return NULL;
|
|
}
|
|
GError *error = NULL;
|
|
gint caretOffset = atspi_text_get_caret_offset(text, &error);
|
|
if (error != NULL) {
|
|
g_error_free(error);
|
|
g_object_unref(text);
|
|
return NULL;
|
|
}
|
|
if (caretOffset < 0) {
|
|
g_object_unref(text);
|
|
return NULL;
|
|
}
|
|
AtspiTextRange *range = atspi_text_get_string_at_offset(
|
|
text, caretOffset, unit, &error);
|
|
g_object_unref(text);
|
|
if (error != NULL) {
|
|
g_error_free(error);
|
|
if (range != NULL) g_boxed_free(atspi_text_range_get_type(), range);
|
|
return NULL;
|
|
}
|
|
if (range == NULL) {
|
|
return NULL;
|
|
}
|
|
gchar *content = g_strdup(range->content != NULL ? range->content : "");
|
|
g_free(range->content);
|
|
g_free(range);
|
|
return content;
|
|
}
|
|
|
|
static void cthulhu_capture_document_metrics(AtspiAccessible *source, cthulhu_snapshot *snapshot) {
|
|
const gchar *role = snapshot->role;
|
|
if (g_strcmp0(role, "paragraph") != 0 &&
|
|
g_strcmp0(role, "list item") != 0 &&
|
|
g_strcmp0(role, "math") != 0 &&
|
|
g_strcmp0(role, "static") != 0 &&
|
|
g_strcmp0(role, "column header") != 0 &&
|
|
g_strcmp0(role, "row header") != 0 &&
|
|
g_strcmp0(role, "table cell") != 0 &&
|
|
g_strcmp0(role, "table row") != 0 &&
|
|
g_strcmp0(role, "text") != 0 &&
|
|
g_strcmp0(role, "section") != 0 &&
|
|
g_strcmp0(role, "article") != 0 &&
|
|
g_strcmp0(role, "description term") != 0 &&
|
|
g_strcmp0(role, "description value") != 0 &&
|
|
g_strcmp0(role, "document email") != 0 &&
|
|
g_strcmp0(role, "document frame") != 0 &&
|
|
g_strcmp0(role, "document presentation") != 0 &&
|
|
g_strcmp0(role, "document spreadsheet") != 0 &&
|
|
g_strcmp0(role, "document text") != 0 &&
|
|
g_strcmp0(role, "document web") != 0 &&
|
|
g_strcmp0(role, "block quote") != 0 &&
|
|
g_strcmp0(role, "description list") != 0 &&
|
|
g_strcmp0(role, "form") != 0 &&
|
|
g_strcmp0(role, "footer") != 0 &&
|
|
g_strcmp0(role, "header") != 0 &&
|
|
g_strcmp0(role, "landmark") != 0 &&
|
|
g_strcmp0(role, "log") != 0 &&
|
|
g_strcmp0(role, "list") != 0 &&
|
|
g_strcmp0(role, "marquee") != 0 &&
|
|
g_strcmp0(role, "panel") != 0 &&
|
|
g_strcmp0(role, "table") != 0 &&
|
|
g_strcmp0(role, "tree") != 0 &&
|
|
g_strcmp0(role, "tree table") != 0) {
|
|
return;
|
|
}
|
|
AtspiText *text = atspi_accessible_get_text(source);
|
|
if (text == NULL) {
|
|
return;
|
|
}
|
|
GError *error = NULL;
|
|
gint characterCount = atspi_text_get_character_count(text, &error);
|
|
if (error != NULL) {
|
|
g_clear_error(&error);
|
|
return;
|
|
}
|
|
snapshot->characterCount = characterCount;
|
|
if (characterCount > 75) {
|
|
error = NULL;
|
|
gchar *contents = atspi_text_get_text(text, 0, characterCount, &error);
|
|
if (error != NULL) {
|
|
g_clear_error(&error);
|
|
} else if (contents != NULL) {
|
|
for (const gchar *cursor = contents; *cursor != '\0'; cursor = g_utf8_next_char(cursor)) {
|
|
if (g_utf8_get_char(cursor) == 0xFFFC) {
|
|
snapshot->embeddedObjectCount++;
|
|
}
|
|
}
|
|
g_free(contents);
|
|
}
|
|
}
|
|
}
|