128 lines
5.9 KiB
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);
|
|
}
|