/* -*- 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 mxLOText; Atspi::Text mxAtspiText; public: AttributesChecker(const uno::Reference& 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 static bool implCheckMapping(const T loValue, const std::string_view atspiValue, const std::unordered_map& 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&) { if (property.Value.get()) 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&) { CPPUNIT_ASSERT_EQUAL(property.Value.get(), OUString::fromUtf8(atspiValue)); return true; } bool checkFloat(std::string_view atspiValue, const beans::PropertyValue& property, const uno::Sequence&) { return implCheckFloat(atspiValue, property.Value.get()); } bool checkVariant(std::string_view atspiValue, const beans::PropertyValue& property, const uno::Sequence&) { if (property.Value.get() == 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&) { double v; char dummy; CPPUNIT_ASSERT_EQUAL(1, sscanf(atspiValue.data(), "%lg%c", &v, &dummy)); CPPUNIT_ASSERT_EQUAL(property.Value.get(), sal_Int16(v * 100)); return true; } // see Escapement2VerticalAlign bool checkVerticalAlign(std::string_view atspiValue, const beans::PropertyValue& property, const uno::Sequence&) { const sal_Int16 n = property.Value.get(); 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&) { auto color = property.Value.get(); if (color == -1) // automatic, use the component's color { uno::Reference 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(r) << 16 | static_cast(g) << 8 | static_cast(b))); return true; } return false; } // See LineSpacing2LineHeight bool checkLineHeight(std::string_view atspiValue, const beans::PropertyValue& property, const uno::Sequence&) { const auto lineSpacing = property.Value.get(); 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(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&) { const auto n = property.Value.get(); 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&) { return implCheckMapping( property.Value.get(), 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&) { return implCheckMapping(static_cast(property.Value.get()), 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&) { if (property.Value.get()) 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&) { auto aLocale = property.Value.get(); 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&) { return implCheckFloat(atspiValue, property.Value.get() / 10.0f); } bool checkWeight(std::string_view atspiValue, const beans::PropertyValue& property, const uno::Sequence&) { return implCheckFloat(atspiValue, property.Value.get() * 4); } bool checkCMMValue(std::string_view atspiValue, const beans::PropertyValue& property, const uno::Sequence&) { 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() * 0.01, v, 1e-4); return true; } bool checkDirection(std::string_view atspiValue, const beans::PropertyValue& property, const uno::Sequence&) { return implCheckMapping(property.Value.get(), 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&) { return implCheckMapping(property.Value.get(), 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& 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& 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& loProperties) { if (auto charContoured = findProperty(&property, loProperties, u"CharContoured"); charContoured && charContoured->Value.get()) { CPPUNIT_ASSERT_EQUAL(std::string_view("outline"), atspiValue); return true; } if (auto charRelief = findProperty(&property, loProperties, u"CharRelief")) { return implCheckMapping(charRelief->Value.get(), 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& loProperties) { if (atspiValue == "none") { if (auto prop = findProperty(loProperties, u"CharFlash")) CPPUNIT_ASSERT_EQUAL(false, prop->Value.get()); if (auto prop = findProperty(loProperties, u"CharUnderline")) CPPUNIT_ASSERT_EQUAL(css::awt::FontUnderline::NONE, prop->Value.get()); if (auto prop = findProperty(loProperties, u"CharStrikeout")) CPPUNIT_ASSERT(prop->Value.get() == css::awt::FontStrikeout::NONE || prop->Value.get() == 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()); } else if (atspiToken == "underline") { CPPUNIT_ASSERT((prop = findProperty(loProperties, u"CharUnderline"))); CPPUNIT_ASSERT(prop->Value.get() != css::awt::FontUnderline::NONE); } else if (atspiToken == "underline") { CPPUNIT_ASSERT((prop = findProperty(loProperties, u"CharStrikeout"))); CPPUNIT_ASSERT(prop->Value.get() != css::awt::FontStrikeout::NONE); CPPUNIT_ASSERT(prop->Value.get() != 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 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 "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&) { return implCheckTabStops(atspiValue, property, true); } bool checkTabStops(std::string_view atspiValue, const beans::PropertyValue& property, const uno::Sequence&) { return implCheckTabStops(atspiValue, property, false); } public: // runner code bool check(const uno::Sequence& xLOAttributeList, const std::unordered_map& xAtspiAttributeList) { const struct { const char* loName; const char* atspiName; bool (AttributesChecker::*checkValue)( std::string_view atspiValue, const beans::PropertyValue& property, const uno::Sequence& 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& 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& 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(xLOText, uno::UNO_QUERY); // default text attributes if (xLOTextAttrs.is()) { const auto aAttributeList = xLOTextAttrs->getDefaultAttributes(uno::Sequence()); 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 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 aAttributeList; if (xLOTextAttrs.is()) aAttributeList = xLOTextAttrs->getRunAttributes(offset, uno::Sequence()); else aAttributeList = xLOText->getCharacterAttributes(offset, uno::Sequence()); 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 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 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 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& 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 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 }