Files
cthulhu/internal/atspi/adapter_position.c

128 lines
5.9 KiB
C

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
#include "adapter_native.h"
gboolean cthulhu_has_set_position(const gchar *role) {
return g_strcmp0(role, "list item") == 0 || g_strcmp0(role, "tree item") == 0 ||
g_strcmp0(role, "menu item") == 0 || g_strcmp0(role, "check menu item") == 0 ||
g_strcmp0(role, "radio menu item") == 0 || g_strcmp0(role, "page tab") == 0 ||
g_strcmp0(role, "radio button") == 0;
}
static gint cthulhu_positive_integer(const gchar *value) {
if (value == NULL || *value < '0' || *value > '9') return 0;
gchar *end = NULL;
gint64 number = g_ascii_strtoll(value, &end, 10);
if (*end != '\0' || number <= 0 || number > G_MAXINT) return 0;
return (gint)number;
}
static gboolean cthulhu_menu_item(AtspiRole role) {
return role == ATSPI_ROLE_MENU_ITEM || role == ATSPI_ROLE_CHECK_MENU_ITEM ||
role == ATSPI_ROLE_RADIO_MENU_ITEM;
}
static gboolean cthulhu_group_member(AtspiRelation *relation, AtspiAccessible *child) {
for (gint index = 0; index < atspi_relation_get_n_targets(relation); index++) {
AtspiAccessible *target = atspi_relation_get_target(relation, index);
gboolean matches = target != NULL &&
g_strcmp0(cthulhu_accessible_bus_name(target), cthulhu_accessible_bus_name(child)) == 0 &&
g_strcmp0(cthulhu_accessible_path(target), cthulhu_accessible_path(child)) == 0;
if (target != NULL) g_object_unref(target);
if (matches) return TRUE;
}
return FALSE;
}
void cthulhu_capture_set_position(AtspiAccessible *source, GHashTable *attributes,
cthulhu_snapshot *snapshot) {
const gchar *position = attributes == NULL ? NULL : g_hash_table_lookup(attributes, "posinset");
const gchar *size = attributes == NULL ? NULL : g_hash_table_lookup(attributes, "setsize");
// Explicit provider values describe virtualized and hierarchical sets.
// Partial, unknown (-1), or invalid values must not become sibling guesses.
if (position != NULL || size != NULL) {
gint parsedPosition = cthulhu_positive_integer(position);
gint parsedSize = cthulhu_positive_integer(size);
if (parsedPosition > 0 && parsedPosition <= parsedSize) {
snapshot->positionInSet = parsedPosition;
snapshot->setSize = parsedSize;
}
return;
}
GError *error = NULL;
AtspiAccessible *parent = atspi_accessible_get_parent(source, &error);
if (error != NULL || parent == NULL) {
g_clear_error(&error);
if (parent != NULL) g_object_unref(parent);
return;
}
AtspiRole sourceRole = atspi_accessible_get_role(source, &error);
GArray *relations = NULL;
AtspiRelation *memberRelation = NULL;
if (error != NULL) goto cleanup;
if (sourceRole == ATSPI_ROLE_RADIO_BUTTON) {
relations = atspi_accessible_get_relation_set(source, &error);
if (error != NULL) goto cleanup;
for (guint index = 0; relations != NULL && index < relations->len; index++) {
AtspiRelation *relation = g_array_index(relations, AtspiRelation *, index);
if (atspi_relation_get_relation_type(relation) == ATSPI_RELATION_MEMBER_OF) {
memberRelation = relation;
if (atspi_relation_get_n_targets(relation) > 128) goto cleanup;
break;
}
}
}
AtspiRole parentRole = atspi_accessible_get_role(parent, &error);
if (error != NULL) goto cleanup;
// A flattened tree does not reliably describe logical sibling sets.
if (sourceRole == ATSPI_ROLE_TREE_ITEM) goto cleanup;
if (sourceRole == ATSPI_ROLE_LIST_ITEM && parentRole != ATSPI_ROLE_LIST && parentRole != ATSPI_ROLE_LIST_BOX)
goto cleanup;
if (sourceRole == ATSPI_ROLE_PAGE_TAB && parentRole != ATSPI_ROLE_PAGE_TAB_LIST) goto cleanup;
if (cthulhu_menu_item(sourceRole) && parentRole != ATSPI_ROLE_MENU && parentRole != ATSPI_ROLE_MENU_BAR)
goto cleanup;
gint count = atspi_accessible_get_child_count(parent, &error);
if (error != NULL) goto cleanup;
if (count <= 0) goto cleanup;
gint index = atspi_accessible_get_index_in_parent(source, &error);
if (error != NULL || index < 0 || index >= count) goto cleanup;
// Lists and tab lists expose their items in child order, including large
// sets. This uses a fixed number of queries rather than a sibling walk.
if (sourceRole == ATSPI_ROLE_LIST_ITEM || sourceRole == ATSPI_ROLE_PAGE_TAB) {
snapshot->positionInSet = index + 1;
snapshot->setSize = count;
goto cleanup;
}
// Menus contain separators; radio containers can contain labels and
// unrelated controls. Count only item peers, with bounded native work.
if (count > 128) goto cleanup;
gint itemPosition = 0, setSize = 0;
for (gint childIndex = 0; childIndex < count; childIndex++) {
AtspiAccessible *child = atspi_accessible_get_child_at_index(parent, childIndex, &error);
if (error != NULL || child == NULL) {
if (child != NULL) g_object_unref(child);
goto cleanup;
}
AtspiRole childRole = atspi_accessible_get_role(child, &error);
gboolean member = memberRelation == NULL || cthulhu_group_member(memberRelation, child);
g_object_unref(child);
if (error != NULL) goto cleanup;
if (!member) continue;
if (childRole != sourceRole && !(cthulhu_menu_item(sourceRole) && cthulhu_menu_item(childRole))) continue;
setSize++;
if (childIndex == index) itemPosition = setSize;
}
snapshot->positionInSet = itemPosition;
snapshot->setSize = setSize;
cleanup:
if (relations != NULL) {
for (guint index = 0; index < relations->len; index++)
g_object_unref(g_array_index(relations, AtspiRelation *, index));
g_array_unref(relations);
}
g_clear_error(&error);
g_object_unref(parent);
}