gtk4 a11y: Implement new GtkAccessibleTextInterface

Implement most of the methods of the
`GtkAccessibleInterface` newly added to Gtk 4 in
Gtk commit [1]

    commit 0ca8d74842837b1ad5dc42c1fcff8b1270e5750b
    Author: Matthias Clasen <mclasen@redhat.com>
    Date:   Tue Feb 20 12:18:27 2024 -0500

        a11y: Add GtkAccessibleText interface

        The AccessibleText interface is meant to be implemented by widgets and
        other accessible objects that expose selectable, navigatable, or rich
        text to assistive technologies.

        This kind of text is not covered by the plain accessible name and
        description, as it contains things like a caret, or text attributes.

        This commit adds a stub GtkAccessibleText with its basic virtual
        functions; the interface will be implemented by widgets like GtkLabel,
        GtkInscription, GtkText, and GtkTextView. A further commit will ensure
        that the AT-SPI implementation will convert from GTK to AT-SPI through a
        generic (internal API); and, finally, we'll remove the widget type
        checks in the AT-SPI implementation of GtkATContext, and only check for
        GtkAccessibleText.

        Fixes: #5912

and follow-up commits. The `css::accessibility::XAccessibleText`
interface provides the required functionality.

With a Writer paragraph consisting of the text
"Hello world. And another sentence."
and the word "world" selected, using some of the AT-SPI Text
interface methods via Accerciser's IPython console behaves as expected
now when the paragraph's a11y object is selected in Accerciser's
treeview:

    In [9]: text = acc.queryText()
    In [10]: text.get_caretOffset()
    Out[10]: 11
    In [11]: text.getText(0, -1)
    Out[11]: 'Hello world. And another sentence.'
    In [12]: text.getText(2,5)
    Out[12]: 'llo'
    In [13]: text.getStringAtOffset(10, pyatspi.TEXT_GRANULARITY_CHAR)
    Out[13]: ('d', 10, 11)
    In [14]: text.getStringAtOffset(10, pyatspi.TEXT_GRANULARITY_WORD)
    Out[14]: ('world', 6, 11)
    In [15]: text.getStringAtOffset(10, pyatspi.TEXT_GRANULARITY_SENTENCE)
    Out[15]: ('Hello world. ', 0, 13)
    In [16]: text.getStringAtOffset(10, pyatspi.TEXT_GRANULARITY_PARAGRAPH)
    Out[16]: ('Hello world. And another sentence.', 0, 34)
    In [17]: text.getNSelections()
    Out[17]: 1
    In [18]: text.getSelection(0)
    Out[18]: (6, 11)

Actual handling of text attributes is left for later (s. TODO comment
in the newly added `lo_accessible_text_get_attributes`).

[1] https://gitlab.gnome.org/GNOME/gtk/-/commit/0ca8d74842837b1ad5dc42c1fcff8b1270e5750b

Change-Id: Icad236cd87285d9a336883e67b191f633e9e4413
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/163733
Tested-by: Jenkins
Reviewed-by: Michael Weghorn <m.weghorn@posteo.de>
diff --git a/vcl/Library_vclplug_gtk4.mk b/vcl/Library_vclplug_gtk4.mk
index 72ffeb0..a383414 100644
--- a/vcl/Library_vclplug_gtk4.mk
+++ b/vcl/Library_vclplug_gtk4.mk
@@ -90,6 +90,7 @@ $(eval $(call gb_Library_add_exception_objects,vclplug_gtk4,\
    vcl/unx/gtk4/customcellrenderer \
    vcl/unx/gtk4/gtkaccessibleeventlistener \
    vcl/unx/gtk4/gtkaccessibleregistry \
    vcl/unx/gtk4/gtkaccessibletext \
    vcl/unx/gtk4/gtkdata \
    vcl/unx/gtk4/gtkinst \
    vcl/unx/gtk4/gtksys \
diff --git a/vcl/unx/gtk4/a11y.cxx b/vcl/unx/gtk4/a11y.cxx
index 19cb941b..41e49bf 100644
--- a/vcl/unx/gtk4/a11y.cxx
+++ b/vcl/unx/gtk4/a11y.cxx
@@ -23,6 +23,7 @@
#include "a11y.hxx"
#include "gtkaccessibleeventlistener.hxx"
#include "gtkaccessibleregistry.hxx"
#include "gtkaccessibletext.hxx"

#define OOO_TYPE_FIXED (ooo_fixed_get_type())
#define OOO_FIXED(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), OOO_TYPE_FIXED, OOoFixed))
@@ -410,9 +411,13 @@ const struct
    GetGIfaceType const aGetGIfaceType;
    const css::uno::Type& (*aGetUnoType)();
} TYPE_TABLE[] = {
#if GTK_CHECK_VERSION(4, 13, 8)
    { "Text", reinterpret_cast<GInterfaceInitFunc>(lo_accessible_text_init),
      gtk_accessible_text_get_type, cppu::UnoType<css::accessibility::XAccessibleText>::get },
#endif
#if GTK_CHECK_VERSION(4, 10, 0)
    { "Value", reinterpret_cast<GInterfaceInitFunc>(lo_accessible_range_init),
      gtk_accessible_range_get_type, cppu::UnoType<css::accessibility::XAccessibleValue>::get }
      gtk_accessible_range_get_type, cppu::UnoType<css::accessibility::XAccessibleValue>::get },
#endif
};

diff --git a/vcl/unx/gtk4/gtkaccessibletext.cxx b/vcl/unx/gtk4/gtkaccessibletext.cxx
new file mode 100644
index 0000000..32e1448
--- /dev/null
+++ b/vcl/unx/gtk4/gtkaccessibletext.cxx
@@ -0,0 +1,145 @@
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This file is part of the LibreOffice project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

#include <com/sun/star/accessibility/AccessibleTextType.hpp>
#include <com/sun/star/accessibility/TextSegment.hpp>
#include <com/sun/star/accessibility/XAccessibleText.hpp>
#include <sal/log.hxx>

#include "a11y.hxx"
#include "gtkaccessibletext.hxx"

#if GTK_CHECK_VERSION(4, 13, 8)

namespace
{
sal_Int16 lcl_GtkTextGranularityToUNOBoundaryType(GtkAccessibleTextGranularity eGranularity)
{
    switch (eGranularity)
    {
        case GTK_ACCESSIBLE_TEXT_GRANULARITY_CHARACTER:
            return com::sun::star::accessibility::AccessibleTextType::CHARACTER;
        case GTK_ACCESSIBLE_TEXT_GRANULARITY_WORD:
            return com::sun::star::accessibility::AccessibleTextType::WORD;
        case GTK_ACCESSIBLE_TEXT_GRANULARITY_SENTENCE:
            return com::sun::star::accessibility::AccessibleTextType::SENTENCE;
        case GTK_ACCESSIBLE_TEXT_GRANULARITY_LINE:
            return com::sun::star::accessibility::AccessibleTextType::LINE;
        case GTK_ACCESSIBLE_TEXT_GRANULARITY_PARAGRAPH:
            return com::sun::star::accessibility::AccessibleTextType::PARAGRAPH;
        default:
            assert(false && "Unhandled GtkAccessibleTextGranularity.");
            return GTK_ACCESSIBLE_TEXT_GRANULARITY_CHARACTER;
    }
}

css::uno::Reference<css::accessibility::XAccessibleText> getXText(GtkAccessibleText* pGtkText)
{
    LoAccessible* pAccessible = LO_ACCESSIBLE(pGtkText);
    if (!pAccessible->uno_accessible)
        return nullptr;

    css::uno::Reference<css::accessibility::XAccessibleContext> xContext(
        pAccessible->uno_accessible->getAccessibleContext());

    css::uno::Reference<css::accessibility::XAccessibleText> xText(xContext, css::uno::UNO_QUERY);
    return xText;
}
}

static GBytes* lo_accessible_text_get_contents(GtkAccessibleText* self, unsigned int start,
                                               unsigned int end)
{
    css::uno::Reference<css::accessibility::XAccessibleText> xText = getXText(self);
    if (!xText.is())
        return nullptr;

    // G_MAXUINT has special meaning: end of the text
    const sal_Int32 nEndIndex = (end == G_MAXUINT) ? xText->getCharacterCount() : end;

    const OString sText
        = rtl::OUStringToOString(xText->getTextRange(start, nEndIndex), RTL_TEXTENCODING_UTF8);
    return g_bytes_new(sText.getStr(), sText.getLength());
}

static GBytes* lo_accessible_text_get_contents_at(GtkAccessibleText* self, unsigned int offset,
                                                  GtkAccessibleTextGranularity eGranularity,
                                                  unsigned int* start, unsigned int* end)
{
    css::uno::Reference<css::accessibility::XAccessibleText> xText = getXText(self);
    if (!xText.is())
        return nullptr;

    if (offset < 0 || offset > o3tl::make_unsigned(xText->getCharacterCount()))
    {
        SAL_WARN("vcl.gtk",
                 "lo_accessible_text_get_contents_at called with invalid offset: " << offset);
        return nullptr;
    }

    const sal_Int16 nUnoBoundaryType = lcl_GtkTextGranularityToUNOBoundaryType(eGranularity);
    const css::accessibility::TextSegment aSegment
        = xText->getTextAtIndex(offset, nUnoBoundaryType);
    *start = o3tl::make_unsigned(aSegment.SegmentStart);
    *end = o3tl::make_unsigned(aSegment.SegmentEnd);
    const OString sText = rtl::OUStringToOString(aSegment.SegmentText, RTL_TEXTENCODING_UTF8);
    return g_bytes_new(sText.getStr(), sText.getLength());
}

static unsigned int lo_accessible_text_get_caret_position(GtkAccessibleText* self)
{
    css::uno::Reference<css::accessibility::XAccessibleText> xText = getXText(self);
    if (!xText.is())
        return 0;

    return std::max(0, xText->getCaretPosition());
}

static gboolean lo_accessible_text_get_selection(GtkAccessibleText* self, gsize* n_ranges,
                                                 GtkAccessibleTextRange** ranges)
{
    css::uno::Reference<css::accessibility::XAccessibleText> xText = getXText(self);
    if (!xText.is())
        return 0;

    if (xText->getSelectedText().isEmpty())
        return false;

    const sal_Int32 nSelectionStart = xText->getSelectionStart();
    const sal_Int32 nSelectionEnd = xText->getSelectionEnd();

    *n_ranges = 1;
    *ranges = g_new(GtkAccessibleTextRange, 1);
    (*ranges)[0].start = std::min(nSelectionStart, nSelectionEnd);
    (*ranges)[0].length = std::abs(nSelectionEnd - nSelectionStart);
    return true;
}

static gboolean lo_accessible_text_get_attributes(GtkAccessibleText* /* self */,
                                                  unsigned int /* offset */, gsize* /* n_ranges */,
                                                  GtkAccessibleTextRange** /* ranges */,
                                                  char*** /* attribute_names */,
                                                  char*** /* attribute_values */)
{
    // TODO: implement
    return false;
}

void lo_accessible_text_init(GtkAccessibleTextInterface* iface)
{
    iface->get_contents = lo_accessible_text_get_contents;
    iface->get_contents_at = lo_accessible_text_get_contents_at;
    iface->get_caret_position = lo_accessible_text_get_caret_position;
    iface->get_selection = lo_accessible_text_get_selection;
    iface->get_attributes = lo_accessible_text_get_attributes;
}

#endif

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/unx/gtk4/gtkaccessibletext.hxx b/vcl/unx/gtk4/gtkaccessibletext.hxx
new file mode 100644
index 0000000..3e8a08d
--- /dev/null
+++ b/vcl/unx/gtk4/gtkaccessibletext.hxx
@@ -0,0 +1,20 @@
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This file is part of the LibreOffice project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

#pragma once

#include <gtk/gtk.h>

#if GTK_CHECK_VERSION(4, 13, 8)

void lo_accessible_text_init(GtkAccessibleTextInterface* iface);

#endif

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */