diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 05:54:39 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 05:54:39 +0000 |
commit | 267c6f2ac71f92999e969232431ba04678e7437e (patch) | |
tree | 358c9467650e1d0a1d7227a21dac2e3d08b622b2 /test/source/a11y | |
parent | Initial commit. (diff) | |
download | libreoffice-267c6f2ac71f92999e969232431ba04678e7437e.tar.xz libreoffice-267c6f2ac71f92999e969232431ba04678e7437e.zip |
Adding upstream version 4:24.2.0.upstream/4%24.2.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'test/source/a11y')
-rw-r--r-- | test/source/a11y/AccessibilityTools.cxx | 753 | ||||
-rw-r--r-- | test/source/a11y/accessibletestbase.cxx | 581 | ||||
-rw-r--r-- | test/source/a11y/eventposter.cxx | 46 | ||||
-rw-r--r-- | test/source/a11y/swaccessibletestbase.cxx | 135 |
4 files changed, 1515 insertions, 0 deletions
diff --git a/test/source/a11y/AccessibilityTools.cxx b/test/source/a11y/AccessibilityTools.cxx new file mode 100644 index 0000000000..f0a28d4cda --- /dev/null +++ b/test/source/a11y/AccessibilityTools.cxx @@ -0,0 +1,753 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <test/a11y/AccessibilityTools.hxx> + +#include <com/sun/star/accessibility/AccessibleEventId.hpp> +#include <com/sun/star/accessibility/AccessibleRelationType.hpp> +#include <com/sun/star/accessibility/AccessibleRole.hpp> +#include <com/sun/star/accessibility/AccessibleStateType.hpp> +#include <com/sun/star/accessibility/XAccessible.hpp> +#include <com/sun/star/accessibility/XAccessibleAction.hpp> +#include <com/sun/star/accessibility/XAccessibleContext.hpp> +#include <com/sun/star/awt/KeyModifier.hpp> + +#include <rtl/ustrbuf.hxx> +#include <sal/log.hxx> +#include <toolkit/awt/vclxaccessiblecomponent.hxx> +#include <vcl/scheduler.hxx> +#include <vcl/timer.hxx> +#include <vcl/window.hxx> +#include <o3tl/string_view.hxx> + +using namespace css; + +uno::Reference<accessibility::XAccessibleContext> +AccessibilityTools::getAccessibleObjectForPredicate( + const uno::Reference<accessibility::XAccessibleContext>& xCtx, + const std::function<bool(const uno::Reference<accessibility::XAccessibleContext>&)>& cPredicate) +{ + if (cPredicate(xCtx)) + { + return xCtx; + } + else + { + sal_Int64 count = xCtx->getAccessibleChildCount(); + + for (sal_Int64 i = 0; i < count && i < AccessibilityTools::MAX_CHILDREN; i++) + { + uno::Reference<accessibility::XAccessibleContext> xCtx2 + = getAccessibleObjectForPredicate(xCtx->getAccessibleChild(i), cPredicate); + if (xCtx2.is()) + return xCtx2; + } + } + return nullptr; +} + +uno::Reference<accessibility::XAccessibleContext> +AccessibilityTools::getAccessibleObjectForPredicate( + const uno::Reference<accessibility::XAccessible>& xAcc, + const std::function<bool(const uno::Reference<accessibility::XAccessibleContext>&)>& cPredicate) +{ + return getAccessibleObjectForPredicate(xAcc->getAccessibleContext(), cPredicate); +} + +uno::Reference<accessibility::XAccessibleContext> AccessibilityTools::getAccessibleObjectForRole( + const uno::Reference<accessibility::XAccessibleContext>& xCtx, sal_Int16 role) +{ + return getAccessibleObjectForPredicate( + xCtx, [&role](const uno::Reference<accessibility::XAccessibleContext>& xObjCtx) { + return (xObjCtx->getAccessibleRole() == role + && xObjCtx->getAccessibleStateSet() + & accessibility::AccessibleStateType::SHOWING); + }); +} + +css::uno::Reference<css::accessibility::XAccessibleContext> +AccessibilityTools::getAccessibleObjectForRole( + const css::uno::Reference<css::accessibility::XAccessible>& xacc, sal_Int16 role) +{ + return getAccessibleObjectForRole(xacc->getAccessibleContext(), role); +} + +/* this is basically the same as getAccessibleObjectForPredicate() but specialized for efficiency, + * and because the template version will not work with getAccessibleObjectForPredicate() anyway */ +css::uno::Reference<css::accessibility::XAccessibleContext> +AccessibilityTools::getAccessibleObjectForName( + const css::uno::Reference<css::accessibility::XAccessibleContext>& xCtx, const sal_Int16 role, + std::u16string_view name) +{ + if (xCtx->getAccessibleRole() == role && nameEquals(xCtx, name)) + return xCtx; + + auto nChildren = xCtx->getAccessibleChildCount(); + for (decltype(nChildren) i = 0; i < nChildren && i < AccessibilityTools::MAX_CHILDREN; i++) + { + if (auto xMatchChild = getAccessibleObjectForName(xCtx->getAccessibleChild(i), role, name)) + return xMatchChild; + } + + return nullptr; +} + +bool AccessibilityTools::equals(const uno::Reference<accessibility::XAccessible>& xacc1, + const uno::Reference<accessibility::XAccessible>& xacc2) +{ + if (!xacc1.is() || !xacc2.is()) + return xacc1.is() == xacc2.is(); + return equals(xacc1->getAccessibleContext(), xacc2->getAccessibleContext()); +} + +bool AccessibilityTools::equals(const uno::Reference<accessibility::XAccessibleContext>& xctx1, + const uno::Reference<accessibility::XAccessibleContext>& xctx2) +{ + if (!xctx1.is() || !xctx2.is()) + return xctx1.is() == xctx2.is(); + + if (xctx1->getAccessibleRole() != xctx2->getAccessibleRole()) + return false; + + if (xctx1->getAccessibleName() != xctx2->getAccessibleName()) + return false; + + if (xctx1->getAccessibleDescription() != xctx2->getAccessibleDescription()) + return false; + + if (xctx1->getAccessibleChildCount() != xctx2->getAccessibleChildCount()) + return false; + + /* this one was not in the Java version */ + if (xctx1->getAccessibleIndexInParent() != xctx2->getAccessibleIndexInParent()) + return false; + + /* because in Writer at least some children only are referenced by their relations to others + * objects, we need to account for that as their index in parent is incorrect (so not + * necessarily unique) */ + auto relset1 = xctx1->getAccessibleRelationSet(); + auto relset2 = xctx2->getAccessibleRelationSet(); + if (relset1.is() != relset2.is()) + return false; + else if (relset1.is()) + { + auto relCount1 = relset1->getRelationCount(); + auto relCount2 = relset2->getRelationCount(); + if (relCount1 != relCount2) + return false; + + for (sal_Int32 i = 0; i < relCount1; ++i) + { + if (relset1->getRelation(i) != relset2->getRelation(i)) + return false; + } + } + + return equals(xctx1->getAccessibleParent(), xctx2->getAccessibleParent()); +} + +bool AccessibilityTools::nameEquals(const uno::Reference<accessibility::XAccessibleContext>& xCtx, + const std::u16string_view name) +{ + auto ctxName = xCtx->getAccessibleName(); + std::u16string_view rest; + + if (!o3tl::starts_with(ctxName, name, &rest)) + return false; + if (rest == u"") + return true; + +#if defined(_WIN32) + // see OAccessibleMenuItemComponent::GetAccessibleName(): + // on Win32, ignore a \tSHORTCUT suffix on a menu item + switch (xCtx->getAccessibleRole()) + { + case accessibility::AccessibleRole::MENU_ITEM: + case accessibility::AccessibleRole::RADIO_MENU_ITEM: + case accessibility::AccessibleRole::CHECK_MENU_ITEM: + return rest[0] == '\t'; + + default: + break; + } +#endif + +#if OSL_DEBUG_LEVEL > 0 + // see VCLXAccessibleComponent::getAccessibleName() + static const char* pEnvAppendType = getenv("LIBO_APPEND_WINDOW_TYPE_TO_ACCESSIBLE_NAME"); + if (pEnvAppendType && OUString::createFromAscii(pEnvAppendType) != u"0") + { + auto pVCLXAccessibleComponent = dynamic_cast<VCLXAccessibleComponent*>(xCtx.get()); + if (pVCLXAccessibleComponent) + { + auto windowType = pVCLXAccessibleComponent->GetWindow()->GetType(); + if (rest + == Concat2View(u" (Type = " + OUString::number(static_cast<sal_Int32>(windowType)) + + ")")) + return true; + } + } +#endif + return false; +} + +static OUString unknownName(const sal_Int64 value) +{ + return "unknown (" + OUString::number(value) + ")"; +} + +OUString AccessibilityTools::getRoleName(const sal_Int16 role) +{ + switch (role) + { + case accessibility::AccessibleRole::UNKNOWN: + return "UNKNOWN"; + case accessibility::AccessibleRole::ALERT: + return "ALERT"; + case accessibility::AccessibleRole::BLOCK_QUOTE: + return "BLOCK_QUOTE"; + case accessibility::AccessibleRole::BUTTON_DROPDOWN: + return "BUTTON_DROPDOWN"; + case accessibility::AccessibleRole::BUTTON_MENU: + return "BUTTON_MENU"; + case accessibility::AccessibleRole::CANVAS: + return "CANVAS"; + case accessibility::AccessibleRole::CAPTION: + return "CAPTION"; + case accessibility::AccessibleRole::CHART: + return "CHART"; + case accessibility::AccessibleRole::CHECK_BOX: + return "CHECK_BOX"; + case accessibility::AccessibleRole::CHECK_MENU_ITEM: + return "CHECK_MENU_ITEM"; + case accessibility::AccessibleRole::COLOR_CHOOSER: + return "COLOR_CHOOSER"; + case accessibility::AccessibleRole::COLUMN_HEADER: + return "COLUMN_HEADER"; + case accessibility::AccessibleRole::COMBO_BOX: + return "COMBO_BOX"; + case accessibility::AccessibleRole::COMMENT: + return "COMMENT"; + case accessibility::AccessibleRole::COMMENT_END: + return "COMMENT_END"; + case accessibility::AccessibleRole::DATE_EDITOR: + return "DATE_EDITOR"; + case accessibility::AccessibleRole::DESKTOP_ICON: + return "DESKTOP_ICON"; + case accessibility::AccessibleRole::DESKTOP_PANE: + return "DESKTOP_PANE"; + case accessibility::AccessibleRole::DIALOG: + return "DIALOG"; + case accessibility::AccessibleRole::DIRECTORY_PANE: + return "DIRECTORY_PANE"; + case accessibility::AccessibleRole::DOCUMENT: + return "DOCUMENT"; + case accessibility::AccessibleRole::DOCUMENT_PRESENTATION: + return "DOCUMENT_PRESENTATION"; + case accessibility::AccessibleRole::DOCUMENT_SPREADSHEET: + return "DOCUMENT_SPREADSHEET"; + case accessibility::AccessibleRole::DOCUMENT_TEXT: + return "DOCUMENT_TEXT"; + case accessibility::AccessibleRole::EDIT_BAR: + return "EDIT_BAR"; + case accessibility::AccessibleRole::EMBEDDED_OBJECT: + return "EMBEDDED_OBJECT"; + case accessibility::AccessibleRole::END_NOTE: + return "END_NOTE"; + case accessibility::AccessibleRole::FILE_CHOOSER: + return "FILE_CHOOSER"; + case accessibility::AccessibleRole::FILLER: + return "FILLER"; + case accessibility::AccessibleRole::FONT_CHOOSER: + return "FONT_CHOOSER"; + case accessibility::AccessibleRole::FOOTER: + return "FOOTER"; + case accessibility::AccessibleRole::FOOTNOTE: + return "FOOTNOTE"; + case accessibility::AccessibleRole::FORM: + return "FORM"; + case accessibility::AccessibleRole::FRAME: + return "FRAME"; + case accessibility::AccessibleRole::GLASS_PANE: + return "GLASS_PANE"; + case accessibility::AccessibleRole::GRAPHIC: + return "GRAPHIC"; + case accessibility::AccessibleRole::GROUP_BOX: + return "GROUP_BOX"; + case accessibility::AccessibleRole::HEADER: + return "HEADER"; + case accessibility::AccessibleRole::HEADING: + return "HEADING"; + case accessibility::AccessibleRole::HYPER_LINK: + return "HYPER_LINK"; + case accessibility::AccessibleRole::ICON: + return "ICON"; + case accessibility::AccessibleRole::IMAGE_MAP: + return "IMAGE_MAP"; + case accessibility::AccessibleRole::INTERNAL_FRAME: + return "INTERNAL_FRAME"; + case accessibility::AccessibleRole::LABEL: + return "LABEL"; + case accessibility::AccessibleRole::LAYERED_PANE: + return "LAYERED_PANE"; + case accessibility::AccessibleRole::LIST: + return "LIST"; + case accessibility::AccessibleRole::LIST_ITEM: + return "LIST_ITEM"; + case accessibility::AccessibleRole::MENU: + return "MENU"; + case accessibility::AccessibleRole::MENU_BAR: + return "MENU_BAR"; + case accessibility::AccessibleRole::MENU_ITEM: + return "MENU_ITEM"; + case accessibility::AccessibleRole::NOTE: + return "NOTE"; + case accessibility::AccessibleRole::OPTION_PANE: + return "OPTION_PANE"; + case accessibility::AccessibleRole::PAGE: + return "PAGE"; + case accessibility::AccessibleRole::PAGE_TAB: + return "PAGE_TAB"; + case accessibility::AccessibleRole::PAGE_TAB_LIST: + return "PAGE_TAB_LIST"; + case accessibility::AccessibleRole::PANEL: + return "PANEL"; + case accessibility::AccessibleRole::PARAGRAPH: + return "PARAGRAPH"; + case accessibility::AccessibleRole::PASSWORD_TEXT: + return "PASSWORD_TEXT"; + case accessibility::AccessibleRole::POPUP_MENU: + return "POPUP_MENU"; + case accessibility::AccessibleRole::PROGRESS_BAR: + return "PROGRESS_BAR"; + case accessibility::AccessibleRole::PUSH_BUTTON: + return "PUSH_BUTTON"; + case accessibility::AccessibleRole::RADIO_BUTTON: + return "RADIO_BUTTON"; + case accessibility::AccessibleRole::RADIO_MENU_ITEM: + return "RADIO_MENU_ITEM"; + case accessibility::AccessibleRole::ROOT_PANE: + return "ROOT_PANE"; + case accessibility::AccessibleRole::ROW_HEADER: + return "ROW_HEADER"; + case accessibility::AccessibleRole::RULER: + return "RULER"; + case accessibility::AccessibleRole::SCROLL_BAR: + return "SCROLL_BAR"; + case accessibility::AccessibleRole::SCROLL_PANE: + return "SCROLL_PANE"; + case accessibility::AccessibleRole::SECTION: + return "SECTION"; + case accessibility::AccessibleRole::SEPARATOR: + return "SEPARATOR"; + case accessibility::AccessibleRole::SHAPE: + return "SHAPE"; + case accessibility::AccessibleRole::SLIDER: + return "SLIDER"; + case accessibility::AccessibleRole::SPIN_BOX: + return "SPIN_BOX"; + case accessibility::AccessibleRole::SPLIT_PANE: + return "SPLIT_PANE"; + case accessibility::AccessibleRole::STATIC: + return "STATIC"; + case accessibility::AccessibleRole::STATUS_BAR: + return "STATUS_BAR"; + case accessibility::AccessibleRole::TABLE: + return "TABLE"; + case accessibility::AccessibleRole::TABLE_CELL: + return "TABLE_CELL"; + case accessibility::AccessibleRole::TEXT: + return "TEXT"; + case accessibility::AccessibleRole::TEXT_FRAME: + return "TEXT_FRAME"; + case accessibility::AccessibleRole::TOGGLE_BUTTON: + return "TOGGLE_BUTTON"; + case accessibility::AccessibleRole::TOOL_BAR: + return "TOOL_BAR"; + case accessibility::AccessibleRole::TOOL_TIP: + return "TOOL_TIP"; + case accessibility::AccessibleRole::TREE: + return "TREE"; + case accessibility::AccessibleRole::TREE_ITEM: + return "TREE_ITEM"; + case accessibility::AccessibleRole::TREE_TABLE: + return "TREE_TABLE"; + case accessibility::AccessibleRole::VIEW_PORT: + return "VIEW_PORT"; + case accessibility::AccessibleRole::WINDOW: + return "WINDOW"; + }; + return unknownName(role); +} + +OUString AccessibilityTools::debugAccessibleStateSet(const sal_Int64 nCombinedState) +{ + OUString combinedName; + + for (int i = 0; i < 63; i++) + { + sal_Int64 state = sal_Int64(1) << i; + if (!(state & nCombinedState)) + continue; + OUString name; + switch (state) + { + case accessibility::AccessibleStateType::ACTIVE: + name = "ACTIVE"; + break; + case accessibility::AccessibleStateType::ARMED: + name = "ARMED"; + break; + case accessibility::AccessibleStateType::BUSY: + name = "BUSY"; + break; + case accessibility::AccessibleStateType::CHECKABLE: + name = "CHECKABLE"; + break; + case accessibility::AccessibleStateType::CHECKED: + name = "CHECKED"; + break; + case accessibility::AccessibleStateType::COLLAPSE: + name = "COLLAPSE"; + break; + case accessibility::AccessibleStateType::DEFAULT: + name = "DEFAULT"; + break; + case accessibility::AccessibleStateType::DEFUNC: + name = "DEFUNC"; + break; + case accessibility::AccessibleStateType::EDITABLE: + name = "EDITABLE"; + break; + case accessibility::AccessibleStateType::ENABLED: + name = "ENABLED"; + break; + case accessibility::AccessibleStateType::EXPANDABLE: + name = "EXPANDABLE"; + break; + case accessibility::AccessibleStateType::EXPANDED: + name = "EXPANDED"; + break; + case accessibility::AccessibleStateType::FOCUSABLE: + name = "FOCUSABLE"; + break; + case accessibility::AccessibleStateType::FOCUSED: + name = "FOCUSED"; + break; + case accessibility::AccessibleStateType::HORIZONTAL: + name = "HORIZONTAL"; + break; + case accessibility::AccessibleStateType::ICONIFIED: + name = "ICONIFIED"; + break; + case accessibility::AccessibleStateType::INDETERMINATE: + name = "INDETERMINATE"; + break; + case accessibility::AccessibleStateType::INVALID: + name = "INVALID"; + break; + case accessibility::AccessibleStateType::MANAGES_DESCENDANTS: + name = "MANAGES_DESCENDANTS"; + break; + case accessibility::AccessibleStateType::MODAL: + name = "MODAL"; + break; + case accessibility::AccessibleStateType::MOVEABLE: + name = "MOVEABLE"; + break; + case accessibility::AccessibleStateType::MULTI_LINE: + name = "MULTI_LINE"; + break; + case accessibility::AccessibleStateType::MULTI_SELECTABLE: + name = "MULTI_SELECTABLE"; + break; + case accessibility::AccessibleStateType::OFFSCREEN: + name = "OFFSCREEN"; + break; + case accessibility::AccessibleStateType::OPAQUE: + name = "OPAQUE"; + break; + case accessibility::AccessibleStateType::PRESSED: + name = "PRESSED"; + break; + case accessibility::AccessibleStateType::RESIZABLE: + name = "RESIZABLE"; + break; + case accessibility::AccessibleStateType::SELECTABLE: + name = "SELECTABLE"; + break; + case accessibility::AccessibleStateType::SELECTED: + name = "SELECTED"; + break; + case accessibility::AccessibleStateType::SENSITIVE: + name = "SENSITIVE"; + break; + case accessibility::AccessibleStateType::SHOWING: + name = "SHOWING"; + break; + case accessibility::AccessibleStateType::SINGLE_LINE: + name = "SINGLE_LINE"; + break; + case accessibility::AccessibleStateType::STALE: + name = "STALE"; + break; + case accessibility::AccessibleStateType::TRANSIENT: + name = "TRANSIENT"; + break; + case accessibility::AccessibleStateType::VERTICAL: + name = "VERTICAL"; + break; + case accessibility::AccessibleStateType::VISIBLE: + name = "VISIBLE"; + break; + default: + name = unknownName(state); + break; + } + if (combinedName.getLength()) + combinedName += " | "; + combinedName += name; + } + + if (combinedName.isEmpty()) + return "unknown"; + return combinedName; +} + +OUString AccessibilityTools::getEventIdName(const sal_Int16 event_id) +{ + switch (event_id) + { + case accessibility::AccessibleEventId::ACTION_CHANGED: + return "ACTION_CHANGED"; + case accessibility::AccessibleEventId::ACTIVE_DESCENDANT_CHANGED: + return "ACTIVE_DESCENDANT_CHANGED"; + case accessibility::AccessibleEventId::ACTIVE_DESCENDANT_CHANGED_NOFOCUS: + return "ACTIVE_DESCENDANT_CHANGED_NOFOCUS"; + case accessibility::AccessibleEventId::BOUNDRECT_CHANGED: + return "BOUNDRECT_CHANGED"; + case accessibility::AccessibleEventId::CARET_CHANGED: + return "CARET_CHANGED"; + case accessibility::AccessibleEventId::CHILD: + return "CHILD"; + case accessibility::AccessibleEventId::COLUMN_CHANGED: + return "COLUMN_CHANGED"; + case accessibility::AccessibleEventId::CONTENT_FLOWS_FROM_RELATION_CHANGED: + return "CONTENT_FLOWS_FROM_RELATION_CHANGED"; + case accessibility::AccessibleEventId::CONTENT_FLOWS_TO_RELATION_CHANGED: + return "CONTENT_FLOWS_TO_RELATION_CHANGED"; + case accessibility::AccessibleEventId::CONTROLLED_BY_RELATION_CHANGED: + return "CONTROLLED_BY_RELATION_CHANGED"; + case accessibility::AccessibleEventId::CONTROLLER_FOR_RELATION_CHANGED: + return "CONTROLLER_FOR_RELATION_CHANGED"; + case accessibility::AccessibleEventId::DESCRIPTION_CHANGED: + return "DESCRIPTION_CHANGED"; + case accessibility::AccessibleEventId::HYPERTEXT_CHANGED: + return "HYPERTEXT_CHANGED"; + case accessibility::AccessibleEventId::INVALIDATE_ALL_CHILDREN: + return "INVALIDATE_ALL_CHILDREN"; + case accessibility::AccessibleEventId::LABELED_BY_RELATION_CHANGED: + return "LABELED_BY_RELATION_CHANGED"; + case accessibility::AccessibleEventId::LABEL_FOR_RELATION_CHANGED: + return "LABEL_FOR_RELATION_CHANGED"; + case accessibility::AccessibleEventId::LISTBOX_ENTRY_COLLAPSED: + return "LISTBOX_ENTRY_COLLAPSED"; + case accessibility::AccessibleEventId::LISTBOX_ENTRY_EXPANDED: + return "LISTBOX_ENTRY_EXPANDED"; + case accessibility::AccessibleEventId::MEMBER_OF_RELATION_CHANGED: + return "MEMBER_OF_RELATION_CHANGED"; + case accessibility::AccessibleEventId::NAME_CHANGED: + return "NAME_CHANGED"; + case accessibility::AccessibleEventId::PAGE_CHANGED: + return "PAGE_CHANGED"; + case accessibility::AccessibleEventId::ROLE_CHANGED: + return "ROLE_CHANGED"; + case accessibility::AccessibleEventId::SECTION_CHANGED: + return "SECTION_CHANGED"; + case accessibility::AccessibleEventId::SELECTION_CHANGED: + return "SELECTION_CHANGED"; + case accessibility::AccessibleEventId::SELECTION_CHANGED_ADD: + return "SELECTION_CHANGED_ADD"; + case accessibility::AccessibleEventId::SELECTION_CHANGED_REMOVE: + return "SELECTION_CHANGED_REMOVE"; + case accessibility::AccessibleEventId::SELECTION_CHANGED_WITHIN: + return "SELECTION_CHANGED_WITHIN"; + case accessibility::AccessibleEventId::STATE_CHANGED: + return "STATE_CHANGED"; + case accessibility::AccessibleEventId::SUB_WINDOW_OF_RELATION_CHANGED: + return "SUB_WINDOW_OF_RELATION_CHANGED"; + case accessibility::AccessibleEventId::TABLE_CAPTION_CHANGED: + return "TABLE_CAPTION_CHANGED"; + case accessibility::AccessibleEventId::TABLE_COLUMN_DESCRIPTION_CHANGED: + return "TABLE_COLUMN_DESCRIPTION_CHANGED"; + case accessibility::AccessibleEventId::TABLE_COLUMN_HEADER_CHANGED: + return "TABLE_COLUMN_HEADER_CHANGED"; + case accessibility::AccessibleEventId::TABLE_MODEL_CHANGED: + return "TABLE_MODEL_CHANGED"; + case accessibility::AccessibleEventId::TABLE_ROW_DESCRIPTION_CHANGED: + return "TABLE_ROW_DESCRIPTION_CHANGED"; + case accessibility::AccessibleEventId::TABLE_ROW_HEADER_CHANGED: + return "TABLE_ROW_HEADER_CHANGED"; + case accessibility::AccessibleEventId::TABLE_SUMMARY_CHANGED: + return "TABLE_SUMMARY_CHANGED"; + case accessibility::AccessibleEventId::TEXT_ATTRIBUTE_CHANGED: + return "TEXT_ATTRIBUTE_CHANGED"; + case accessibility::AccessibleEventId::TEXT_CHANGED: + return "TEXT_CHANGED"; + case accessibility::AccessibleEventId::TEXT_SELECTION_CHANGED: + return "TEXT_SELECTION_CHANGED"; + case accessibility::AccessibleEventId::VALUE_CHANGED: + return "VALUE_CHANGED"; + case accessibility::AccessibleEventId::VISIBLE_DATA_CHANGED: + return "VISIBLE_DATA_CHANGED"; + } + return unknownName(event_id); +} + +OUString AccessibilityTools::getRelationTypeName(const sal_Int16 rel_type) +{ + switch (rel_type) + { + case accessibility::AccessibleRelationType::CONTENT_FLOWS_FROM: + return "CONTENT_FLOWS_FROM"; + case accessibility::AccessibleRelationType::CONTENT_FLOWS_TO: + return "CONTENT_FLOWS_TO"; + case accessibility::AccessibleRelationType::CONTROLLED_BY: + return "CONTROLLED_BY"; + case accessibility::AccessibleRelationType::CONTROLLER_FOR: + return "CONTROLLER_FOR"; + case accessibility::AccessibleRelationType::DESCRIBED_BY: + return "DESCRIBED_BY"; + case accessibility::AccessibleRelationType::INVALID: + return "INVALID"; + case accessibility::AccessibleRelationType::LABELED_BY: + return "LABELED_BY"; + case accessibility::AccessibleRelationType::LABEL_FOR: + return "LABEL_FOR"; + case accessibility::AccessibleRelationType::MEMBER_OF: + return "MEMBER_OF"; + case accessibility::AccessibleRelationType::NODE_CHILD_OF: + return "NODE_CHILD_OF"; + case accessibility::AccessibleRelationType::SUB_WINDOW_OF: + return "SUB_WINDOW_OF"; + } + return unknownName(rel_type); +} + +OUString AccessibilityTools::debugName(accessibility::XAccessibleContext* ctx) +{ + return "role=" + AccessibilityTools::getRoleName(ctx->getAccessibleRole()) + " name=\"" + + ctx->getAccessibleName() + "\" description=\"" + ctx->getAccessibleDescription() + + "\""; +} + +OUString AccessibilityTools::debugName(accessibility::XAccessible* acc) +{ + return debugName(acc->getAccessibleContext().get()); +} + +OUString AccessibilityTools::debugName(accessibility::XAccessibleAction* xAct) +{ + OUStringBuffer r = "actions=["; + + const sal_Int32 nActions = xAct->getAccessibleActionCount(); + for (sal_Int32 i = 0; i < nActions; i++) + { + if (i > 0) + r.append(", "); + + r.append("description=\"" + xAct->getAccessibleActionDescription(i) + "\""); + + const auto& xKeyBinding = xAct->getAccessibleActionKeyBinding(i); + if (xKeyBinding) + { + r.append(" keybindings=["); + const sal_Int32 nKeyBindings = xKeyBinding->getAccessibleKeyBindingCount(); + for (sal_Int32 j = 0; j < nKeyBindings; j++) + { + if (j > 0) + r.append(", "); + + int k = 0; + for (const auto& keyStroke : xKeyBinding->getAccessibleKeyBinding(j)) + { + if (k++ > 0) + r.append(", "); + + r.append('"'); + if (keyStroke.Modifiers & awt::KeyModifier::MOD1) + r.append("<Mod1>"); + if (keyStroke.Modifiers & awt::KeyModifier::MOD2) + r.append("<Mod2>"); + if (keyStroke.Modifiers & awt::KeyModifier::MOD3) + r.append("<Mod3>"); + if (keyStroke.Modifiers & awt::KeyModifier::SHIFT) + r.append("<Shift>"); + r.append(OUStringChar(keyStroke.KeyChar) + "\""); + } + } + r.append("]"); + } + } + r.append("]"); + return r.makeStringAndClear(); +} + +OUString AccessibilityTools::debugName(accessibility::XAccessibleText* xTxt) +{ + uno::Reference<accessibility::XAccessibleContext> xCtx(xTxt, uno::UNO_QUERY); + return debugName(xCtx.get()); +} + +OUString AccessibilityTools::debugName(const accessibility::AccessibleEventObject* evobj) +{ + return "(AccessibleEventObject) { id=" + getEventIdName(evobj->EventId) + + " old=" + evobj->OldValue.getValueTypeName() + + " new=" + evobj->NewValue.getValueTypeName() + " }"; +} + +bool AccessibilityTools::Await(const std::function<bool()>& cUntilCallback, sal_uInt64 nTimeoutMs) +{ + bool success = false; + Timer aTimer("wait for event"); + aTimer.SetTimeout(nTimeoutMs); + aTimer.Start(); + do + { + Scheduler::ProcessEventsToIdle(); + success = cUntilCallback(); + } while (!success && aTimer.IsActive()); + SAL_WARN_IF(!success, "test", "timeout reached"); + return success; +} + +void AccessibilityTools::Wait(sal_uInt64 nTimeoutMs) +{ + Timer aTimer("wait for event"); + aTimer.SetTimeout(nTimeoutMs); + aTimer.Start(); + std::cout << "waiting for " << nTimeoutMs << "ms... "; + do + { + Scheduler::ProcessEventsToIdle(); + } while (aTimer.IsActive()); + std::cout << "ok." << std::endl; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/test/source/a11y/accessibletestbase.cxx b/test/source/a11y/accessibletestbase.cxx new file mode 100644 index 0000000000..92a40e87f7 --- /dev/null +++ b/test/source/a11y/accessibletestbase.cxx @@ -0,0 +1,581 @@ +/* -*- 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 <test/a11y/accessibletestbase.hxx> + +#include <string> + +#include <com/sun/star/accessibility/AccessibleRole.hpp> +#include <com/sun/star/accessibility/AccessibleStateType.hpp> +#include <com/sun/star/accessibility/XAccessible.hpp> +#include <com/sun/star/accessibility/XAccessibleAction.hpp> +#include <com/sun/star/accessibility/XAccessibleContext.hpp> +#include <com/sun/star/awt/XDialog2.hpp> +#include <com/sun/star/awt/XExtendedToolkit.hpp> +#include <com/sun/star/awt/XTopWindow.hpp> +#include <com/sun/star/awt/XTopWindowListener.hpp> +#include <com/sun/star/frame/Desktop.hpp> +#include <com/sun/star/frame/FrameSearchFlag.hpp> +#include <com/sun/star/frame/XFrame.hpp> +#include <com/sun/star/frame/XFrame2.hpp> +#include <com/sun/star/frame/XModel.hpp> +#include <com/sun/star/uno/Reference.hxx> +#include <com/sun/star/uno/RuntimeException.hpp> +#include <com/sun/star/util/XCloseable.hpp> + +#include <vcl/idle.hxx> +#include <vcl/scheduler.hxx> +#include <vcl/svapp.hxx> + +#include <cppuhelper/implbase.hxx> + +#include <test/a11y/AccessibilityTools.hxx> + +using namespace css; + +void test::AccessibleTestBase::setUp() +{ + test::BootstrapFixture::setUp(); + + mxDesktop = frame::Desktop::create(mxComponentContext); +} + +void test::AccessibleTestBase::close() +{ + if (mxDocument.is()) + { + uno::Reference<util::XCloseable> xCloseable(mxDocument, uno::UNO_QUERY_THROW); + xCloseable->close(false); + mxDocument.clear(); + } +} + +void test::AccessibleTestBase::tearDown() { close(); } + +void test::AccessibleTestBase::load(const rtl::OUString& sURL) +{ + // make sure there is no open document in case it is called more than once + close(); + mxDocument = mxDesktop->loadComponentFromURL(sURL, "_blank", frame::FrameSearchFlag::AUTO, {}); + + uno::Reference<frame::XModel> xModel(mxDocument, uno::UNO_QUERY_THROW); + mxWindow.set(xModel->getCurrentController()->getFrame()->getContainerWindow()); + + // bring window to front + uno::Reference<awt::XTopWindow> xTopWindow(mxWindow, uno::UNO_QUERY_THROW); + xTopWindow->toFront(); +} + +void test::AccessibleTestBase::loadFromSrc(const rtl::OUString& sSrcPath) +{ + load(m_directories.getURLFromSrc(sSrcPath)); +} + +uno::Reference<accessibility::XAccessibleContext> +test::AccessibleTestBase::getWindowAccessibleContext() +{ + uno::Reference<accessibility::XAccessible> xAccessible(mxWindow, uno::UNO_QUERY_THROW); + + return xAccessible->getAccessibleContext(); +} + +bool test::AccessibleTestBase::isDocumentRole(const sal_Int16 role) +{ + return (role == accessibility::AccessibleRole::DOCUMENT + || role == accessibility::AccessibleRole::DOCUMENT_PRESENTATION + || role == accessibility::AccessibleRole::DOCUMENT_SPREADSHEET + || role == accessibility::AccessibleRole::DOCUMENT_TEXT); +} + +uno::Reference<accessibility::XAccessibleContext> +test::AccessibleTestBase::getDocumentAccessibleContext() +{ + uno::Reference<frame::XModel> xModel(mxDocument, uno::UNO_QUERY_THROW); + uno::Reference<accessibility::XAccessible> xAccessible( + xModel->getCurrentController()->getFrame()->getComponentWindow(), uno::UNO_QUERY_THROW); + + return AccessibilityTools::getAccessibleObjectForPredicate( + xAccessible->getAccessibleContext(), + [](const uno::Reference<accessibility::XAccessibleContext>& xCtx) { + return (isDocumentRole(xCtx->getAccessibleRole()) + && xCtx->getAccessibleStateSet() & accessibility::AccessibleStateType::SHOWING); + }); +} + +uno::Reference<accessibility::XAccessibleContext> +test::AccessibleTestBase::getFirstRelationTargetOfType( + const uno::Reference<accessibility::XAccessibleContext>& xContext, sal_Int16 relationType) +{ + auto relset = xContext->getAccessibleRelationSet(); + + if (relset.is()) + { + for (sal_Int32 i = 0; i < relset->getRelationCount(); ++i) + { + const auto& rel = relset->getRelation(i); + if (rel.RelationType == relationType) + { + for (auto& target : rel.TargetSet) + { + uno::Reference<accessibility::XAccessible> targetAccessible(target, + uno::UNO_QUERY); + if (targetAccessible.is()) + return targetAccessible->getAccessibleContext(); + } + } + } + } + + return nullptr; +} + +std::deque<uno::Reference<accessibility::XAccessibleContext>> +test::AccessibleTestBase::getAllChildren( + const uno::Reference<accessibility::XAccessibleContext>& xContext) +{ + std::deque<uno::Reference<accessibility::XAccessibleContext>> children; + auto childCount = xContext->getAccessibleChildCount(); + + for (sal_Int64 i = 0; i < childCount && i < AccessibilityTools::MAX_CHILDREN; i++) + { + auto child = xContext->getAccessibleChild(i); + children.push_back(child->getAccessibleContext()); + } + + return children; +} + +/** Prints the tree of accessible objects starting at @p xContext to stdout */ +void test::AccessibleTestBase::dumpA11YTree( + const uno::Reference<accessibility::XAccessibleContext>& xContext, const int depth) +{ + Scheduler::ProcessEventsToIdle(); + auto xRelSet = xContext->getAccessibleRelationSet(); + + std::cout << AccessibilityTools::debugString(xContext); + /* relation set is not included in AccessibilityTools::debugString(), but might be useful in + * this context, so we compute it here */ + if (xRelSet.is()) + { + auto relCount = xRelSet->getRelationCount(); + if (relCount) + { + std::cout << " rels=["; + for (sal_Int32 i = 0; i < relCount; ++i) + { + if (i > 0) + std::cout << ", "; + + const auto& rel = xRelSet->getRelation(i); + std::cout << "(type=" << AccessibilityTools::getRelationTypeName(rel.RelationType) + << " (" << rel.RelationType << ")"; + std::cout << " targets=["; + int j = 0; + for (auto& target : rel.TargetSet) + { + if (j++ > 0) + std::cout << ", "; + uno::Reference<accessibility::XAccessible> ta(target, uno::UNO_QUERY_THROW); + std::cout << AccessibilityTools::debugString(ta); + } + std::cout << "])"; + } + std::cout << "]"; + } + } + std::cout << std::endl; + + sal_Int32 i = 0; + for (auto& child : getAllChildren(xContext)) + { + for (int j = 0; j < depth; j++) + std::cout << " "; + std::cout << " * child " << i++ << ": "; + dumpA11YTree(child, depth + 1); + } +} + +/** Gets a child by name (usually in a menu) */ +uno::Reference<accessibility::XAccessibleContext> test::AccessibleTestBase::getItemFromName( + const uno::Reference<accessibility::XAccessibleContext>& xMenuCtx, std::u16string_view name) +{ + auto childCount = xMenuCtx->getAccessibleChildCount(); + + std::cout << "looking up item " << OUString(name) << " in " + << AccessibilityTools::debugString(xMenuCtx) << std::endl; + for (sal_Int64 i = 0; i < childCount && i < AccessibilityTools::MAX_CHILDREN; i++) + { + auto item = xMenuCtx->getAccessibleChild(i)->getAccessibleContext(); + if (AccessibilityTools::nameEquals(item, name)) + { + std::cout << "-> found " << AccessibilityTools::debugString(item) << std::endl; + return item; + } + } + + std::cout << "-> NOT FOUND!" << std::endl; + std::cout << " Contents was: "; + dumpA11YTree(xMenuCtx, 1); + + return uno::Reference<accessibility::XAccessibleContext>(); +} + +bool test::AccessibleTestBase::activateMenuItem( + const uno::Reference<accessibility::XAccessibleAction>& xAction) +{ + // assume first action is the right one, there's not description anyway + CPPUNIT_ASSERT_EQUAL(sal_Int32(1), xAction->getAccessibleActionCount()); + if (xAction->doAccessibleAction(0)) + { + Scheduler::ProcessEventsToIdle(); + return true; + } + return false; +} + +uno::Reference<accessibility::XAccessibleContext> test::AccessibleTestBase::getFocusedObject( + const uno::Reference<accessibility::XAccessibleContext>& xCtx) +{ + return AccessibilityTools::getAccessibleObjectForPredicate( + xCtx, [](const uno::Reference<accessibility::XAccessibleContext>& xCandidateCtx) { + const auto states = (accessibility::AccessibleStateType::FOCUSED + | accessibility::AccessibleStateType::SHOWING); + return (xCandidateCtx->getAccessibleStateSet() & states) == states; + }); +} + +uno::Reference<accessibility::XAccessibleContext> +test::AccessibleTestBase::tabTo(const uno::Reference<accessibility::XAccessible>& xRoot, + const sal_Int16 role, const std::u16string_view name, + const EventPosterHelperBase* pEventPosterHelper) +{ + AccessibleEventPosterHelper eventHelper; + if (!pEventPosterHelper) + { + eventHelper.setWindow(xRoot); + pEventPosterHelper = &eventHelper; + } + + auto xOriginalFocus = getFocusedObject(xRoot); + auto xFocus = xOriginalFocus; + int nSteps = 0; + + std::cout << "Tabbing to '" << OUString(name) << "'..." << std::endl; + while (xFocus && (nSteps == 0 || xFocus != xOriginalFocus)) + { + std::cout << " focused object is: " << AccessibilityTools::debugString(xFocus) + << std::endl; + if (xFocus->getAccessibleRole() == role && AccessibilityTools::nameEquals(xFocus, name)) + { + std::cout << " -> OK, focus matches" << std::endl; + return xFocus; + } + if (++nSteps > 100) + { + std::cerr << "Object not found after tabbing 100 times! bailing out" << std::endl; + break; + } + + std::cout << " -> no match, sending <TAB>" << std::endl; + pEventPosterHelper->postKeyEventAsync(0, awt::Key::TAB); + Scheduler::ProcessEventsToIdle(); + + const auto xPrevFocus = xFocus; + xFocus = getFocusedObject(xRoot); + if (!xFocus) + std::cerr << "Focus lost after sending <TAB>!" << std::endl; + else if (xPrevFocus == xFocus) + { + std::cerr << "Focus didn't move after sending <TAB>! bailing out" << std::endl; + std::cerr << "Focused object(s):" << std::endl; + int iFocusedCount = 0; + // count and print out objects with focused state + AccessibilityTools::getAccessibleObjectForPredicate( + xRoot, + [&iFocusedCount](const uno::Reference<accessibility::XAccessibleContext>& xCtx) { + const auto states = (accessibility::AccessibleStateType::FOCUSED + | accessibility::AccessibleStateType::SHOWING); + if ((xCtx->getAccessibleStateSet() & states) == states) + { + std::cerr << " * " << AccessibilityTools::debugString(xCtx) << std::endl; + iFocusedCount++; + } + return false; // keep going + }); + std::cerr << "Total focused element(s): " << iFocusedCount << std::endl; + if (iFocusedCount > 1) + std::cerr << "WARNING: there are more than one focused object! This usually means " + "there is a BUG in the focus handling of that accessibility tree." + << std::endl; + break; + } + } + + std::cerr << "NOT FOUND" << std::endl; + return nullptr; +} + +bool test::AccessibleTestBase::tabTo( + const uno::Reference<accessibility::XAccessible>& xRoot, + const uno::Reference<accessibility::XAccessibleContext>& xChild, + const EventPosterHelperBase* pEventPosterHelper) +{ + AccessibleEventPosterHelper eventHelper; + if (!pEventPosterHelper) + { + eventHelper.setWindow(xRoot); + pEventPosterHelper = &eventHelper; + } + + std::cout << "Tabbing to " << AccessibilityTools::debugString(xChild) << "..." << std::endl; + for (int i = 0; i < 100; i++) + { + if (xChild->getAccessibleStateSet() & accessibility::AccessibleStateType::FOCUSED) + return true; + + std::cout << " no match, sending <TAB>" << std::endl; + pEventPosterHelper->postKeyEventAsync(0, awt::Key::TAB); + Scheduler::ProcessEventsToIdle(); + } + + std::cerr << "NOT FOUND" << std::endl; + return false; +} + +#if !defined(MACOSX) +/* Dialog handling + * + * For now this doesn't actually work under macos, so the API is not available there not to create + * confusion. The problem there is we don't get notified of new dialogs, so we can't manage them + * or interact with them. + */ + +test::AccessibleTestBase::Dialog::Dialog(uno::Reference<awt::XDialog2>& xDialog2, bool bAutoClose) + : mbAutoClose(bAutoClose) + , mxDialog2(xDialog2) +{ + CPPUNIT_ASSERT(xDialog2.is()); + + mxAccessible.set(xDialog2, uno::UNO_QUERY); + if (mxAccessible) + setWindow(mxAccessible); + else + { + std::cerr << "WARNING: AccessibleTestBase::Dialog() constructed with awt::XDialog2 '" + << xDialog2->getTitle() + << "' not implementing accessibility::XAccessible. Event delivery will not work." + << std::endl; + } +} + +test::AccessibleTestBase::Dialog::~Dialog() +{ + if (mbAutoClose) + close(); +} + +void test::AccessibleTestBase::Dialog::close(sal_Int32 result) +{ + if (mxDialog2) + { + mxDialog2->endDialog(result); + mxDialog2.clear(); + } +} + +std::shared_ptr<test::AccessibleTestBase::DialogWaiter> +test::AccessibleTestBase::awaitDialog(const std::u16string_view name, + std::function<void(Dialog&)> callback, bool bAutoClose) +{ + /* Helper class to wait on a dialog to pop up and to close, running user code between the + * two. This has to work both for "other window"-style dialogues (non-modal), as well as + * for modal dialogues using Dialog::Execute() (which runs a nested main loop, hence + * blocking our test flow execution. + * The approach here is to wait on the WindowActivate event for the dialog, and run the + * test code in there. Then, close the dialog if not already done, resuming normal flow to + * the caller. */ + class ListenerHelper : public DialogWaiter + { + DialogCancelMode miPreviousDialogCancelMode; + uno::Reference<awt::XExtendedToolkit> mxToolkit; + bool mbWaitingForDialog; + std::exception_ptr mpException; + std::u16string_view msName; + std::function<void(Dialog&)> mCallback; + bool mbAutoClose; + Timer maTimeoutTimer; + Idle maIdleHandler; + uno::Reference<awt::XTopWindowListener> mxTopWindowListener; + std::unique_ptr<Dialog> mxDialog; + + public: + virtual ~ListenerHelper() + { + Application::SetDialogCancelMode(miPreviousDialogCancelMode); + mxToolkit->removeTopWindowListener(mxTopWindowListener); + maTimeoutTimer.Stop(); + maIdleHandler.Stop(); + } + + ListenerHelper(const std::u16string_view& name, std::function<void(Dialog&)> callback, + bool bAutoClose) + : mbWaitingForDialog(true) + , msName(name) + , mCallback(callback) + , mbAutoClose(bAutoClose) + , maTimeoutTimer("workaround timer if we don't catch WindowActivate") + , maIdleHandler("runs user callback in idle time") + { + mxTopWindowListener.set(new MyTopWindowListener(this)); + mxToolkit.set(Application::GetVCLToolkit(), uno::UNO_QUERY_THROW); + mxToolkit->addTopWindowListener(mxTopWindowListener); + + maTimeoutTimer.SetInvokeHandler(LINK(this, ListenerHelper, timeoutTimerHandler)); + maTimeoutTimer.SetTimeout(60000); + maTimeoutTimer.Start(); + + maIdleHandler.SetInvokeHandler(LINK(this, ListenerHelper, idleHandler)); + maIdleHandler.SetPriority(TaskPriority::DEFAULT_IDLE); + + miPreviousDialogCancelMode = Application::GetDialogCancelMode(); + Application::SetDialogCancelMode(DialogCancelMode::Off); + } + + private: + // mimic IMPL_LINK inline + static void LinkStubtimeoutTimerHandler(void* instance, Timer* timer) + { + static_cast<ListenerHelper*>(instance)->timeoutTimerHandler(timer); + } + + void timeoutTimerHandler(Timer*) + { + std::cerr << "timeout waiting for dialog '" << OUString(msName) << "' to show up" + << std::endl; + + assert(mbWaitingForDialog); + + // This is not very nice, but it should help fail earlier if we never catch the dialog + // yet we're in a sub-loop and waitEndDialog() didn't have a chance to run yet. + throw new css::uno::RuntimeException("Timeout waiting for dialog"); + } + + class MyTopWindowListener : public ::cppu::WeakImplHelper<awt::XTopWindowListener> + { + private: + ListenerHelper* mpHelper; + + public: + MyTopWindowListener(ListenerHelper* pHelper) + : mpHelper(pHelper) + { + assert(mpHelper); + } + + // XTopWindowListener + virtual void SAL_CALL windowOpened(const lang::EventObject&) override {} + virtual void SAL_CALL windowClosing(const lang::EventObject&) override {} + virtual void SAL_CALL windowClosed(const lang::EventObject&) override {} + virtual void SAL_CALL windowMinimized(const lang::EventObject&) override {} + virtual void SAL_CALL windowNormalized(const lang::EventObject&) override {} + virtual void SAL_CALL windowDeactivated(const lang::EventObject&) override {} + virtual void SAL_CALL windowActivated(const lang::EventObject& xEvent) override + { + assert(mpHelper->mbWaitingForDialog); + + if (!xEvent.Source) + return; + + uno::Reference<awt::XDialog2> xDialog(xEvent.Source, uno::UNO_QUERY); + if (!xDialog) + return; + + // remove ourselves, we don't want to run again + mpHelper->mxToolkit->removeTopWindowListener(this); + + mpHelper->mxDialog = std::make_unique<Dialog>(xDialog, true); + + mpHelper->maIdleHandler.Start(); + } + + // XEventListener + virtual void SAL_CALL disposing(const lang::EventObject&) override {} + }; + + // mimic IMPL_LINK inline + static void LinkStubidleHandler(void* instance, Timer* idle) + { + static_cast<ListenerHelper*>(instance)->idleHandler(idle); + } + + void idleHandler(Timer*) + { + mbWaitingForDialog = false; + + maTimeoutTimer.ClearInvokeHandler(); + maTimeoutTimer.Stop(); + + /* The popping up dialog ought to be the right one, or something's fishy and + * we're bound to failure (e.g. waiting on a dialog that either will never come, or + * that will not run after the current one -- deadlock style) */ + if (msName != mxDialog->getWindow()->GetText()) + { + mpException = std::make_exception_ptr(css::uno::RuntimeException( + "Unexpected dialog '" + mxDialog->getWindow()->GetText() + + "' opened instead of the expected '" + msName + "'")); + } + else + { + std::cout << "found dialog, calling user callback" << std::endl; + + // set the real requested auto close now we're just calling the user callback + mxDialog->setAutoClose(mbAutoClose); + + try + { + mCallback(*mxDialog); + } + catch (...) + { + mpException = std::current_exception(); + } + } + + mxDialog.reset(); + } + + public: + virtual bool waitEndDialog(sal_uInt64 nTimeoutMs) override + { + /* Usually this loop will actually never run at all because a previous + * Scheduler::ProcessEventsToIdle() would have triggered the dialog already, but we + * can't be sure of that or of delays, so be safe and wait with a timeout. */ + if (mbWaitingForDialog) + { + Timer aTimer("wait for dialog"); + aTimer.SetTimeout(nTimeoutMs); + aTimer.Start(); + do + { + Application::Yield(); + } while (mbWaitingForDialog && aTimer.IsActive()); + } + + if (mpException) + std::rethrow_exception(mpException); + + return !mbWaitingForDialog; + } + }; + + return std::make_shared<ListenerHelper>(name, callback, bAutoClose); +} +#endif //defined(MACOSX) + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/test/source/a11y/eventposter.cxx b/test/source/a11y/eventposter.cxx new file mode 100644 index 0000000000..39e178e227 --- /dev/null +++ b/test/source/a11y/eventposter.cxx @@ -0,0 +1,46 @@ +/* -*- 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 <test/a11y/eventposter.hxx> + +#include <com/sun/star/accessibility/XAccessible.hpp> +#include <com/sun/star/accessibility/XAccessibleContext.hpp> +#include <com/sun/star/uno/Reference.hxx> + +#include <sfx2/lokhelper.hxx> +#include <test/a11y/AccessibilityTools.hxx> +#include <toolkit/awt/vclxwindow.hxx> + +void test::EventPosterHelper::postKeyEventAsync(int nType, int nCharCode, int nKeyCode) const +{ + SfxLokHelper::postKeyEventAsync(mxWindow, nType, nCharCode, nKeyCode); +} + +void test::EventPosterHelper::postExtTextEventAsync(int nType, const OUString& rText) const +{ + SfxLokHelper::postExtTextEventAsync(mxWindow, nType, rText); +} + +void test::AccessibleEventPosterHelper::setWindow( + css::uno::Reference<css::accessibility::XAccessible> xAcc) +{ + while (auto xParent = xAcc->getAccessibleContext()->getAccessibleParent()) + xAcc = xParent; + auto vclXWindow = dynamic_cast<VCLXWindow*>(xAcc.get()); + if (!vclXWindow) + { + std::cerr << "WARNING: AccessibleEventPosterHelper::setWindow() called on " + "unsupported object " + << AccessibilityTools::debugString(xAcc) << ". Event delivery will not work." + << std::endl; + } + mxWindow = vclXWindow ? vclXWindow->GetWindow() : nullptr; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/test/source/a11y/swaccessibletestbase.cxx b/test/source/a11y/swaccessibletestbase.cxx new file mode 100644 index 0000000000..b43d65c0cf --- /dev/null +++ b/test/source/a11y/swaccessibletestbase.cxx @@ -0,0 +1,135 @@ +/* -*- 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 <test/a11y/swaccessibletestbase.hxx> + +#include <com/sun/star/accessibility/AccessibleRelationType.hpp> +#include <com/sun/star/accessibility/XAccessibleContext.hpp> +#include <com/sun/star/accessibility/XAccessibleText.hpp> +#include <com/sun/star/uno/Reference.hxx> + +#include <rtl/ustrbuf.hxx> + +#include <test/a11y/AccessibilityTools.hxx> + +using namespace css; + +uno::Reference<accessibility::XAccessibleContext> +test::SwAccessibleTestBase::getPreviousFlowingSibling( + const uno::Reference<accessibility::XAccessibleContext>& xContext) +{ + return getFirstRelationTargetOfType(xContext, + accessibility::AccessibleRelationType::CONTENT_FLOWS_FROM); +} + +uno::Reference<accessibility::XAccessibleContext> test::SwAccessibleTestBase::getNextFlowingSibling( + const uno::Reference<accessibility::XAccessibleContext>& xContext) +{ + return getFirstRelationTargetOfType(xContext, + accessibility::AccessibleRelationType::CONTENT_FLOWS_TO); +} + +/* Care has to be taken not to walk sideways as the relation is also used + * with children of nested containers (possibly as the "natural"/"perceived" flow?). */ +std::deque<uno::Reference<accessibility::XAccessibleContext>> +test::SwAccessibleTestBase::getAllChildren( + const uno::Reference<accessibility::XAccessibleContext>& xContext) +{ + /* first, get all "natural" children */ + auto children = AccessibleTestBase::getAllChildren(xContext); + if (!children.size()) + return children; + + /* then, try and find flowing siblings at the same levels that are not included in the list */ + /* first, backwards: */ + auto child = getPreviousFlowingSibling(children.front()); + while (child.is() && children.size() < AccessibilityTools::MAX_CHILDREN) + { + auto childParent = child->getAccessibleParent(); + if (childParent.is() + && AccessibilityTools::equals(xContext, childParent->getAccessibleContext())) + children.push_front(child); + child = getPreviousFlowingSibling(child); + } + /* then forward */ + child = getNextFlowingSibling(children.back()); + while (child.is() && children.size() < AccessibilityTools::MAX_CHILDREN) + { + auto childParent = child->getAccessibleParent(); + if (childParent.is() + && AccessibilityTools::equals(xContext, childParent->getAccessibleContext())) + children.push_back(child); + child = getNextFlowingSibling(child); + } + + return children; +} + +void test::SwAccessibleTestBase::collectText( + const uno::Reference<accessibility::XAccessibleContext>& xContext, rtl::OUStringBuffer& buffer, + bool onlyChildren) +{ + const auto& roleName = AccessibilityTools::getRoleName(xContext->getAccessibleRole()); + + std::cout << "collecting text for child of role " << roleName << "..." << std::endl; + + if (!onlyChildren) + { + const struct + { + std::u16string_view name; + rtl::OUString value; + } attrs[] = { + { u"name", xContext->getAccessibleName() }, + { u"description", xContext->getAccessibleDescription() }, + }; + + buffer.append('<'); + buffer.append(roleName); + for (auto& attr : attrs) + { + if (attr.value.getLength() == 0) + continue; + buffer.append(' '); + buffer.append(attr.name); + buffer.append(u"=\"" + attr.value.replaceAll(u"\"", u""") + "\""); + } + buffer.append('>'); + } + auto openTagLength = buffer.getLength(); + + uno::Reference<accessibility::XAccessibleText> xText(xContext, uno::UNO_QUERY); + if (xText.is()) + buffer.append(xText->getText()); + + for (auto& childContext : getAllChildren(xContext)) + collectText(childContext, buffer); + + if (!onlyChildren) + { + if (buffer.getLength() != openTagLength) + buffer.append("</" + roleName + ">"); + else + { + /* there was no content, so make is a short tag for more concise output */ + buffer[openTagLength - 1] = '/'; + buffer.append('>'); + } + } +} + +OUString test::SwAccessibleTestBase::collectText( + const uno::Reference<accessibility::XAccessibleContext>& xContext) +{ + rtl::OUStringBuffer buf; + collectText(xContext, buf, isDocumentRole(xContext->getAccessibleRole())); + return buf.makeStringAndClear(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ |