Files
cthulhu/internal/atspi/review_table_native.go

238 lines
7.5 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package atspi
/*
#include "adapter_native.h"
*/
import "C"
import (
"context"
"fmt"
)
// native_review_table_children enumerates a selectable table's viewport, as
// Orca's flat review does. GTK exposes every offscreen row as a child; fetching
// that list creates thousands of transient cell proxies during timeline refresh.
// Returned references belong to the caller. All queries run on the GLib owner.
func native_review_table_children(ctx context.Context, source *C.AtspiAccessible, clip Rectangle) (children []*C.AtspiAccessible, handled bool, err error) {
if !clip.Valid() {
return nil, false, nil
}
table := C.atspi_accessible_get_table_iface(source)
if table == nil {
return nil, false, nil
}
defer C.g_object_unref(C.gpointer(table))
selection := C.atspi_accessible_get_selection_iface(source)
if selection == nil {
return nil, false, nil
}
defer C.g_object_unref(C.gpointer(selection))
component := C.atspi_accessible_get_component_iface(source)
if component == nil {
return nil, false, nil
}
defer C.g_object_unref(C.gpointer(component))
defer func() {
if err != nil {
for _, child := range children {
C.g_object_unref(C.gpointer(child))
}
children = nil
}
}()
var queryError *C.GError
rows := int(C.atspi_table_get_n_rows(table, &queryError))
if queryError != nil {
err = nativeError("query review table rows", queryError)
C.g_error_free(queryError)
return nil, true, err
}
columns := int(C.atspi_table_get_n_columns(table, &queryError))
if queryError != nil {
err = nativeError("query review table columns", queryError)
C.g_error_free(queryError)
return nil, true, err
}
if rows < 0 || columns < 0 {
return nil, true, fmt.Errorf("%w: negative review table dimensions", ErrInvalidTree)
}
// Native focus normalization uses the first selected cell of a uniquely
// selected row. Keep that identity as an anchor when horizontal scrolling
// hides its column; its geometry still excludes it from review output.
if rows > 0 {
if selected := C.atspi_selection_get_selected_child(selection, 0, nil); selected != nil {
children = append(children, selected)
}
}
appendHeaders := func(first, last int) error {
if last-first >= maxTreeNodes {
return fmt.Errorf("%w: visible table exceeds limit %d", ErrInvalidTree, maxTreeNodes)
}
for column := first; column <= last; column++ {
if err := ctx.Err(); err != nil {
return err
}
header := C.atspi_table_get_column_header(table, C.gint(column), nil)
if header == nil {
continue
}
geometry, geometryErr := geometryFromNative(ctx, header)
_, intersects := geometry.Bounds.Intersection(clip)
if geometryErr == nil && geometry.Visible && geometry.Showing && intersects {
children = append(children, header)
} else {
C.g_object_unref(C.gpointer(header))
}
if geometryErr != nil {
return geometryErr
}
}
return nil
}
if rows == 0 || columns == 0 {
err = appendHeaders(0, columns-1)
return children, true, err
}
// Start just below the table's header. Hit testing avoids walking from row
// zero when the viewport is thousands of rows down the model. A header's
// bounds let us skip directly to its bottom edge.
seedRow, seedColumn := -1, -1
bottom := int64(clip.Y) + int64(clip.Height)
for y := int64(clip.Y) + 1; y < bottom && seedRow < 0; y++ {
if err := ctx.Err(); err != nil {
return children, true, err
}
for _, x := range []int64{int64(clip.X) + 1, int64(clip.X) + int64(clip.Width)/2} {
child := C.atspi_component_get_accessible_at_point(component, C.gint(x), C.gint(y), C.ATSPI_COORD_TYPE_SCREEN, &queryError)
if queryError != nil {
err = nativeError("locate visible review table cell", queryError)
C.g_error_free(queryError)
if child != nil {
C.g_object_unref(C.gpointer(child))
}
return children, true, err
}
if child == nil {
continue
}
row, column := -1, -1
if C.atspi_accessible_get_role(child, nil) == C.ATSPI_ROLE_TABLE_CELL {
if cell := C.atspi_accessible_get_table_cell(child); cell != nil {
var nativeRow, nativeColumn C.gint
C.atspi_table_cell_get_position(cell, &nativeRow, &nativeColumn, &queryError)
if queryError == nil {
row, column = int(nativeRow), int(nativeColumn)
} else {
err = nativeError("query visible review cell position", queryError)
C.g_error_free(queryError)
C.g_object_unref(C.gpointer(cell))
C.g_object_unref(C.gpointer(child))
return children, true, err
}
C.g_object_unref(C.gpointer(cell))
} else {
index := C.atspi_accessible_get_index_in_parent(child, nil)
row = int(C.atspi_table_get_row_at_index(table, index, nil))
column = int(C.atspi_table_get_column_at_index(table, index, nil))
}
}
geometry, geometryErr := geometryFromNative(ctx, child)
C.g_object_unref(C.gpointer(child))
if geometryErr != nil {
return children, true, geometryErr
}
if row >= 0 && row < rows && column >= 0 && column < columns {
seedRow, seedColumn = row, column
break
}
if geometry.BoundsKnown {
y = max(y, int64(geometry.Bounds.Y)+int64(geometry.Bounds.Height)-1)
}
}
}
if seedRow < 0 {
// An empty or clipped body can still have visible column headers.
err = appendHeaders(0, columns-1)
return children, true, err
}
visible := func(row, column int, keep bool) (bool, error) {
if err := ctx.Err(); err != nil {
return false, err
}
child := C.atspi_table_get_accessible_at(table, C.gint(row), C.gint(column), &queryError)
if queryError != nil {
err := nativeError("query visible review table cell", queryError)
C.g_error_free(queryError)
queryError = nil
if child != nil {
C.g_object_unref(C.gpointer(child))
}
return false, err
}
if child == nil {
return false, nil
}
geometry, geometryErr := geometryFromNative(ctx, child)
_, intersects := geometry.Bounds.Intersection(clip)
showing := geometry.Visible && geometry.Showing && geometry.BoundsKnown && intersects
if keep && showing && geometryErr == nil {
children = append(children, child)
} else {
C.g_object_unref(C.gpointer(child))
}
return showing, geometryErr
}
firstRow, lastRow := seedRow, seedRow
firstColumn, lastColumn := seedColumn, seedColumn
for _, axis := range []struct {
first, last *int
limit int
row bool
}{{&firstRow, &lastRow, rows, true}, {&firstColumn, &lastColumn, columns, false}} {
for _, step := range []int{-1, 1} {
edge := axis.first
if step > 0 {
edge = axis.last
}
for next := *edge + step; next >= 0 && next < axis.limit; next += step {
if *axis.last-*axis.first >= maxTreeNodes {
return children, true, fmt.Errorf("%w: visible table exceeds limit %d", ErrInvalidTree, maxTreeNodes)
}
row, column := seedRow, seedColumn
if axis.row {
row = next
} else {
column = next
}
showing, err := visible(row, column, false)
if err != nil {
return children, true, err
}
if !showing {
break
}
*edge = next
}
}
}
if err := appendHeaders(firstColumn, lastColumn); err != nil {
return children, true, err
}
for row := firstRow; row <= lastRow; row++ {
for column := firstColumn; column <= lastColumn; column++ {
if len(children) >= maxTreeNodes {
return children, true, fmt.Errorf("%w: visible table exceeds limit %d", ErrInvalidTree, maxTreeNodes)
}
if _, err := visible(row, column, true); err != nil {
return children, true, err
}
}
}
return children, true, nil
}