1
0
Fork 0
libreoffice/vcl/qa/cppunit/a11y/atspi2/atspi2text.cxx
Daniel Baumann 8e63e14cf6
Adding upstream version 4:25.2.3.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-22 16:20:04 +02:00

1017 lines
44 KiB
C++

/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
/*
* 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/AccessibleRelationType.hpp>
#include <com/sun/star/accessibility/AccessibleTextType.hpp>
#include <com/sun/star/accessibility/XAccessibleComponent.hpp>
#include <com/sun/star/accessibility/XAccessibleText.hpp>
#include <com/sun/star/accessibility/XAccessibleTextAttributes.hpp>
#include <com/sun/star/accessibility/XAccessibleTextMarkup.hpp>
#include <com/sun/star/awt/FontSlant.hpp>
#include <com/sun/star/awt/FontStrikeout.hpp>
#include <com/sun/star/awt/FontUnderline.hpp>
#include <com/sun/star/style/CaseMap.hpp>
#include <com/sun/star/style/LineSpacing.hpp>
#include <com/sun/star/style/LineSpacingMode.hpp>
#include <com/sun/star/style/ParagraphAdjust.hpp>
#include <com/sun/star/style/TabStop.hpp>
#include <com/sun/star/text/FontRelief.hpp>
#include <com/sun/star/text/WritingMode2.hpp>
#include <com/sun/star/text/TextMarkupType.hpp>
#include <i18nlangtag/languagetag.hxx>
#include <tools/UnitConversion.hxx>
#include <rtl/character.hxx>
#include <test/a11y/AccessibilityTools.hxx>
#include "atspi2.hxx"
#include "atspiwrapper.hxx"
using namespace css;
namespace
{
/** @brief Helper class to check text attributes are properly exported to Atspi.
*
* This kind of duplicates most of the logic in atktextattributes.cxx, but if we want to check the
* values are correct (which includes whether they are properly updated for example), we have to do
* this, even though it means quite some processing for some of the attributes.
* This has to be kept in sync with how atktextattributes.cxx exposes those attributes. */
class AttributesChecker
{
private:
uno::Reference<accessibility::XAccessibleText> mxLOText;
Atspi::Text mxAtspiText;
public:
AttributesChecker(const uno::Reference<accessibility::XAccessibleText>& xLOText,
const Atspi::Text& xAtspiText)
: mxLOText(xLOText)
, mxAtspiText(xAtspiText)
{
}
private:
// helper to validate a value represented as a single float in ATSPI
static bool implCheckFloat(std::string_view atspiValue, float expected)
{
float f;
char dummy;
CPPUNIT_ASSERT_EQUAL(1, sscanf(atspiValue.data(), "%g%c", &f, &dummy));
CPPUNIT_ASSERT_DOUBLES_EQUAL(expected, f, 1e-4);
return true;
}
// helper to check simple mappings between LO and ATSPI
template <typename T>
static bool implCheckMapping(const T loValue, const std::string_view atspiValue,
const std::unordered_map<T, std::string_view>& map,
const bool retIfMissing = false)
{
const auto& iter = map.find(loValue);
if (iter != map.end())
{
CPPUNIT_ASSERT_EQUAL(iter->second, atspiValue);
return true;
}
return retIfMissing;
}
// checkers, see atktextattributes.cxx
bool checkBoolean(std::string_view atspiValue, const beans::PropertyValue& property,
const uno::Sequence<beans::PropertyValue>&)
{
if (property.Value.get<bool>())
CPPUNIT_ASSERT_EQUAL(std::string_view("true"), atspiValue);
else
CPPUNIT_ASSERT_EQUAL(std::string_view("false"), atspiValue);
return true;
}
bool checkString(std::string_view atspiValue, const beans::PropertyValue& property,
const uno::Sequence<beans::PropertyValue>&)
{
CPPUNIT_ASSERT_EQUAL(property.Value.get<OUString>(), OUString::fromUtf8(atspiValue));
return true;
}
bool checkFloat(std::string_view atspiValue, const beans::PropertyValue& property,
const uno::Sequence<beans::PropertyValue>&)
{
return implCheckFloat(atspiValue, property.Value.get<float>());
}
bool checkVariant(std::string_view atspiValue, const beans::PropertyValue& property,
const uno::Sequence<beans::PropertyValue>&)
{
if (property.Value.get<short>() == style::CaseMap::SMALLCAPS)
CPPUNIT_ASSERT_EQUAL(std::string_view("small_caps"), atspiValue);
else
CPPUNIT_ASSERT_EQUAL(std::string_view("normal"), atspiValue);
return true;
}
// See Scale2String
bool checkScale(std::string_view atspiValue, const beans::PropertyValue& property,
const uno::Sequence<beans::PropertyValue>&)
{
double v;
char dummy;
CPPUNIT_ASSERT_EQUAL(1, sscanf(atspiValue.data(), "%lg%c", &v, &dummy));
CPPUNIT_ASSERT_EQUAL(property.Value.get<sal_Int16>(), sal_Int16(v * 100));
return true;
}
// see Escapement2VerticalAlign
bool checkVerticalAlign(std::string_view atspiValue, const beans::PropertyValue& property,
const uno::Sequence<beans::PropertyValue>&)
{
const sal_Int16 n = property.Value.get<sal_Int16>();
if (n == 0)
CPPUNIT_ASSERT_EQUAL(std::string_view("baseline"), atspiValue);
else if (n == -101)
CPPUNIT_ASSERT_EQUAL(std::string_view("sub"), atspiValue);
else if (n == 101)
CPPUNIT_ASSERT_EQUAL(std::string_view("super"), atspiValue);
else
{
int v;
char dummy;
CPPUNIT_ASSERT_EQUAL(1, sscanf(atspiValue.data(), "%d%%%c", &v, &dummy));
CPPUNIT_ASSERT_EQUAL(int(n), v);
}
return true;
}
bool checkColor(std::string_view atspiValue, const beans::PropertyValue& property,
const uno::Sequence<beans::PropertyValue>&)
{
auto color = property.Value.get<sal_Int32>();
if (color == -1) // automatic, use the component's color
{
uno::Reference<accessibility::XAccessibleComponent> xComponent(mxLOText,
uno::UNO_QUERY);
if (xComponent.is())
{
if (property.Name == u"CharBackColor")
color = xComponent->getBackground();
else if (property.Name == u"CharColor")
color = xComponent->getForeground();
}
}
if (color != -1)
{
unsigned int r, g, b;
char dummy;
CPPUNIT_ASSERT_EQUAL(3, sscanf(atspiValue.data(), "%u,%u,%u%c", &r, &g, &b, &dummy));
CPPUNIT_ASSERT_EQUAL((color & 0xFFFFFF),
(static_cast<sal_Int32>(r) << 16 | static_cast<sal_Int32>(g) << 8
| static_cast<sal_Int32>(b)));
return true;
}
return false;
}
// See LineSpacing2LineHeight
bool checkLineHeight(std::string_view atspiValue, const beans::PropertyValue& property,
const uno::Sequence<beans::PropertyValue>&)
{
const auto lineSpacing = property.Value.get<style::LineSpacing>();
char dummy;
if (lineSpacing.Mode == style::LineSpacingMode::PROP)
{
int h;
CPPUNIT_ASSERT_EQUAL(1, sscanf(atspiValue.data(), "%d%%%c", &h, &dummy));
CPPUNIT_ASSERT_EQUAL(lineSpacing.Height, sal_Int16(h));
}
else if (lineSpacing.Mode == style::LineSpacingMode::FIX)
{
double pt;
CPPUNIT_ASSERT_EQUAL(1, sscanf(atspiValue.data(), "%lgpt%c", &pt, &dummy));
CPPUNIT_ASSERT_DOUBLES_EQUAL(convertMm100ToPoint<double>(lineSpacing.Height), pt, 1e-4);
CPPUNIT_ASSERT_EQUAL(lineSpacing.Height, sal_Int16(convertPointToMm100(pt)));
}
else
return false;
return true;
}
bool checkStretch(std::string_view atspiValue, const beans::PropertyValue& property,
const uno::Sequence<beans::PropertyValue>&)
{
const auto n = property.Value.get<sal_Int16>();
if (n < 0)
CPPUNIT_ASSERT_EQUAL(std::string_view("condensed"), atspiValue);
else if (n > 0)
CPPUNIT_ASSERT_EQUAL(std::string_view("expanded"), atspiValue);
else
CPPUNIT_ASSERT_EQUAL(std::string_view("normal"), atspiValue);
return true;
}
bool checkStyle(std::string_view atspiValue, const beans::PropertyValue& property,
const uno::Sequence<beans::PropertyValue>&)
{
return implCheckMapping(
property.Value.get<awt::FontSlant>(), atspiValue,
{ { awt::FontSlant_NONE, std::string_view("normal") },
{ awt::FontSlant_OBLIQUE, std::string_view("oblique") },
{ awt::FontSlant_ITALIC, std::string_view("italic") },
{ awt::FontSlant_REVERSE_OBLIQUE, std::string_view("reverse oblique") },
{ awt::FontSlant_REVERSE_ITALIC, std::string_view("reverse italic") } });
}
bool checkJustification(std::string_view atspiValue, const beans::PropertyValue& property,
const uno::Sequence<beans::PropertyValue>&)
{
return implCheckMapping(static_cast<style::ParagraphAdjust>(property.Value.get<short>()),
atspiValue,
{ { style::ParagraphAdjust_LEFT, std::string_view("left") },
{ style::ParagraphAdjust_RIGHT, std::string_view("right") },
{ style::ParagraphAdjust_BLOCK, std::string_view("fill") },
{ style::ParagraphAdjust_STRETCH, std::string_view("fill") },
{ style::ParagraphAdjust_CENTER, std::string_view("center") } });
}
bool checkShadow(std::string_view atspiValue, const beans::PropertyValue& property,
const uno::Sequence<beans::PropertyValue>&)
{
if (property.Value.get<bool>())
CPPUNIT_ASSERT_EQUAL(std::string_view("black"), atspiValue);
else
CPPUNIT_ASSERT_EQUAL(std::string_view("none"), atspiValue);
return true;
}
bool checkLanguage(std::string_view atspiValue, const beans::PropertyValue& property,
const uno::Sequence<beans::PropertyValue>&)
{
auto aLocale = property.Value.get<lang::Locale>();
LanguageTag aLanguageTag(aLocale);
CPPUNIT_ASSERT_EQUAL(OUString(aLanguageTag.getLanguage() + "-"
+ aLanguageTag.getCountry().toAsciiLowerCase()),
OUString::fromUtf8(atspiValue));
return true;
}
bool checkTextRotation(std::string_view atspiValue, const beans::PropertyValue& property,
const uno::Sequence<beans::PropertyValue>&)
{
return implCheckFloat(atspiValue, property.Value.get<sal_Int16>() / 10.0f);
}
bool checkWeight(std::string_view atspiValue, const beans::PropertyValue& property,
const uno::Sequence<beans::PropertyValue>&)
{
return implCheckFloat(atspiValue, property.Value.get<float>() * 4);
}
bool checkCMMValue(std::string_view atspiValue, const beans::PropertyValue& property,
const uno::Sequence<beans::PropertyValue>&)
{
double v;
char dummy;
// CMM is 1/100th of a mm
CPPUNIT_ASSERT_EQUAL(1, sscanf(atspiValue.data(), "%lgmm%c", &v, &dummy));
CPPUNIT_ASSERT_DOUBLES_EQUAL(property.Value.get<sal_Int32>() * 0.01, v, 1e-4);
return true;
}
bool checkDirection(std::string_view atspiValue, const beans::PropertyValue& property,
const uno::Sequence<beans::PropertyValue>&)
{
return implCheckMapping(property.Value.get<sal_Int16>(), atspiValue,
{ { text::WritingMode2::TB_LR, std::string_view("ltr") },
{ text::WritingMode2::LR_TB, std::string_view("ltr") },
{ text::WritingMode2::TB_RL, std::string_view("rtl") },
{ text::WritingMode2::RL_TB, std::string_view("rtl") },
{ text::WritingMode2::PAGE, std::string_view("none") } });
}
bool checkWritingMode(std::string_view atspiValue, const beans::PropertyValue& property,
const uno::Sequence<beans::PropertyValue>&)
{
return implCheckMapping(property.Value.get<sal_Int16>(), atspiValue,
{ { text::WritingMode2::TB_LR, std::string_view("tb-lr") },
{ text::WritingMode2::LR_TB, std::string_view("lr-tb") },
{ text::WritingMode2::TB_RL, std::string_view("tb-rl") },
{ text::WritingMode2::RL_TB, std::string_view("rl-tb") },
{ text::WritingMode2::PAGE, std::string_view("none") } });
}
static const beans::PropertyValue*
findProperty(const uno::Sequence<beans::PropertyValue>& properties, std::u16string_view name)
{
auto prop = std::find_if(properties.begin(), properties.end(),
[name](auto& p) { return p.Name == name; });
if (prop == properties.end())
prop = nullptr;
return prop;
}
// same as findProperty() above, but with a fast path is @p property is a match
static const beans::PropertyValue*
findProperty(const beans::PropertyValue* property,
const uno::Sequence<beans::PropertyValue>& properties, std::u16string_view name)
{
if (property->Name == name)
return property;
return findProperty(properties, name);
}
bool checkFontEffect(std::string_view atspiValue, const beans::PropertyValue& property,
const uno::Sequence<beans::PropertyValue>& loProperties)
{
if (auto charContoured = findProperty(&property, loProperties, u"CharContoured");
charContoured && charContoured->Value.get<bool>())
{
CPPUNIT_ASSERT_EQUAL(std::string_view("outline"), atspiValue);
return true;
}
if (auto charRelief = findProperty(&property, loProperties, u"CharRelief"))
{
return implCheckMapping(charRelief->Value.get<sal_Int16>(), atspiValue,
{ { text::FontRelief::NONE, std::string_view("none") },
{ text::FontRelief::EMBOSSED, std::string_view("emboss") },
{ text::FontRelief::ENGRAVED, std::string_view("engrave") } },
true);
}
return false;
}
bool checkTextDecoration(std::string_view atspiValue, const beans::PropertyValue&,
const uno::Sequence<beans::PropertyValue>& loProperties)
{
if (atspiValue == "none")
{
if (auto prop = findProperty(loProperties, u"CharFlash"))
CPPUNIT_ASSERT_EQUAL(false, prop->Value.get<bool>());
if (auto prop = findProperty(loProperties, u"CharUnderline"))
CPPUNIT_ASSERT_EQUAL(css::awt::FontUnderline::NONE, prop->Value.get<sal_Int16>());
if (auto prop = findProperty(loProperties, u"CharStrikeout"))
CPPUNIT_ASSERT(prop->Value.get<sal_Int16>() == css::awt::FontStrikeout::NONE
|| prop->Value.get<sal_Int16>()
== css::awt::FontStrikeout::DONTKNOW);
}
else
{
sal_Int32 nIndex = 0;
const auto atspiValueString = OUString::fromUtf8(atspiValue);
do
{
OUString atspiToken = atspiValueString.getToken(0, ' ', nIndex);
const beans::PropertyValue* prop;
if (atspiToken == "blink")
{
CPPUNIT_ASSERT((prop = findProperty(loProperties, u"CharFlash")));
CPPUNIT_ASSERT_EQUAL(true, prop->Value.get<bool>());
}
else if (atspiToken == "underline")
{
CPPUNIT_ASSERT((prop = findProperty(loProperties, u"CharUnderline")));
CPPUNIT_ASSERT(prop->Value.get<sal_Int16>() != css::awt::FontUnderline::NONE);
}
else if (atspiToken == "underline")
{
CPPUNIT_ASSERT((prop = findProperty(loProperties, u"CharStrikeout")));
CPPUNIT_ASSERT(prop->Value.get<sal_Int16>() != css::awt::FontStrikeout::NONE);
CPPUNIT_ASSERT(prop->Value.get<sal_Int16>()
!= css::awt::FontStrikeout::DONTKNOW);
}
else
{
CPPUNIT_ASSERT_MESSAGE(
OUString("Unknown text decoration \"" + atspiToken).toUtf8().getStr(),
false);
}
} while (nIndex > 0);
}
return true;
}
static bool implCheckTabStops(std::string_view atspiValue, const beans::PropertyValue& property,
const bool defaultTabs)
{
uno::Sequence<style::TabStop> theTabStops;
if (property.Value >>= theTabStops)
{
sal_Unicode lastFillChar = ' ';
const char* p = atspiValue.data();
for (const auto& rTabStop : theTabStops)
{
if ((style::TabAlign_DEFAULT == rTabStop.Alignment) != defaultTabs)
continue;
const char* tab_align = "";
switch (rTabStop.Alignment)
{
case style::TabAlign_LEFT:
tab_align = "left ";
break;
case style::TabAlign_CENTER:
tab_align = "center ";
break;
case style::TabAlign_RIGHT:
tab_align = "right ";
break;
case style::TabAlign_DECIMAL:
tab_align = "decimal ";
break;
default:
break;
}
const char* lead_char = "";
if (rTabStop.FillChar != lastFillChar)
{
lastFillChar = rTabStop.FillChar;
switch (lastFillChar)
{
case ' ':
lead_char = "blank ";
break;
case '.':
lead_char = "dotted ";
break;
case '-':
lead_char = "dashed ";
break;
case '_':
lead_char = "lined ";
break;
default:
lead_char = "custom ";
break;
}
}
// check this matches "<lead_char><tab_align><position>mm"
CPPUNIT_ASSERT_EQUAL(0, strncmp(p, lead_char, strlen(lead_char)));
p += strlen(lead_char);
CPPUNIT_ASSERT_EQUAL(0, strncmp(p, tab_align, strlen(tab_align)));
p += strlen(tab_align);
float atspiPosition;
int nConsumed;
CPPUNIT_ASSERT_EQUAL(1, sscanf(p, "%gmm%n", &atspiPosition, &nConsumed));
CPPUNIT_ASSERT_DOUBLES_EQUAL(float(rTabStop.Position * 0.01f), atspiPosition, 1e-4);
p += nConsumed;
if (*p)
{
CPPUNIT_ASSERT_EQUAL(' ', *p);
p++;
}
}
// make sure there isn't garbage at the end
CPPUNIT_ASSERT_EQUAL(char(0), *p);
return true;
}
return false;
}
bool checkDefaultTabStops(std::string_view atspiValue, const beans::PropertyValue& property,
const uno::Sequence<beans::PropertyValue>&)
{
return implCheckTabStops(atspiValue, property, true);
}
bool checkTabStops(std::string_view atspiValue, const beans::PropertyValue& property,
const uno::Sequence<beans::PropertyValue>&)
{
return implCheckTabStops(atspiValue, property, false);
}
public:
// runner code
bool check(const uno::Sequence<beans::PropertyValue>& xLOAttributeList,
const std::unordered_map<std::string, std::string>& xAtspiAttributeList)
{
const struct
{
const char* loName;
const char* atspiName;
bool (AttributesChecker::*checkValue)(
std::string_view atspiValue, const beans::PropertyValue& property,
const uno::Sequence<beans::PropertyValue>& loAttributeList);
} atspiMap[]
= { // LO name AT-SPI name check function
{ "CharBackColor", "bg-color", &AttributesChecker::checkColor },
{ "CharCaseMap", "variant", &AttributesChecker::checkVariant },
{ "CharColor", "fg-color", &AttributesChecker::checkColor },
{ "CharContoured", "font-effect", &AttributesChecker::checkFontEffect },
{ "CharEscapement", "vertical-align", &AttributesChecker::checkVerticalAlign },
{ "CharFlash", "text-decoration", &AttributesChecker::checkTextDecoration },
{ "CharFontName", "family-name", &AttributesChecker::checkString },
{ "CharHeight", "size", &AttributesChecker::checkFloat },
{ "CharHidden", "invisible", &AttributesChecker::checkBoolean },
{ "CharKerning", "stretch", &AttributesChecker::checkStretch },
{ "CharLocale", "language", &AttributesChecker::checkLanguage },
{ "CharPosture", "style", &AttributesChecker::checkStyle },
{ "CharRelief", "font-effect", &AttributesChecker::checkFontEffect },
{ "CharRotation", "text-rotation", &AttributesChecker::checkTextRotation },
{ "CharScaleWidth", "scale", &AttributesChecker::checkScale },
{ "CharShadowed", "text-shadow", &AttributesChecker::checkShadow },
{ "CharStrikeout", "text-decoration", &AttributesChecker::checkTextDecoration },
{ "CharUnderline", "text-decoration", &AttributesChecker::checkTextDecoration },
{ "CharWeight", "weight", &AttributesChecker::checkWeight },
{ "MMToPixelRatio", "mm-to-pixel-ratio", &AttributesChecker::checkFloat },
{ "ParaAdjust", "justification", &AttributesChecker::checkJustification },
{ "ParaBottomMargin", "pixels-below-lines", &AttributesChecker::checkCMMValue },
{ "ParaFirstLineIndent", "indent", &AttributesChecker::checkCMMValue },
{ "ParaLeftMargin", "left-margin", &AttributesChecker::checkCMMValue },
{ "ParaLineSpacing", "line-height", &AttributesChecker::checkLineHeight },
{ "ParaRightMargin", "right-margin", &AttributesChecker::checkCMMValue },
{ "ParaStyleName", "paragraph-style", &AttributesChecker::checkString },
{ "ParaTabStops", "tab-interval", &AttributesChecker::checkDefaultTabStops },
{ "ParaTabStops", "tab-stops", &AttributesChecker::checkTabStops },
{ "ParaTopMargin", "pixels-above-lines", &AttributesChecker::checkCMMValue },
{ "WritingMode", "direction", &AttributesChecker::checkDirection },
{ "WritingMode", "writing-mode", &AttributesChecker::checkWritingMode }
};
for (const auto& prop : xLOAttributeList)
{
std::cout << "found run attribute: " << prop.Name << "=" << prop.Value << std::endl;
/* we need to loop on all entries because there might be more than one for a single
* property */
for (const auto& entry : atspiMap)
{
if (!prop.Name.equalsAscii(entry.loName))
continue;
const auto atspiIter = xAtspiAttributeList.find(entry.atspiName);
/* we use an empty value if there isn't one, which can happen if the value cannot
* be represented by Atspi, or if the actual LO value is also empty */
std::string atspiValue;
if (atspiIter != xAtspiAttributeList.end())
atspiValue = atspiIter->second;
std::cout << " matching atspi attribute is: " << entry.atspiName << "="
<< atspiValue << std::endl;
CPPUNIT_ASSERT(
std::invoke(entry.checkValue, this, atspiValue, prop, xLOAttributeList));
}
}
return true;
}
};
}
/* LO doesn't implement it itself, but ATK provides a fallback. Add a test here merely for the
* future when we have a direct AT-SPI implementation for e.g. GTK4.
* Just like atk-adaptor, we compute the bounding box by combining extents for each character
* in the range */
static awt::Rectangle getRangeBounds(const uno::Reference<accessibility::XAccessibleText>& xText,
sal_Int32 startOffset, sal_Int32 endOffset)
{
awt::Rectangle bounds;
for (auto offset = startOffset; offset < endOffset; offset++)
{
const auto chBounds = xText->getCharacterBounds(offset);
if (offset == 0)
bounds = chBounds;
else
{
const auto x = std::min(bounds.X, chBounds.X);
const auto y = std::min(bounds.Y, chBounds.Y);
bounds.Width = std::max(bounds.X + bounds.Width, chBounds.X + chBounds.Width) - x;
bounds.Height = std::max(bounds.Y + bounds.Height, chBounds.Y + chBounds.Height) - y;
bounds.X = x;
bounds.Y = y;
}
}
return bounds;
}
void Atspi2TestTree::compareTextObjects(
const uno::Reference<accessibility::XAccessibleText>& xLOText, const Atspi::Text& pAtspiText)
{
CPPUNIT_ASSERT_EQUAL(xLOText->getCharacterCount(), sal_Int32(pAtspiText.getCharacterCount()));
CPPUNIT_ASSERT_EQUAL(xLOText->getCaretPosition(), sal_Int32(pAtspiText.getCaretOffset()));
CPPUNIT_ASSERT_EQUAL(xLOText->getText(), OUString::fromUtf8(pAtspiText.getText(0, -1)));
const auto characterCount = xLOText->getCharacterCount();
auto offset = decltype(characterCount){ 0 };
auto atspiPosition = Atspi::Point{ 0, 0 };
AttributesChecker attributesChecker(xLOText, pAtspiText);
auto xLOTextAttrs
= uno::Reference<accessibility::XAccessibleTextAttributes>(xLOText, uno::UNO_QUERY);
// default text attributes
if (xLOTextAttrs.is())
{
const auto aAttributeList = xLOTextAttrs->getDefaultAttributes(uno::Sequence<OUString>());
const auto atspiAttributeList = pAtspiText.getDefaultAttributes();
attributesChecker.check(aAttributeList, atspiAttributeList);
}
if (characterCount > 0)
{
const auto atspiComponent = pAtspiText.queryComponent();
atspiPosition = atspiComponent.getPosition(ATSPI_COORD_TYPE_WINDOW);
}
// text run attributes
uno::Reference<accessibility::XAccessibleTextMarkup> xTextMarkup(xLOText, uno::UNO_QUERY);
while (offset < characterCount)
{
// message for the assertions so we know where it comes from
OString offsetMsg(OString::Concat("in ") + AccessibilityTools::debugString(xLOText).c_str()
+ " at offset " + OString::number(offset));
uno::Sequence<beans::PropertyValue> aAttributeList;
if (xLOTextAttrs.is())
aAttributeList = xLOTextAttrs->getRunAttributes(offset, uno::Sequence<OUString>());
else
aAttributeList = xLOText->getCharacterAttributes(offset, uno::Sequence<OUString>());
int atspiStartOffset = 0, atspiEndOffset = 0;
const auto atspiAttributeList
= pAtspiText.getAttributeRun(offset, false, &atspiStartOffset, &atspiEndOffset);
accessibility::TextSegment aTextSegment
= xLOText->getTextAtIndex(offset, accessibility::AccessibleTextType::ATTRIBUTE_RUN);
/* Handle misspelled text and tracked changes as atktext.cxx does as it affects the run
* boundaries. Also check the attributes are properly forwarded. */
if (xTextMarkup.is())
{
const struct
{
sal_Int32 markupType;
const char* atspiAttribute;
const char* atspiValue;
} aTextMarkupTypes[]
= { { text::TextMarkupType::SPELLCHECK, "text-spelling", "misspelled" },
{ text::TextMarkupType::TRACK_CHANGE_INSERTION, "text-tracked-change",
"insertion" },
{ text::TextMarkupType::TRACK_CHANGE_DELETION, "text-tracked-change",
"deletion" },
{ text::TextMarkupType::TRACK_CHANGE_FORMATCHANGE, "text-tracked-change",
"attribute-change" } };
for (const auto& aTextMarkupType : aTextMarkupTypes)
{
const auto nTextMarkupCount
= xTextMarkup->getTextMarkupCount(aTextMarkupType.markupType);
if (nTextMarkupCount <= 0)
continue;
for (auto nTextMarkupIndex = decltype(nTextMarkupCount){ 0 };
nTextMarkupIndex < nTextMarkupCount; ++nTextMarkupIndex)
{
const auto aMarkupTextSegment
= xTextMarkup->getTextMarkup(nTextMarkupIndex, aTextMarkupType.markupType);
if (aMarkupTextSegment.SegmentStart > offset)
{
aTextSegment.SegmentEnd
= ::std::min(aTextSegment.SegmentEnd, aMarkupTextSegment.SegmentStart);
break; // no further iteration.
}
else if (offset < aMarkupTextSegment.SegmentEnd)
{
// text markup at <offset>
aTextSegment.SegmentStart = ::std::max(aTextSegment.SegmentStart,
aMarkupTextSegment.SegmentStart);
aTextSegment.SegmentEnd
= ::std::min(aTextSegment.SegmentEnd, aMarkupTextSegment.SegmentEnd);
// check the attribute is set
const auto atspiIter
= atspiAttributeList.find(aTextMarkupType.atspiAttribute);
CPPUNIT_ASSERT_MESSAGE(offsetMsg.getStr(),
atspiIter != atspiAttributeList.end());
CPPUNIT_ASSERT_EQUAL_MESSAGE(offsetMsg.getStr(),
std::string_view(aTextMarkupType.atspiValue),
std::string_view(atspiIter->second));
break; // no further iteration needed.
}
else
{
aTextSegment.SegmentStart
= ::std::max(aTextSegment.SegmentStart, aMarkupTextSegment.SegmentEnd);
// continue iteration.
}
}
}
}
CPPUNIT_ASSERT_EQUAL_MESSAGE(offsetMsg.getStr(), aTextSegment.SegmentStart,
sal_Int32(atspiStartOffset));
CPPUNIT_ASSERT_EQUAL_MESSAGE(offsetMsg.getStr(), aTextSegment.SegmentEnd,
sal_Int32(atspiEndOffset));
attributesChecker.check(aAttributeList, atspiAttributeList);
CPPUNIT_ASSERT_MESSAGE(offsetMsg.getStr(), aTextSegment.SegmentEnd > offset);
offset = aTextSegment.SegmentEnd;
}
// loop over each character
for (offset = 0; offset < characterCount;)
{
const auto aTextSegment
= xLOText->getTextAtIndex(offset, accessibility::AccessibleTextType::CHARACTER);
OString offsetMsg(OString::Concat("in ") + AccessibilityTools::debugString(xLOText).c_str()
+ " at offset " + OString::number(offset));
// getCharacterAtOffset()
sal_Int32 nChOffset = 0;
sal_Int32 cp = aTextSegment.SegmentText.iterateCodePoints(&nChOffset);
/* do not check unpaired surrogates, because they are unlikely to make any sense and LO's
* GTK VCL doesn't like them */
if (!rtl::isSurrogate(cp))
CPPUNIT_ASSERT_EQUAL_MESSAGE(offsetMsg.getStr(), cp,
pAtspiText.getCharacterAtOffset(offset));
// getTextAtOffset()
const struct
{
sal_Int16 loTextType;
AtspiTextBoundaryType atspiBoundaryType;
} textTypeMap[] = {
{ accessibility::AccessibleTextType::CHARACTER, ATSPI_TEXT_BOUNDARY_CHAR },
{ accessibility::AccessibleTextType::WORD, ATSPI_TEXT_BOUNDARY_WORD_START },
{ accessibility::AccessibleTextType::SENTENCE, ATSPI_TEXT_BOUNDARY_SENTENCE_START },
{ accessibility::AccessibleTextType::LINE, ATSPI_TEXT_BOUNDARY_LINE_START },
};
for (const auto& pair : textTypeMap)
{
auto loTextSegment = xLOText->getTextAtIndex(offset, pair.loTextType);
const auto atspiTextRange = pAtspiText.getTextAtOffset(offset, pair.atspiBoundaryType);
// for WORD there's adjustments to be made, see atktext.cxx:adjust_boundaries()
if (pair.loTextType == accessibility::AccessibleTextType::WORD
&& !loTextSegment.SegmentText.isEmpty())
{
// Determine the start index of the next segment
const auto loTextSegmentBehind
= xLOText->getTextBehindIndex(loTextSegment.SegmentEnd, pair.loTextType);
if (!loTextSegmentBehind.SegmentText.isEmpty())
loTextSegment.SegmentEnd = loTextSegmentBehind.SegmentStart;
else
loTextSegment.SegmentEnd = xLOText->getCharacterCount();
loTextSegment.SegmentText
= xLOText->getTextRange(loTextSegment.SegmentStart, loTextSegment.SegmentEnd);
}
OString boundaryMsg(offsetMsg + " with boundary type "
+ Atspi::TextBoundaryType::getName(pair.atspiBoundaryType).c_str());
CPPUNIT_ASSERT_EQUAL_MESSAGE(boundaryMsg.getStr(), loTextSegment.SegmentText,
OUString::fromUtf8(atspiTextRange.content));
/* if the segment is empty, LO API gives -1 offsets, but maps to 0 for AT-SPI. This is
* fine, AT-SPI doesn't really say what the offsets should be when the text is empty */
if (!loTextSegment.SegmentText.isEmpty())
{
CPPUNIT_ASSERT_EQUAL_MESSAGE(boundaryMsg.getStr(), loTextSegment.SegmentStart,
sal_Int32(atspiTextRange.startOffset));
CPPUNIT_ASSERT_EQUAL_MESSAGE(boundaryMsg.getStr(), loTextSegment.SegmentEnd,
sal_Int32(atspiTextRange.endOffset));
}
}
// character bounds
const auto loRect = xLOText->getCharacterBounds(offset);
auto atspiRect = pAtspiText.getCharacterExtents(offset, ATSPI_COORD_TYPE_WINDOW);
atspiRect.x -= atspiPosition.x;
atspiRect.y -= atspiPosition.y;
CPPUNIT_ASSERT_EQUAL_MESSAGE(offsetMsg.getStr(), loRect.Y, sal_Int32(atspiRect.y));
CPPUNIT_ASSERT_EQUAL_MESSAGE(offsetMsg.getStr(), loRect.Height,
sal_Int32(atspiRect.height));
/* for some reason getCharacterBounds() might return negative widths in some cases
* (including a space at the end of a right-justified line), and ATK will then adjust
* the X and width values to positive to workaround RTL issues (see
* https://bugzilla.gnome.org/show_bug.cgi?id=102954), so we work around that */
if (loRect.Width < 0)
{
/* ATK will make x += width; width *= -1, but we don't really want to depend on the
* ATK behavior so we allow it to match as well */
CPPUNIT_ASSERT_MESSAGE(offsetMsg.getStr(),
loRect.X == sal_Int32(atspiRect.x)
|| loRect.X + loRect.Width == sal_Int32(atspiRect.x));
CPPUNIT_ASSERT_MESSAGE(offsetMsg.getStr(),
loRect.Width == sal_Int32(atspiRect.width)
|| -loRect.Width == sal_Int32(atspiRect.width));
}
else
{
// normal case
CPPUNIT_ASSERT_EQUAL_MESSAGE(offsetMsg.getStr(), loRect.X, sal_Int32(atspiRect.x));
CPPUNIT_ASSERT_EQUAL_MESSAGE(offsetMsg.getStr(), loRect.Width,
sal_Int32(atspiRect.width));
}
// indexAtPoint()
CPPUNIT_ASSERT_EQUAL_MESSAGE(
offsetMsg.getStr(), xLOText->getIndexAtPoint(awt::Point(loRect.X, loRect.Y)),
sal_Int32(pAtspiText.getOffsetAtPoint(
atspiPosition.x + loRect.X, atspiPosition.y + loRect.Y, ATSPI_COORD_TYPE_WINDOW)));
CPPUNIT_ASSERT_MESSAGE(offsetMsg.getStr(), aTextSegment.SegmentEnd > offset);
offset = aTextSegment.SegmentEnd;
}
// getRangeExtents() -- ATK doesn't like empty ranges, so only test when not empty
if (characterCount > 0)
{
const auto loRangeBounds = getRangeBounds(xLOText, 0, characterCount);
const auto atspiRangeExtents
= pAtspiText.getRangeExtents(0, characterCount, ATSPI_COORD_TYPE_WINDOW);
CPPUNIT_ASSERT_EQUAL(loRangeBounds.X, sal_Int32(atspiRangeExtents.x - atspiPosition.x));
CPPUNIT_ASSERT_EQUAL(loRangeBounds.Y, sal_Int32(atspiRangeExtents.y - atspiPosition.y));
CPPUNIT_ASSERT_EQUAL(loRangeBounds.Width, sal_Int32(atspiRangeExtents.width));
CPPUNIT_ASSERT_EQUAL(loRangeBounds.Height, sal_Int32(atspiRangeExtents.height));
}
// selection (LO only have one selection, so some of the API doesn't really make sense)
CPPUNIT_ASSERT_EQUAL(xLOText->getSelectionEnd() != xLOText->getSelectionStart() ? 1 : 0,
pAtspiText.getNSelections());
const auto atspiSelection = pAtspiText.getSelection(0);
CPPUNIT_ASSERT_EQUAL(xLOText->getSelectionStart(), sal_Int32(atspiSelection.startOffset));
CPPUNIT_ASSERT_EQUAL(xLOText->getSelectionEnd(), sal_Int32(atspiSelection.endOffset));
/* We need to take extra care with setSelection() because it could result to scrolling, which
* might result in node destruction, which can mess up the parent's children enumeration.
* So we only test nodes that are neither the first nor last child in its parent, hoping that
* means it won't require scrolling to show the end of the selection. */
uno::Reference<accessibility::XAccessibleContext> xLOContext(xLOText, uno::UNO_QUERY_THROW);
const auto nIndexInParent = xLOContext->getAccessibleIndexInParent();
if (characterCount && nIndexInParent > 0
&& nIndexInParent + 1 < xLOContext->getAccessibleParent()
->getAccessibleContext()
->getAccessibleChildCount()
&& pAtspiText.setSelection(0, 0, characterCount))
{
CPPUNIT_ASSERT_EQUAL(sal_Int32(0), xLOText->getSelectionStart());
CPPUNIT_ASSERT_EQUAL(characterCount, xLOText->getSelectionEnd());
// try and restore previous selection, if any
CPPUNIT_ASSERT(xLOText->setSelection(std::max(0, atspiSelection.startOffset),
std::max(0, atspiSelection.endOffset)));
}
// scrollSubstringTo() is tested in the parent, because it might dispose ourselves otherwise.
// TODO: more checks here...
}
#if HAVE_ATSPI2_SCROLL_TO
// like getFirstRelationTargetOfType() but for Atspi objects
static Atspi::Accessible
atspiGetFirstRelationTargetOfType(const Atspi::Accessible& pAtspiAccessible,
const AtspiRelationType relationType)
{
for (const auto& rel : pAtspiAccessible.getRelationSet())
{
if (rel.getRelationType() == relationType && rel.getNTargets() > 0)
return rel.getTarget(0);
}
return nullptr;
}
#endif // HAVE_ATSPI2_SCROLL_TO
/**
* @brief Gets the index of a Writer child hopping through flows-from relationships
* @param xContext The accessible context to locate
* @returns The index of @c xContext in the flows-from chain
*
* Gets the index of a child in its parent regardless of whether it is on screen or not.
*
* @warning This relying on the flows-from relationships, it only works for the connected nodes,
* and might not work for e.g. frames.
*/
sal_Int64 Atspi2TestTree::swChildIndex(uno::Reference<accessibility::XAccessibleContext> xContext)
{
for (sal_Int64 n = 0;; n++)
{
auto xPrev = getFirstRelationTargetOfType(
xContext, accessibility::AccessibleRelationType_CONTENT_FLOWS_FROM);
if (!xPrev.is())
return n;
xContext = xPrev;
}
}
/**
* @brief tests scrolling in Writer.
* @param xLOContext The @c XAccessibleContext for the writer document
* @param xAtspiAccessible The AT-SPI2 equivalent of @c xLOContext.
*
* Test scrolling (currently XAccessibleText::scrollSubstringTo()) in Writer.
*/
void Atspi2TestTree::testSwScroll(
const uno::Reference<accessibility::XAccessibleContext>& xLOContext,
const Atspi::Accessible& xAtspiAccessible)
{
#if HAVE_ATSPI2_SCROLL_TO
/* Currently LO only implements SCROLL_ANYWHERE, so to be sure we need to find something
* offscreen and try and bring it in. LO only has implementation for SwAccessibleParagraph,
* so we find the last child, and then try and find a FLOWS_TO relationship -- that's a hack
* based on how LO exposes offscreen children, e.g. not as "real" children. Once done so, we
* have to make sure the child is now on screen, so we should find it in the children list. We
* cannot rely on anything we had still being visible, as it could very well have scrolled it to
* the top. */
assert(accessibility::AccessibleRole::DOCUMENT_TEXT == xLOContext->getAccessibleRole());
auto nLOChildCount = xLOContext->getAccessibleChildCount();
if (nLOChildCount <= 0)
return;
// find the first off-screen text child
auto xLONextContext = xLOContext->getAccessibleChild(nLOChildCount - 1)->getAccessibleContext();
uno::Reference<accessibility::XAccessibleText> xLONextText;
unsigned int nAfterLast = 0;
do
{
xLONextContext = getFirstRelationTargetOfType(
xLONextContext, accessibility::AccessibleRelationType_CONTENT_FLOWS_TO);
xLONextText.set(xLONextContext, uno::UNO_QUERY);
nAfterLast++;
} while (xLONextContext.is() && !xLONextText.is());
if (!xLONextText.is())
return; // we have nothing off-screen to scroll to
// get the global index of the off-screen child so we can match it later
auto nLOChildIndex = swChildIndex(xLONextContext);
// find the corresponding Atspi child to call the API on
auto xAtspiNextChild = xAtspiAccessible.getChildAtIndex(nLOChildCount - 1);
while (nAfterLast-- > 0 && xAtspiNextChild)
xAtspiNextChild
= atspiGetFirstRelationTargetOfType(xAtspiNextChild, ATSPI_RELATION_FLOWS_TO);
/* the child ought to be found and implement the same interfaces, otherwise there's a problem
* in LO <> Atspi child mapping */
CPPUNIT_ASSERT(xAtspiNextChild);
const auto xAtspiNextText = xAtspiNextChild.queryText();
// scroll the child into view
CPPUNIT_ASSERT(xAtspiNextText.scrollSubstringTo(0, 1, ATSPI_SCROLL_ANYWHERE));
// now, check that the nLOChildIndex is in the visible area (among the regular children)
nLOChildCount = xLOContext->getAccessibleChildCount();
CPPUNIT_ASSERT_GREATER(sal_Int64(0), nLOChildCount);
const auto nLOFirstChildIndex
= swChildIndex(xLOContext->getAccessibleChild(0)->getAccessibleContext());
CPPUNIT_ASSERT_LESSEQUAL(nLOChildIndex, nLOFirstChildIndex);
CPPUNIT_ASSERT_GREATER(nLOChildIndex, nLOFirstChildIndex + nLOChildCount);
#else // !HAVE_ATSPI2_SCROLL_TO
// unused
(void)xLOContext;
(void)xAtspiAccessible;
#endif // !HAVE_ATSPI2_SCROLL_TO
}