tdf#158030 qt a11y: Implement new QAccessibleAttributesInterface

Implement the new `QAccessibleAttributesInterface`
just added upstream to Qt in qtbase commit [1]

    commit fb5ffe862688a87cfc136113e067bcba0c49a7ae
    Author:     Michael Weghorn <m.weghorn@posteo.de>
    AuthorDate: Fri Nov 10 18:25:02 2023 +0100
    Commit:     Volker Hilsheimer <volker.hilsheimer@qt.io>
    CommitDate: Thu Feb 29 04:44:22 2024 +0000

        a11y: Add new QAccessibleAttributesInterface

, see also QTBUG-119057. [2]
This API is available with Qt >= 6.8.

That interface makes it possible to report
object attributes that are bridged to the platform a11y
layers.
Use it to bridge the attributes retrieved from
the `XAccessibleExtendedAttributes` UNO interface.

Together with the pending upstream qtbase change that
implements the AT-SPI bridge for Linux [3], the "level"
AT-SPI object attribute for headings in Writer
is correctly reported to AT-SPI, making the
Orca screen reader announce "Heading level N" as expected.

For now, map not explicitly handled attributes
as key-value pairs to Qt via the special
`QAccessible::Attribute::Custom` attribute,
which causes them to be mapped to AT-SPI unchanged, and
can e.g. be used for testing the tdf#158030 scenario.

For common attributes - like those specified in the
Core Accessibility API Mappings specification [4] -
suggesting to add new enum values to the
`QAccessible::Attribute` enum to upstream Qt
and using those instead should be considered for the future.

Related commit for gtk4:

    commit 3aca2d9776a871f15009a1aa70628ba3a03ee147
    Author: Michael Weghorn <m.weghorn@posteo.de>
    Date:   Thu Nov 9 15:31:57 2023 +0100

        gtk4 a11y: Handle the "level" object attribute

[1] https://code.qt.io/cgit/qt/qtbase.git/commit/?id=fb5ffe862688a87cfc136113e067bcba0c49a7ae
[2] https://bugreports.qt.io/browse/QTBUG-119057
[3] https://codereview.qt-project.org/c/qt/qtbase/+/517526
[4] https://www.w3.org/TR/core-aam-1.2/

Change-Id: Ibe357bfd72bb2dc6e44ad941e62737d5cac21e1c
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/159309
Tested-by: Jenkins
Reviewed-by: Michael Weghorn <m.weghorn@posteo.de>
diff --git a/vcl/inc/qt5/QtAccessibleWidget.hxx b/vcl/inc/qt5/QtAccessibleWidget.hxx
index 8d71ecd..46d7be2 100644
--- a/vcl/inc/qt5/QtAccessibleWidget.hxx
+++ b/vcl/inc/qt5/QtAccessibleWidget.hxx
@@ -38,6 +38,9 @@ class QtWidget;

class QtAccessibleWidget final : public QAccessibleInterface,
                                 public QAccessibleActionInterface,
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
                                 public QAccessibleAttributesInterface,
#endif
                                 public QAccessibleTextInterface,
                                 public QAccessibleEditableTextInterface,
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
@@ -85,6 +88,15 @@ public:
    void doAction(const QString& actionName) override;
    QStringList keyBindingsForAction(const QString& actionName) const override;

#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
    // helper method for QAccessibleAttributesInterface
    QHash<QAccessible::Attribute, QVariant> attributes() const;

    // QAccessibleAttributesInterface
    QList<QAccessible::Attribute> attributeKeys() const override;
    QVariant attributeValue(QAccessible::Attribute key) const override;
#endif

    // QAccessibleTextInterface
    void addSelection(int startOffset, int endOffset) override;
    QString attributes(int offset, int* startOffset, int* endOffset) const override;
diff --git a/vcl/qt5/QtAccessibleWidget.cxx b/vcl/qt5/QtAccessibleWidget.cxx
index 7eadc33..790e2009 100644
--- a/vcl/qt5/QtAccessibleWidget.cxx
+++ b/vcl/qt5/QtAccessibleWidget.cxx
@@ -39,6 +39,7 @@
#include <com/sun/star/accessibility/XAccessibleEditableText.hpp>
#include <com/sun/star/accessibility/XAccessibleEventBroadcaster.hpp>
#include <com/sun/star/accessibility/XAccessibleEventListener.hpp>
#include <com/sun/star/accessibility/XAccessibleExtendedAttributes.hpp>
#include <com/sun/star/accessibility/XAccessibleKeyBinding.hpp>
#include <com/sun/star/accessibility/XAccessibleRelationSet.hpp>
#include <com/sun/star/accessibility/XAccessibleSelection.hpp>
@@ -742,6 +743,10 @@ void* QtAccessibleWidget::interface_cast(QAccessible::InterfaceType t)
    if (t == QAccessible::SelectionInterface && accessibleProvidesInterface<XAccessibleSelection>())
        return static_cast<QAccessibleSelectionInterface*>(this);
#endif
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
    if (t == QAccessible::AttributesInterface)
        return static_cast<QAccessibleAttributesInterface*>(this);
#endif
    return nullptr;
}

@@ -855,6 +860,78 @@ QStringList QtAccessibleWidget::keyBindingsForAction(const QString& actionName) 
    return keyBindings;
}

#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)

// QAccessibleAttributesInterface helpers
namespace
{
void lcl_insertAttribute(QHash<QAccessible::Attribute, QVariant>& rQtAttrs, const OUString& rName,
                         const OUString& rValue)
{
    if (rName == u"level"_ustr)
    {
        rQtAttrs.insert(QAccessible::Attribute::Level,
                        QVariant::fromValue(static_cast<int>(rValue.toInt32())));
    }
    else
    {
        // for now, leave not explicitly handled attributes as they are and report
        // via QAccessible::Attribute::Custom, but should consider suggesting to
        // add more specific attributes on Qt side and use those instead
        const QVariant aVariant = rQtAttrs.value(QAccessible::Attribute::Custom,
                                                 QVariant::fromValue(QHash<QString, QString>()));
        assert((aVariant.canConvert<QHash<QString, QString>>()));
        QHash<QString, QString> aAttrs = aVariant.value<QHash<QString, QString>>();
        aAttrs.insert(toQString(rName), toQString(rValue));
        rQtAttrs.insert(QAccessible::Attribute::Custom, QVariant::fromValue(aAttrs));
    }
}
}

QHash<QAccessible::Attribute, QVariant> QtAccessibleWidget::attributes() const
{
    Reference<XAccessibleContext> xContext = getAccessibleContextImpl();
    if (!xContext.is())
        return {};

    Reference<XAccessibleExtendedAttributes> xAttributes(xContext, UNO_QUERY);
    if (!xAttributes.is())
        return {};

    OUString sAttrs;
    xAttributes->getExtendedAttributes() >>= sAttrs;

    QHash<QAccessible::Attribute, QVariant> aQtAttrs;
    sal_Int32 nIndex = 0;
    do
    {
        const OUString sAttribute = sAttrs.getToken(0, ';', nIndex);
        sal_Int32 nColonPos = 0;
        const OUString sName = sAttribute.getToken(0, ':', nColonPos);
        const OUString sValue = sAttribute.getToken(0, ':', nColonPos);
        assert(nColonPos == -1
               && "Too many colons in attribute that should have \"name:value\" syntax");
        if (!sName.isEmpty())
            lcl_insertAttribute(aQtAttrs, sName, sValue);
    } while (nIndex >= 0);

    return aQtAttrs;
}

// QAccessibleAttributesInterface
QList<QAccessible::Attribute> QtAccessibleWidget::attributeKeys() const
{
    const QHash<QAccessible::Attribute, QVariant> aAttributes = attributes();
    return aAttributes.keys();
}

QVariant QtAccessibleWidget::attributeValue(QAccessible::Attribute eAttribute) const
{
    const QHash<QAccessible::Attribute, QVariant> aAllAttributes = attributes();
    return aAllAttributes.value(eAttribute);
}
#endif

// QAccessibleTextInterface
void QtAccessibleWidget::addSelection(int /* startOffset */, int /* endOffset */)
{