Files

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);
}
}
}