/* -*- 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 "atspi2.hxx" #include #include #include #include #include #include #include #include #include "atspiwrapper.hxx" using namespace css; // from gtk3/a11y/atkwrapper.cxx static AtspiRole mapToAtspiRole(sal_Int16 nRole, sal_Int64 nStates) { switch (nRole) { #define MAP(lo, atspi) \ case accessibility::AccessibleRole::lo: \ return ATSPI_ROLE_##atspi #define MAP_DIRECT(a) MAP(a, a) MAP_DIRECT(UNKNOWN); MAP_DIRECT(ALERT); MAP_DIRECT(BLOCK_QUOTE); MAP_DIRECT(COLUMN_HEADER); MAP_DIRECT(CANVAS); MAP_DIRECT(CHECK_BOX); MAP_DIRECT(CHECK_MENU_ITEM); MAP_DIRECT(COLOR_CHOOSER); MAP_DIRECT(COMBO_BOX); MAP_DIRECT(DATE_EDITOR); MAP_DIRECT(DESKTOP_ICON); MAP(DESKTOP_PANE, DESKTOP_FRAME); MAP_DIRECT(DIRECTORY_PANE); MAP_DIRECT(DIALOG); MAP(DOCUMENT, DOCUMENT_FRAME); MAP(EMBEDDED_OBJECT, EMBEDDED); MAP(END_NOTE, FOOTNOTE); MAP_DIRECT(FILE_CHOOSER); MAP_DIRECT(FILLER); MAP_DIRECT(FONT_CHOOSER); MAP_DIRECT(FOOTER); MAP_DIRECT(FOOTNOTE); MAP_DIRECT(FRAME); MAP_DIRECT(GLASS_PANE); MAP(GRAPHIC, IMAGE); MAP(GROUP_BOX, GROUPING); MAP_DIRECT(HEADER); MAP_DIRECT(HEADING); MAP(HYPER_LINK, LINK); MAP_DIRECT(ICON); MAP_DIRECT(INTERNAL_FRAME); MAP_DIRECT(LABEL); MAP_DIRECT(LAYERED_PANE); MAP_DIRECT(LIST); MAP_DIRECT(LIST_ITEM); MAP_DIRECT(MENU); MAP_DIRECT(MENU_BAR); MAP_DIRECT(MENU_ITEM); MAP_DIRECT(OPTION_PANE); MAP_DIRECT(PAGE_TAB); MAP_DIRECT(PAGE_TAB_LIST); MAP_DIRECT(PANEL); MAP_DIRECT(PARAGRAPH); MAP_DIRECT(PASSWORD_TEXT); MAP_DIRECT(POPUP_MENU); MAP_DIRECT(PUSH_BUTTON); MAP_DIRECT(PROGRESS_BAR); MAP_DIRECT(RADIO_BUTTON); MAP_DIRECT(RADIO_MENU_ITEM); MAP_DIRECT(ROW_HEADER); MAP_DIRECT(ROOT_PANE); MAP_DIRECT(SCROLL_BAR); MAP_DIRECT(SCROLL_PANE); MAP(SHAPE, PANEL); MAP_DIRECT(SEPARATOR); MAP_DIRECT(SLIDER); MAP(SPIN_BOX, SPIN_BUTTON); MAP_DIRECT(SPLIT_PANE); MAP_DIRECT(STATUS_BAR); MAP_DIRECT(TABLE); MAP_DIRECT(TABLE_CELL); MAP_DIRECT(TEXT); MAP(TEXT_FRAME, PANEL); MAP_DIRECT(TOGGLE_BUTTON); MAP_DIRECT(TOOL_BAR); MAP_DIRECT(TOOL_TIP); MAP_DIRECT(TREE); MAP(VIEW_PORT, VIEWPORT); MAP_DIRECT(WINDOW); #if ATSPI_ROLE_COUNT > 130 /* ATSPI_ROLE_PUSH_BUTTON_MENU is 129 */ MAP(BUTTON_MENU, PUSH_BUTTON_MENU); #else MAP(BUTTON_MENU, PUSH_BUTTON); #endif MAP_DIRECT(CAPTION); MAP_DIRECT(CHART); MAP(EDIT_BAR, EDITBAR); MAP_DIRECT(FORM); MAP_DIRECT(IMAGE_MAP); MAP(NOTE, COMMENT); MAP_DIRECT(PAGE); MAP_DIRECT(RULER); MAP_DIRECT(SECTION); MAP_DIRECT(TREE_ITEM); MAP_DIRECT(TREE_TABLE); MAP_DIRECT(COMMENT); MAP(COMMENT_END, UNKNOWN); MAP_DIRECT(DOCUMENT_PRESENTATION); MAP_DIRECT(DOCUMENT_SPREADSHEET); MAP_DIRECT(DOCUMENT_TEXT); MAP_DIRECT(STATIC); MAP_DIRECT(NOTIFICATION); #undef MAP_DIRECT #undef MAP case css::accessibility::AccessibleRole::BUTTON_DROPDOWN: if (nStates & css::accessibility::AccessibleStateType::CHECKABLE) return ATSPI_ROLE_TOGGLE_BUTTON; return ATSPI_ROLE_PUSH_BUTTON; default: SAL_WARN("vcl.gtk", "Unmapped accessible role: " << nRole); return ATSPI_ROLE_UNKNOWN; } } static AtspiStateType mapAtspiState(sal_Int64 nState) { // A perfect / complete mapping ... switch (nState) { #define MAP(lo, atspi) \ case accessibility::AccessibleStateType::lo: \ return ATSPI_STATE_##atspi #define MAP_DIRECT(a) MAP(a, a) MAP_DIRECT(INVALID); MAP_DIRECT(ACTIVE); MAP_DIRECT(ARMED); MAP_DIRECT(BUSY); MAP_DIRECT(CHECKABLE); MAP_DIRECT(CHECKED); MAP_DIRECT(EDITABLE); MAP_DIRECT(ENABLED); MAP_DIRECT(EXPANDABLE); MAP_DIRECT(EXPANDED); MAP_DIRECT(FOCUSABLE); MAP_DIRECT(FOCUSED); MAP_DIRECT(HORIZONTAL); MAP_DIRECT(ICONIFIED); MAP_DIRECT(INDETERMINATE); MAP_DIRECT(MANAGES_DESCENDANTS); MAP_DIRECT(MODAL); MAP_DIRECT(MULTI_LINE); MAP(MULTI_SELECTABLE, MULTISELECTABLE); MAP_DIRECT(OPAQUE); MAP_DIRECT(PRESSED); MAP_DIRECT(RESIZABLE); MAP_DIRECT(SELECTABLE); MAP_DIRECT(SELECTED); MAP_DIRECT(SENSITIVE); MAP_DIRECT(SHOWING); MAP_DIRECT(SINGLE_LINE); MAP_DIRECT(STALE); MAP_DIRECT(TRANSIENT); MAP_DIRECT(VERTICAL); MAP_DIRECT(VISIBLE); MAP(DEFAULT, IS_DEFAULT); // a spelling error ... MAP(DEFUNC, DEFUNCT); #undef MAP_DIRECT #undef MAP default: //Mis-use ATK_STATE_LAST_DEFINED to check if a state is unmapped //NOTE! Do not report it return ATSPI_STATE_LAST_DEFINED; } } static AtspiRelationType mapRelationType(AccessibleRelationType eRelation) { switch (eRelation) { case accessibility::AccessibleRelationType_CONTENT_FLOWS_FROM: return ATSPI_RELATION_FLOWS_FROM; case accessibility::AccessibleRelationType_CONTENT_FLOWS_TO: return ATSPI_RELATION_FLOWS_TO; case accessibility::AccessibleRelationType_CONTROLLED_BY: return ATSPI_RELATION_CONTROLLED_BY; case accessibility::AccessibleRelationType_CONTROLLER_FOR: return ATSPI_RELATION_CONTROLLER_FOR; case accessibility::AccessibleRelationType_DESCRIBED_BY: return ATSPI_RELATION_DESCRIBED_BY; case accessibility::AccessibleRelationType_LABEL_FOR: return ATSPI_RELATION_LABEL_FOR; case accessibility::AccessibleRelationType_LABELED_BY: return ATSPI_RELATION_LABELLED_BY; case accessibility::AccessibleRelationType_MEMBER_OF: return ATSPI_RELATION_MEMBER_OF; case accessibility::AccessibleRelationType_SUB_WINDOW_OF: return ATSPI_RELATION_SUBWINDOW_OF; case accessibility::AccessibleRelationType_NODE_CHILD_OF: return ATSPI_RELATION_NODE_CHILD_OF; default: return ATSPI_RELATION_NULL; } } static std::string debugString(const Atspi::Accessible& pAtspiAccessible) { CPPUNIT_NS::OStringStream ost; ost << "(" << static_cast(pAtspiAccessible.get()) << ")"; if (pAtspiAccessible) { ost << " role=\"" << pAtspiAccessible.getRoleName() << '"'; ost << " name=\"" << pAtspiAccessible.getName() << '"'; ost << " description=\"" << pAtspiAccessible.getDescription() << '"'; } return ost.str(); } static void dumpAtspiTree(const Atspi::Accessible& pAcc, const int depth = 0) { std::cout << debugString(pAcc) << std::endl; sal_Int32 i = 0; for (const auto pChild : pAcc) { for (auto j = decltype(depth){ 0 }; j < depth; j++) std::cout << " "; std::cout << " * child " << i++ << ": "; dumpAtspiTree(pChild, depth + 1); } } void Atspi2TestTree::compareObjects(const uno::Reference& xLOAccessible, const Atspi::Accessible& pAtspiAccessible, const sal_uInt16 recurseFlags) { if (recurseFlags != RecurseFlags::NONE) std::cout << "checking " << debugString(pAtspiAccessible) << " against " << AccessibilityTools::debugString(xLOAccessible) << std::endl; CPPUNIT_ASSERT(xLOAccessible); CPPUNIT_ASSERT(pAtspiAccessible); auto xLOContext = xLOAccessible->getAccessibleContext(); /* role: we translate to ATSPI role, because the value was created by LO already and converted * to ATK, which in turn converts it to ATSPI. However, ATK and ATSPI are roughly equivalent * (ATK basically follows ATSPI), but LO's internal might have more complex mappings that can't * be represented with a round trip. */ const AtspiRole nLORole = mapToAtspiRole(xLOContext->getAccessibleRole(), xLOContext->getAccessibleStateSet()); const auto nAtspiRole = pAtspiAccessible.getRole(); CPPUNIT_ASSERT_EQUAL(nLORole, nAtspiRole); /* name (no need to worry about debugging suffixes as AccessibilityTools::nameEquals does, as * that will also be part of the name sent to ATSPI) */ CPPUNIT_ASSERT_EQUAL(xLOContext->getAccessibleName(), OUString::fromUtf8(pAtspiAccessible.getName())); // description CPPUNIT_ASSERT_EQUAL(xLOContext->getAccessibleDescription(), OUString::fromUtf8(pAtspiAccessible.getDescription())); // parent relationship (this is conditional as the ATSPI tree has additional parents, as well as // because we don't want to recurse up the tree) if (recurseFlags & RecurseFlags::PARENT) { // index in parent CPPUNIT_ASSERT_EQUAL(xLOContext->getAccessibleIndexInParent(), sal_Int64(pAtspiAccessible.getIndexInParent())); // parent (well, that's making things a lot more expensive...) compareObjects(xLOContext->getAccessibleParent(), pAtspiAccessible.getParent(), RecurseFlags::NONE); } // state set const auto loStateSet = xLOContext->getAccessibleStateSet(); const auto atspiStateSet = pAtspiAccessible.getStateSet(); const auto nBits = (sizeof(decltype(loStateSet)) * 8) - (std::is_signed_v ? 1 : 0); for (auto shift = decltype(nBits){ 0 }; shift < nBits; shift++) { const auto loState = decltype(loStateSet){ 1 } << shift; const auto atspiState = mapAtspiState(loState); // ignore a state that does not map to Atspi if (atspiState == ATSPI_STATE_LAST_DEFINED) continue; /* FIXME: The ATK implementation in LO adds FOCUSED if the obj == atk_get_focus_object() * (see atkwrapper.cxx::wrapper_ref_state_set()), but there seem to be some bug (or delay? * as it's done in idle) in the tracking, so we can end up with extra FOCUSED states on the * Atspi side. To work around that, we skip the case where it's not set on LO's side */ if (atspiState == ATSPI_STATE_FOCUSED && !(loStateSet & loState)) continue; CPPUNIT_ASSERT_EQUAL_MESSAGE("Unmatched state: " + Atspi::State::getName(atspiState), (loStateSet & loState) != 0, atspiStateSet.contains(atspiState)); } // attributes if (auto xLOAttrs = uno::Reference(xLOContext, uno::UNO_QUERY)) { // see atktextattributes.cxx:attribute_set_new_from_extended_attributes const uno::Any anyVal = xLOAttrs->getExtendedAttributes(); OUString sExtendedAttrs; anyVal >>= sExtendedAttrs; sal_Int32 nIndex = 0; const auto atspiAttrs = pAtspiAccessible.getAttributes(); do { OUString sProperty = sExtendedAttrs.getToken(0, ';', nIndex); sal_Int32 nColonPos = 0; const OString sPropertyName = OUStringToOString( o3tl::getToken(sProperty, 0, ':', nColonPos), RTL_TEXTENCODING_UTF8); const OString sPropertyValue = OUStringToOString( o3tl::getToken(sProperty, 0, ':', nColonPos), RTL_TEXTENCODING_UTF8); const auto atspiAttrIter = atspiAttrs.find(std::string(sPropertyName)); CPPUNIT_ASSERT_MESSAGE(std::string("Missing attribute: ") + sPropertyName.getStr(), atspiAttrIter != atspiAttrs.end()); CPPUNIT_ASSERT_EQUAL(std::string_view(sPropertyName), std::string_view(atspiAttrIter->first)); CPPUNIT_ASSERT_EQUAL(std::string_view(sPropertyValue), std::string_view(atspiAttrIter->second)); } while (nIndex >= 0 && nIndex < sExtendedAttrs.getLength()); } // relations const auto xLORelationSet = xLOContext->getAccessibleRelationSet(); const auto aAtspiRelationSet = pAtspiAccessible.getRelationSet(); const auto nLORelationCount = xLORelationSet.is() ? xLORelationSet->getRelationCount() : 0; CPPUNIT_ASSERT_EQUAL(nLORelationCount, sal_Int32(aAtspiRelationSet.size())); for (auto i = decltype(nLORelationCount){ 0 }; i < nLORelationCount; i++) { const auto xLORelation = xLORelationSet->getRelation(i); const auto pAtspiRelation = aAtspiRelationSet[i]; const auto nLOTargetsCount = xLORelation.TargetSet.getLength(); CPPUNIT_ASSERT_EQUAL(mapRelationType(xLORelation.RelationType), pAtspiRelation.getRelationType()); CPPUNIT_ASSERT_EQUAL(nLOTargetsCount, static_cast(pAtspiRelation.getNTargets())); if (recurseFlags & RecurseFlags::RELATIONS_TARGETS) { for (auto j = decltype(nLOTargetsCount){ 0 }; j < nLOTargetsCount; j++) { uno::Reference xLOTarget = xLORelation.TargetSet[j]; compareObjects(xLOTarget, pAtspiRelation.getTarget(j), RecurseFlags::NONE); } } } // other interfaces if (auto xLOText = uno::Reference(xLOContext, uno::UNO_QUERY)) { Atspi::Text pAtspiText; CPPUNIT_ASSERT_NO_THROW(pAtspiText = pAtspiAccessible.queryText()); compareTextObjects(xLOText, pAtspiText); } // TODO: more checks here... } void Atspi2TestTree::compareTrees(const uno::Reference& xLOAccessible, const Atspi::Accessible& xAtspiAccessible, const int depth) { sal_uInt16 recurseFlags = RecurseFlags::ALL; if (depth == 0) recurseFlags ^= RecurseFlags::PARENT; compareObjects(xLOAccessible, xAtspiAccessible, recurseFlags); if (!xLOAccessible || !xAtspiAccessible) return; auto xLOContext = xLOAccessible->getAccessibleContext(); CPPUNIT_ASSERT(xLOContext); const auto nLOChildCount = xLOContext->getAccessibleChildCount(); const auto nAtspiChildCount = decltype(nLOChildCount){ xAtspiAccessible.getChildCount() }; /* We use >= instead of == because GTK exposes scrollbar objects LO doesn't. We possibly * should check better than merely accept more children, but it's probably OK if there are * *more* children as viewed by ATSPI, rather than less. And we're comparing them anyway. */ CPPUNIT_ASSERT_GREATEREQUAL(nLOChildCount, nAtspiChildCount); for (auto nthChild = decltype(nLOChildCount){ 0 }; nthChild < nLOChildCount; nthChild++) { for (auto i = decltype(depth){ 0 }; i < depth; i++) std::cout << " "; std::cout << "* child " << nthChild << ": "; compareTrees(xLOContext->getAccessibleChild(nthChild), xAtspiAccessible.getChildAtIndex(nthChild), depth + 1); } /* We need to scrolling test here, because they might modify the tree and invalidate children, * so we can't do it from the children themselves as they might get disposed during the test */ if (nLOChildCount > 0 && accessibility::AccessibleRole::DOCUMENT_TEXT == xLOContext->getAccessibleRole()) { testSwScroll(xLOContext, xAtspiAccessible); } } // gets the nth child of @p pAcc and check its role is @p role static Atspi::Accessible getDescendentAtPath(const Atspi::Accessible& xAcc, int nthChild, AtspiRole role) { CPPUNIT_ASSERT(xAcc); CPPUNIT_ASSERT_GREATER(nthChild, xAcc.getChildCount()); auto xChild = xAcc.getChildAtIndex(nthChild); CPPUNIT_ASSERT(xChild); CPPUNIT_ASSERT_EQUAL(role, xChild.getRole()); return xChild; } // gets the nth child of @p pAcc and check its role is @p role, then gets the nth child of that one, etc. template static Atspi::Accessible getDescendentAtPath(const Atspi::Accessible& xAcc, int nthChild, AtspiRole role, Ts... args) { return getDescendentAtPath(getDescendentAtPath(xAcc, nthChild, role), args...); } CPPUNIT_TEST_FIXTURE(Atspi2TestTree, Test1) { loadFromSrc(u"vcl/qa/cppunit/a11y/atspi2/testdocuments/ecclectic.fodt"_ustr); /* FIXME: We zoom out for everything to fit in the view not to have off-screen children * that the controller code fails to clean up properly in some situations. * Once the root issue is fixed in LO, remove this. * Note that zooming out like so, and not having off-screen children, renders the * Atspi2TestTree::testSwScroll() test useless as it has nothing to scroll into view. */ unotest::MacrosTest::dispatchCommand(mxDocument, ".uno:ZoomPage", {}); unotest::MacrosTest::dispatchCommand( mxDocument, ".uno:ViewLayout", { comphelper::makePropertyValue("ViewLayout.Columns", sal_Int16(2)), comphelper::makePropertyValue("ViewLayout.BookMode", false), }); /* HACK: verify the whole content of the document is actually visible (nothing overflows) * after zooming out above */ const auto xLODocContext = getDocumentAccessibleContext(); const auto xLODocFirstChild = xLODocContext->getAccessibleChild(0); CPPUNIT_ASSERT(xLODocFirstChild.is()); CPPUNIT_ASSERT( !getFirstRelationTargetOfType(xLODocFirstChild->getAccessibleContext(), accessibility::AccessibleRelationType_CONTENT_FLOWS_FROM)); const auto nLODocChildCount = xLODocContext->getAccessibleChildCount(); const auto xLODocLastChild = xLODocContext->getAccessibleChild(nLODocChildCount - 1); CPPUNIT_ASSERT(xLODocLastChild.is()); CPPUNIT_ASSERT( !getFirstRelationTargetOfType(xLODocLastChild->getAccessibleContext(), accessibility::AccessibleRelationType_CONTENT_FLOWS_TO)); // END HACK auto xContext = getWindowAccessibleContext(); CPPUNIT_ASSERT(xContext.is()); //~ dumpA11YTree(xContext); // get the window manager frame auto xAtspiWindow = getDescendentAtPath(m_pAtspiApp, 0, ATSPI_ROLE_FRAME); CPPUNIT_ASSERT(xAtspiWindow); dumpAtspiTree(xAtspiWindow); /* The ATSPI representation has extra nodes around the relevant ones, which look like leftovers * from the start center. Ignore those and dive directly to the meaningful node (which is the * 1st child of the 2nd child of the 1st child -- ask me how I know) */ auto xAtspiPane = getDescendentAtPath(xAtspiWindow, 0, ATSPI_ROLE_PANEL, 1, ATSPI_ROLE_PANEL, 0, ATSPI_ROLE_ROOT_PANE); compareTrees(uno::Reference(mxWindow, uno::UNO_QUERY_THROW), xAtspiPane); } CPPUNIT_PLUGIN_IMPLEMENT(); /* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */