238 lines
7.5 KiB
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
|
|
}
|