// 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" // Only the native owner calls this, after foreground validation. Counts use a // fixed number of RPCs; no selected-row/column arrays or sheet walks are used. // LibreOffice itself scans its row marks when answering selected-row counts. func captureLibreOfficeSelection(source *C.AtspiAccessible) *LibreOfficeSelection { selection := C.atspi_accessible_get_selection_iface(source) table := C.atspi_accessible_get_table_iface(source) if selection == nil || table == nil { if selection != nil { C.g_object_unref(C.gpointer(selection)) } if table != nil { C.g_object_unref(C.gpointer(table)) } return nil } defer C.g_object_unref(C.gpointer(selection)) defer C.g_object_unref(C.gpointer(table)) var queryError *C.GError count := int(C.atspi_selection_get_n_selected_children(selection, &queryError)) if queryError != nil { C.g_error_free(queryError) return nil } if count < 0 { return nil } result := &LibreOfficeSelection{Kind: "cells", Count: count, CountLimited: count == 1<<31-1} if count == 0 { result.Complete = true return result } rows := int(C.atspi_table_get_n_rows(table, &queryError)) if queryError != nil { C.g_error_free(queryError) return nil } columns := int(C.atspi_table_get_n_columns(table, &queryError)) if queryError != nil { C.g_error_free(queryError) return nil } selectedRows, selectedColumns := 0, 0 // A small selection cannot contain a whole row or column. Avoid making // LibreOffice scan all its row marks for ordinary Shift+Arrow movement. if count >= columns { selectedRows = int(C.atspi_table_get_n_selected_rows(table, &queryError)) if queryError != nil { C.g_error_free(queryError) return nil } } if count >= rows { selectedColumns = int(C.atspi_table_get_n_selected_columns(table, &queryError)) if queryError != nil { C.g_error_free(queryError) return nil } } if rows <= 0 || columns <= 0 || selectedRows < 0 || selectedColumns < 0 { return nil } if selectedRows == rows || selectedColumns == columns { return &LibreOfficeSelection{Kind: "all", Complete: true} } stride := 1 // Only classify pure row/column selections. Mixed selections retain an // honest cell count. GTK caps counts above INT_MAX, rather than wrapping. if !result.CountLimited && selectedRows > 0 && int64(selectedRows)*int64(columns) == int64(count) { result.Kind, result.Count, result.CountLimited = "rows", selectedRows, false stride = columns } else if !result.CountLimited && selectedColumns > 0 && int64(selectedColumns)*int64(rows) == int64(count) { result.Kind, result.Count, result.CountLimited = "columns", selectedColumns, false } indices := make([]int, 0, libreOfficeSelectionLimit) if result.Count > libreOfficeSelectionLimit { indices = append(indices, 0, count-1) } else { for index := 0; index < result.Count; index++ { indices = append(indices, index*stride) } } for _, index := range indices { child := C.atspi_selection_get_selected_child(selection, C.gint(index), &queryError) if queryError != nil { C.g_error_free(queryError) if child != nil { C.g_object_unref(C.gpointer(child)) } return nil } if child == nil { return nil } cell := C.atspi_accessible_get_table_cell(child) var row, column C.gint valid := false if cell != nil { valid = C.atspi_table_cell_get_position(cell, &row, &column, &queryError) != 0 C.g_object_unref(C.gpointer(cell)) } C.g_object_unref(C.gpointer(child)) if queryError != nil { C.g_error_free(queryError) return nil } if !valid || row < 0 || column < 0 || int(row) >= rows || int(column) >= columns { return nil } position := LibreOfficeCell{Row: int(row), Column: int(column)} if result.Kind == "rows" { position.Column = 0 } if result.Kind == "columns" { position.Row = 0 } result.Cells = append(result.Cells, position) } result.First, result.Last = result.Cells[0], result.Cells[len(result.Cells)-1] result.Complete = result.Count <= libreOfficeSelectionLimit if !result.Complete { result.Cells = nil } return result }