diff options
-rw-r--r-- | compilerplugins/clang/staticmethods.cxx | 4 | ||||
-rw-r--r-- | config_host.mk.in | 5 | ||||
-rw-r--r-- | config_host/config_atspi.h.in | 14 | ||||
-rw-r--r-- | configure.ac | 62 | ||||
-rw-r--r-- | distro-configs/Jenkins/linux_clang_dbgutil_64 | 1 | ||||
-rw-r--r-- | distro-configs/Jenkins/linux_gcc_release_64 | 1 | ||||
-rw-r--r-- | include/test/a11y/AccessibilityTools.hxx | 2 | ||||
-rw-r--r-- | test/source/a11y/AccessibilityTools.cxx | 6 | ||||
-rw-r--r-- | vcl/CppunitTest_vcl_gtk3_a11y.mk | 61 | ||||
-rw-r--r-- | vcl/Module_vcl.mk | 6 | ||||
-rw-r--r-- | vcl/qa/cppunit/a11y/atspi2/atspi2.cxx | 498 | ||||
-rw-r--r-- | vcl/qa/cppunit/a11y/atspi2/atspi2.hxx | 45 | ||||
-rw-r--r-- | vcl/qa/cppunit/a11y/atspi2/atspi2testbase.hxx | 94 | ||||
-rw-r--r-- | vcl/qa/cppunit/a11y/atspi2/atspi2text.cxx | 1017 | ||||
-rw-r--r-- | vcl/qa/cppunit/a11y/atspi2/atspiwrapper.cxx | 22 | ||||
-rw-r--r-- | vcl/qa/cppunit/a11y/atspi2/atspiwrapper.hxx | 784 | ||||
-rw-r--r-- | vcl/qa/cppunit/a11y/atspi2/testdocuments/ecclectic.fodt | 258 |
17 files changed, 2879 insertions, 1 deletions
diff --git a/compilerplugins/clang/staticmethods.cxx b/compilerplugins/clang/staticmethods.cxx index 4651a4a3d060..38180c1daa2c 100644 --- a/compilerplugins/clang/staticmethods.cxx +++ b/compilerplugins/clang/staticmethods.cxx @@ -184,7 +184,9 @@ bool StaticMethods::TraverseCXXMethodDecl(const CXXMethodDecl * pCXXMethodDecl) .GlobalNamespace()) || (fdc.Function("Read_Majority").Class("SwWW8ImplReader") .GlobalNamespace()) - || fdc.Function("Ignore").Class("SwWrtShell").GlobalNamespace()) + || fdc.Function("Ignore").Class("SwWrtShell").GlobalNamespace() + || (cdc.Class("AttributesChecker").AnonymousNamespace().GlobalNamespace() + && startsWith(pCXXMethodDecl->getNameAsString(), "check"))) { return true; } diff --git a/config_host.mk.in b/config_host.mk.in index acb868b8f05f..85089efb254e 100644 --- a/config_host.mk.in +++ b/config_host.mk.in @@ -31,6 +31,8 @@ export ASSERT_ALWAYS_ABORT=@ASSERT_ALWAYS_ABORT@ export ATL_INCLUDE=@ATL_INCLUDE@ export ATL_LIB=@ATL_LIB@ export ATOMIC_LIB=@ATOMIC_LIB@ +export ATSPI2_CFLAGS=$(gb_SPACE)@ATSPI2_CFLAGS@ +export ATSPI2_LIBS=$(gb_SPACE)@ATSPI2_LIBS@ export AVAHI_CFLAGS=$(gb_SPACE)@AVAHI_CFLAGS@ export AVAHI_LIBS=$(gb_SPACE)@AVAHI_LIBS@ export LIBATOMIC_OPS_CFLAGS=$(gb_SPACE)@LIBATOMIC_OPS_CFLAGS@ @@ -123,6 +125,7 @@ export DBUS_LIBS=$(gb_SPACE)@DBUS_LIBS@ export DBUS_GLIB_CFLAGS=$(gb_SPACE)@DBUS_GLIB_CFLAGS@ export DBUS_GLIB_LIBS=$(gb_SPACE)@DBUS_GLIB_LIBS@ export DBUS_HAVE_GLIB=@DBUS_HAVE_GLIB@ +export DBUS_LAUNCH=@DBUS_LAUNCH@ export DCONF_CFLAGS=@DCONF_CFLAGS@ export DCONF_LIBS=@DCONF_LIBS@ export DEFAULT_BRAND_IMAGES=@DEFAULT_BRAND_IMAGES@ @@ -153,6 +156,7 @@ export ENABLE_COINMP=@ENABLE_COINMP@ SYSTEM_COINMP=@SYSTEM_COINMP@ export COINMP_CFLAGS=@COINMP_CFLAGS@ export COINMP_LIBS=@COINMP_LIBS@ +export ENABLE_ATSPI_TESTS=@ENABLE_ATSPI_TESTS@ export ENABLE_CUPS=@ENABLE_CUPS@ export ENABLE_CURL=@ENABLE_CURL@ export ENABLE_DBGUTIL=@ENABLE_DBGUTIL@ @@ -757,6 +761,7 @@ export XRANDR_LIBS=$(gb_SPACE)@XRANDR_LIBS@ export XRENDER_CFLAGS=$(gb_SPACE)@XRENDER_CFLAGS@ export XRENDER_LIBS=$(gb_SPACE)@XRENDER_LIBS@ export XSLTPROC=@XSLTPROC@ +export XVFB_RUN=@XVFB_RUN@ export ZLIB_CFLAGS=$(gb_SPACE)@ZLIB_CFLAGS@ export ZLIB_LIBS=$(gb_SPACE)@ZLIB_LIBS@ export ZMF_CFLAGS=$(gb_SPACE)@ZMF_CFLAGS@ diff --git a/config_host/config_atspi.h.in b/config_host/config_atspi.h.in new file mode 100644 index 000000000000..7a4a48f2d2f1 --- /dev/null +++ b/config_host/config_atspi.h.in @@ -0,0 +1,14 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * 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/. + */ + +#pragma once + +#define HAVE_ATSPI2_SCROLL_TO 0 + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/configure.ac b/configure.ac index 3dea1ce2bd47..ca0bdbcadfe7 100644 --- a/configure.ac +++ b/configure.ac @@ -1199,6 +1199,8 @@ test "${test_gtk3_kde5+set}" != set -a "$test_kf5" = yes -a "$test_gtk3" = yes & test "${test_system_fontconfig+set}" != set -a "${test_system_freetype+set}" = set && test_system_fontconfig="$test_system_freetype" test "${test_system_freetype+set}" != set -a "${test_system_fontconfig+set}" = set && test_system_freetype="$test_system_fontconfig" +test "${test_atspi_tests+set}" = set || test_atspi_tests="$test_gtk3" + # convenience / platform overriding "fixes" # Don't sort! test "$test_kf5" = yes -a "$test_qt5" = no && test_kf5=no @@ -1711,6 +1713,10 @@ AC_ARG_ENABLE(gtk4, AS_HELP_STRING([--enable-gtk4], [Determines whether to use Gtk+ 4.0 vclplug on platforms where Gtk+ 4.0 is available.])) +AC_ARG_ENABLE(atspi-tests, + AS_HELP_STRING([--disable-atspi-tests], + [Determines whether to enable AT-SPI2 tests for the GTK3 vclplug.])) + AC_ARG_ENABLE(introspection, AS_HELP_STRING([--enable-introspection], [Generate files for GObject introspection. Requires --enable-gtk3. (Typically used by @@ -11776,6 +11782,61 @@ if test "$enable_introspection" = yes; then fi fi +# AT-SPI2 tests require gtk3, xvfb-run, dbus-launch and atspi-2 +test enable_atspi_tests = yes && test_atspi_tests=yes +if test "$test_atspi_tests" = yes && ! test "$ENABLE_GTK3" = TRUE; then + if test "$enable_atspi_tests" = yes; then + AC_MSG_ERROR([--enable-atspi-tests requires --enable-gtk3]) + fi + test_atspi_tests=no +fi +if test "$test_atspi_tests" = yes; then + AC_PATH_PROGS([XVFB_RUN], [xvfb-run], no) + if ! test "$XVFB_RUN" = no; then + dnl make sure the found xvfb-run actually works + AC_MSG_CHECKING([whether $XVFB_RUN works...]) + if $XVFB_RUN true >&AS_MESSAGE_LOG_FD 2>&AS_MESSAGE_LOG_FD; then + AC_MSG_RESULT([yes]) + else + AC_MSG_RESULT([no]) + XVFB_RUN=no + fi + fi + if test "$XVFB_RUN" = no; then + if test "$enable_atspi_tests" = yes; then + AC_MSG_ERROR([xvfb-run required by --enable-atspi-tests not found]) + fi + test_atspi_tests=no + fi +fi +if test "$test_atspi_tests" = yes; then + AC_PATH_PROGS([DBUS_LAUNCH], [dbus-launch], no) + if test "$DBUS_LAUNCH" = no; then + if test "$enable_atspi_tests" = yes; then + AC_MSG_ERROR([dbus-launch required by --enable-atspi-tests not found]) + fi + test_atspi_tests=no + fi +fi +if test "$test_atspi_tests" = yes; then + PKG_CHECK_MODULES([ATSPI2], [atspi-2 gobject-2.0],, + [if test "$enable_atspi_tests" = yes; then + AC_MSG_ERROR([$ATSPI2_PKG_ERRORS]) + else + test_atspi_tests=no + fi]) +fi +if test "x$test_atspi_tests" = xyes; then + PKG_CHECK_MODULES([ATSPI2_2_32], [atspi-2 >= 2.32], + [have_atspi_scroll_to=1], + [have_atspi_scroll_to=0]) + AC_DEFINE_UNQUOTED([HAVE_ATSPI2_SCROLL_TO], [$have_atspi_scroll_to], + [Whether AT-SPI2 has the scrollTo API]) +fi +ENABLE_ATSPI_TESTS= +test "$test_atspi_tests" = yes && ENABLE_ATSPI_TESTS=TRUE +AC_SUBST([ENABLE_ATSPI_TESTS]) + dnl =================================================================== dnl check for dbus support dnl =================================================================== @@ -14936,6 +14997,7 @@ AC_CONFIG_FILES([config_host.mk sysui/desktop/macosx/Info.plist vs-code.code-workspace.template:.vscode/vs-code-template.code-workspace.in]) +AC_CONFIG_HEADERS([config_host/config_atspi.h]) AC_CONFIG_HEADERS([config_host/config_buildconfig.h]) AC_CONFIG_HEADERS([config_host/config_buildid.h]) AC_CONFIG_HEADERS([config_host/config_box2d.h]) diff --git a/distro-configs/Jenkins/linux_clang_dbgutil_64 b/distro-configs/Jenkins/linux_clang_dbgutil_64 index 6262dae9276e..3409189e151c 100644 --- a/distro-configs/Jenkins/linux_clang_dbgutil_64 +++ b/distro-configs/Jenkins/linux_clang_dbgutil_64 @@ -8,3 +8,4 @@ --enable-odk --disable-dconf --enable-python=internal +--enable-atspi-tests diff --git a/distro-configs/Jenkins/linux_gcc_release_64 b/distro-configs/Jenkins/linux_gcc_release_64 index 37f0b4b46946..c4b24f68b007 100644 --- a/distro-configs/Jenkins/linux_gcc_release_64 +++ b/distro-configs/Jenkins/linux_gcc_release_64 @@ -7,3 +7,4 @@ CXX=/opt/rh/devtoolset-7/root/usr/bin/g++ --enable-mergelibs --disable-dconf --enable-python=internal +--enable-atspi-tests diff --git a/include/test/a11y/AccessibilityTools.hxx b/include/test/a11y/AccessibilityTools.hxx index 38a76ce407f1..5235faedd377 100644 --- a/include/test/a11y/AccessibilityTools.hxx +++ b/include/test/a11y/AccessibilityTools.hxx @@ -30,6 +30,7 @@ #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/accessibility/XAccessibleText.hpp> class OOO_DLLPUBLIC_TEST AccessibilityTools { @@ -258,6 +259,7 @@ private: static OUString debugName(css::accessibility::XAccessible* xacc); static OUString debugName(const css::accessibility::AccessibleEventObject* evobj); static OUString debugName(css::accessibility::XAccessibleAction* xAct); + static OUString debugName(css::accessibility::XAccessibleText* xTxt); }; CPPUNIT_NS_BEGIN diff --git a/test/source/a11y/AccessibilityTools.cxx b/test/source/a11y/AccessibilityTools.cxx index 33270505f3a6..b51c7cbac239 100644 --- a/test/source/a11y/AccessibilityTools.cxx +++ b/test/source/a11y/AccessibilityTools.cxx @@ -700,6 +700,12 @@ OUString AccessibilityTools::debugName(accessibility::XAccessibleAction* xAct) 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) diff --git a/vcl/CppunitTest_vcl_gtk3_a11y.mk b/vcl/CppunitTest_vcl_gtk3_a11y.mk new file mode 100644 index 000000000000..0981a21857d2 --- /dev/null +++ b/vcl/CppunitTest_vcl_gtk3_a11y.mk @@ -0,0 +1,61 @@ +# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t -*- +# +# 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/. +# + +# hack plugging into the CppunitTest machinery yet using a xvfb-wrapper GTK3 run +$(call gb_CppunitTest_get_target,vcl_gtk3_a11y) : gb_TEST_ENV_VARS += SAL_USE_VCLPLUGIN=gtk3 +ifeq (,$(VCL_GTK3_TESTS_NO_XVFB)) +$(call gb_CppunitTest_get_target,vcl_gtk3_a11y) : \ + ICECREAM_RUN += $(XVFB_RUN) $(DBUS_LAUNCH) --exit-with-session +endif + +$(eval $(call gb_CppunitTest_CppunitTest,vcl_gtk3_a11y)) + +$(eval $(call gb_CppunitTest_add_exception_objects,vcl_gtk3_a11y, \ + vcl/qa/cppunit/a11y/atspi2/atspiwrapper \ + vcl/qa/cppunit/a11y/atspi2/atspi2 \ + vcl/qa/cppunit/a11y/atspi2/atspi2text \ +)) + +$(eval $(call gb_CppunitTest_set_include,vcl_gtk3_a11y,\ + $$(INCLUDE) \ + $$(ATSPI2_CFLAGS) \ +)) + +$(eval $(call gb_CppunitTest_add_libs,vcl_gtk3_a11y,\ + $$(ATSPI2_LIBS) \ +)) + +$(eval $(call gb_CppunitTest_use_libraries,vcl_gtk3_a11y, \ + sal \ + cppu \ + subsequenttest \ + test \ + i18nlangtag \ + unotest \ + vcl \ +)) + +$(eval $(call gb_CppunitTest_use_externals,vcl_gtk3_a11y,\ + boost_headers \ +)) + +$(eval $(call gb_CppunitTest_use_api,vcl_gtk3_a11y,\ + offapi \ + udkapi \ +)) + +$(eval $(call gb_CppunitTest_use_sdk_api,vcl_gtk3_a11y)) +$(eval $(call gb_CppunitTest_use_rdb,vcl_gtk3_a11y,services)) +$(eval $(call gb_CppunitTest_use_ure,vcl_gtk3_a11y)) +$(eval $(call gb_CppunitTest_use_vcl,vcl_gtk3_a11y)) + +$(eval $(call gb_CppunitTest_use_instdir_configuration,vcl_gtk3_a11y)) +$(eval $(call gb_CppunitTest_use_common_configuration,vcl_gtk3_a11y)) + +# vim: set noet sw=4 ts=4: diff --git a/vcl/Module_vcl.mk b/vcl/Module_vcl.mk index 5c6fbe987430..704b324622fe 100644 --- a/vcl/Module_vcl.mk +++ b/vcl/Module_vcl.mk @@ -82,6 +82,12 @@ ifneq ($(ENABLE_GTK3),) $(eval $(call gb_Module_add_targets,vcl,\ Library_vclplug_gtk3 \ )) + +ifneq ($(ENABLE_ATSPI_TESTS),) +$(eval $(call gb_Module_add_check_targets,vcl,\ + CppunitTest_vcl_gtk3_a11y \ +)) +endif endif ifneq ($(ENABLE_GTK4),) diff --git a/vcl/qa/cppunit/a11y/atspi2/atspi2.cxx b/vcl/qa/cppunit/a11y/atspi2/atspi2.cxx new file mode 100644 index 000000000000..bca752e85cbe --- /dev/null +++ b/vcl/qa/cppunit/a11y/atspi2/atspi2.cxx @@ -0,0 +1,498 @@ +/* -*- 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 <com/sun/star/accessibility/AccessibleRelationType.hpp> +#include <com/sun/star/accessibility/AccessibleStateType.hpp> +#include <com/sun/star/accessibility/XAccessibleExtendedAttributes.hpp> + +#include <comphelper/propertyvalue.hxx> +#include <o3tl/string_view.hxx> +#include <sfx2/zoomitem.hxx> +#include <unotest/macros_test.hxx> + +#include <test/a11y/AccessibilityTools.hxx> + +#include "atspiwrapper.hxx" + +using namespace css; + +// from gtk3/a11y/atkwrapper.cxx +static AtspiRole mapToAtspiRole(sal_Int16 nRole) +{ + 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(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); + MAP(BUTTON_DROPDOWN, PUSH_BUTTON); +#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 + + 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(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(sal_Int16 nRelation) +{ + switch (nRelation) + { + 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::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; + } + + return ATSPI_RELATION_NULL; +} + +static std::string debugString(const Atspi::Accessible& pAtspiAccessible) +{ + CPPUNIT_NS::OStringStream ost; + + ost << "(" << static_cast<const void*>(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<accessibility::XAccessible>& 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 auto nLORole = mapToAtspiRole(xLOContext->getAccessibleRole()); + 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<decltype(loStateSet)> ? 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<accessibility::XAccessibleExtendedAttributes>(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, pAtspiRelation.getNTargets()); + + if (recurseFlags & RecurseFlags::RELATIONS_TARGETS) + { + for (auto j = decltype(nLOTargetsCount){ 0 }; j < nLOTargetsCount; j++) + { + uno::Reference<accessibility::XAccessible> xLOTarget(xLORelation.TargetSet[j], + uno::UNO_QUERY_THROW); + compareObjects(xLOTarget, pAtspiRelation.getTarget(j), RecurseFlags::NONE); + } + } + } + + // other interfaces + if (auto xLOText = uno::Reference<accessibility::XAccessibleText>(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<accessibility::XAccessible>& 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 <typename... Ts> +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"); + + /* 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<accessibility::XAccessible>(mxWindow, uno::UNO_QUERY_THROW), + xAtspiPane); +} + +CPPUNIT_PLUGIN_IMPLEMENT(); + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/vcl/qa/cppunit/a11y/atspi2/atspi2.hxx b/vcl/qa/cppunit/a11y/atspi2/atspi2.hxx new file mode 100644 index 000000000000..87c0b698f3ab --- /dev/null +++ b/vcl/qa/cppunit/a11y/atspi2/atspi2.hxx @@ -0,0 +1,45 @@ +/* -*- 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/. + */ + +#pragma once + +#include <com/sun/star/accessibility/XAccessible.hpp> +#include <com/sun/star/accessibility/XAccessibleContext.hpp> +#include <com/sun/star/accessibility/XAccessibleText.hpp> +#include <com/sun/star/uno/Reference.h> + +#include "atspi2testbase.hxx" +#include "atspiwrapper.hxx" + +class Atspi2TestTree : public Atspi2TestBase +{ +protected: + enum RecurseFlags : sal_uInt16 + { + NONE = 0, + PARENT = 1 << 0, + RELATIONS_TARGETS = 1 << 1, + ALL = 0xffff + }; + + static sal_Int64 + swChildIndex(css::uno::Reference<css::accessibility::XAccessibleContext> xContext); + static void + testSwScroll(const css::uno::Reference<css::accessibility::XAccessibleContext>& xLOContext, + const Atspi::Accessible& pAtspiAccessible); + static void + compareObjects(const css::uno::Reference<css::accessibility::XAccessible>& xLOAccessible, + const Atspi::Accessible& pAtspiAccessible, const sal_uInt16 recurseFlags); + static void + compareTrees(const css::uno::Reference<css::accessibility::XAccessible>& xLOAccessible, + const Atspi::Accessible& xAtspiAccessible, const int depth = 0); + static void + compareTextObjects(const css::uno::Reference<css::accessibility::XAccessibleText>& xLOText, + const Atspi::Text& pAtspiText); +}; diff --git a/vcl/qa/cppunit/a11y/atspi2/atspi2testbase.hxx b/vcl/qa/cppunit/a11y/atspi2/atspi2testbase.hxx new file mode 100644 index 000000000000..12a58da79d4b --- /dev/null +++ b/vcl/qa/cppunit/a11y/atspi2/atspi2testbase.hxx @@ -0,0 +1,94 @@ +/* -*- 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/. + */ + +#pragma once + +#include <vcl/svapp.hxx> + +#include <test/a11y/accessibletestbase.hxx> +#include <test/a11y/AccessibilityTools.hxx> + +#include "atspiwrapper.hxx" + +/** + * @brief Base class for AT-SPI2 tests. + * + * This provides the basis for interacting with AT-SPI2, including getting the object representing + * our application, and facility for obtaining a top-level window of that app. + */ +class Atspi2TestBase : public test::AccessibleTestBase +{ +protected: + Atspi::Accessible m_pAtspiApp; + + static Atspi::Accessible getApp(const std::string_view appName) + { + std::cout << "Looking for AT-SPI application \"" << appName << "\"" << std::endl; + const auto nDesktops = atspi_get_desktop_count(); + for (auto desktopId = decltype(nDesktops){ 0 }; desktopId < nDesktops; desktopId++) + { + Atspi::Accessible desktop(atspi_get_desktop(desktopId)); + + for (auto&& child : desktop) + { + if (!child) // is that useful? + continue; + if (child.getRole() != ATSPI_ROLE_APPLICATION) + continue; + const auto name = child.getName(); + std::cout << "Found desktop child: " << name << std::endl; + if (appName != name) + { + continue; + } + return std::move(child); + } + } + return nullptr; + } + + static Atspi::Accessible getSelfApp() + { + const auto appFileName = Application::GetAppFileName(); + const auto slash = appFileName.lastIndexOf('/'); + const auto baseName = (slash >= 0) ? OUString(appFileName.subView(slash + 1)) : appFileName; + return getApp(baseName.getLength() > 0 ? baseName.toUtf8().getStr() : "cppunittester"); + } + +protected: + Atspi::Accessible getWindow(const std::string_view windowName) + { + for (auto&& child : m_pAtspiApp) + { + const auto name = child.getName(); + std::cout << "Found window: " << name << std::endl; + if (windowName == name) + return std::move(child); + } + return nullptr; + } + +public: + Atspi2TestBase() + { + if (!atspi_is_initialized()) + atspi_init(); + } + + virtual void setUp() override + { + test::AccessibleTestBase::setUp(); + + AccessibilityTools::Await([this]() { + m_pAtspiApp = getSelfApp(); + return bool(m_pAtspiApp); + }); + assert(m_pAtspiApp); + } +}; diff --git a/vcl/qa/cppunit/a11y/atspi2/atspi2text.cxx b/vcl/qa/cppunit/a11y/atspi2/atspi2text.cxx new file mode 100644 index 000000000000..68ac59e6bf62 --- /dev/null +++ b/vcl/qa/cppunit/a11y/atspi2/atspi2text.cxx @@ -0,0 +1,1017 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <com/sun/star/accessibility/AccessibleRelationType.hpp> +#include <com/sun/star/accessibility/AccessibleTextType.hpp> +#include <com/sun/star/accessibility/XAccessibleComponent.hpp> +#include <com/sun/star/accessibility/XAccessibleText.hpp> +#include <com/sun/star/accessibility/XAccessibleTextAttributes.hpp> +#include <com/sun/star/accessibility/XAccessibleTextMarkup.hpp> + +#include <com/sun/star/awt/FontSlant.hpp> +#include <com/sun/star/awt/FontStrikeout.hpp> +#include <com/sun/star/awt/FontUnderline.hpp> +#include <com/sun/star/style/CaseMap.hpp> +#include <com/sun/star/style/LineSpacing.hpp> +#include <com/sun/star/style/LineSpacingMode.hpp> +#include <com/sun/star/style/ParagraphAdjust.hpp> +#include <com/sun/star/style/TabStop.hpp> +#include <com/sun/star/text/FontRelief.hpp> +#include <com/sun/star/text/WritingMode2.hpp> +#include <com/sun/star/text/TextMarkupType.hpp> + +#include <i18nlangtag/languagetag.hxx> +#include <tools/UnitConversion.hxx> +#include <rtl/character.hxx> + +#include <test/a11y/AccessibilityTools.hxx> + +#include "atspi2.hxx" +#include "atspiwrapper.hxx" + +using namespace css; + +namespace +{ +/** @brief Helper class to check text attributes are properly exported to Atspi. + * + * This kind of duplicates most of the logic in atktextattributes.cxx, but if we want to check the + * values are correct (which includes whether they are properly updated for example), we have to do + * this, even though it means quite some processing for some of the attributes. + * This has to be kept in sync with how atktextattributes.cxx exposes those attributes. */ +class AttributesChecker +{ +private: + uno::Reference<accessibility::XAccessibleText> mxLOText; + Atspi::Text mxAtspiText; + +public: + AttributesChecker(const uno::Reference<accessibility::XAccessibleText>& xLOText, + const Atspi::Text& xAtspiText) + : mxLOText(xLOText) + , mxAtspiText(xAtspiText) + { + } + +private: + // helper to validate a value represented as a single float in ATSPI + static bool implCheckFloat(std::string_view atspiValue, float expected) + { + float f; + char dummy; + + CPPUNIT_ASSERT_EQUAL(1, sscanf(atspiValue.data(), "%g%c", &f, &dummy)); + CPPUNIT_ASSERT_DOUBLES_EQUAL(expected, f, 1e-4); + + return true; + } + + // helper to check simple mappings between LO and ATSPI + template <typename T> + static bool implCheckMapping(const T loValue, const std::string_view atspiValue, + const std::unordered_map<T, std::string_view>& map, + const bool retIfMissing = false) + { + const auto& iter = map.find(loValue); + if (iter != map.end()) + { + CPPUNIT_ASSERT_EQUAL(iter->second, atspiValue); + return true; + } + return retIfMissing; + } + + // checkers, see atktextattributes.cxx + bool checkBoolean(std::string_view atspiValue, const beans::PropertyValue& property, + const uno::Sequence<beans::PropertyValue>&) + { + if (property.Value.get<bool>()) + CPPUNIT_ASSERT_EQUAL(std::string_view("true"), atspiValue); + else + CPPUNIT_ASSERT_EQUAL(std::string_view("false"), atspiValue); + + return true; + } + + bool checkString(std::string_view atspiValue, const beans::PropertyValue& property, + const uno::Sequence<beans::PropertyValue>&) + { + CPPUNIT_ASSERT_EQUAL(property.Value.get<OUString>(), OUString::fromUtf8(atspiValue)); + return true; + } + + bool checkFloat(std::string_view atspiValue, const beans::PropertyValue& property, + const uno::Sequence<beans::PropertyValue>&) + { + return implCheckFloat(atspiValue, property.Value.get<float>()); + } + + bool checkVariant(std::string_view atspiValue, const beans::PropertyValue& property, + const uno::Sequence<beans::PropertyValue>&) + { + if (property.Value.get<short>() == style::CaseMap::SMALLCAPS) + CPPUNIT_ASSERT_EQUAL(std::string_view("small_caps"), atspiValue); + else + CPPUNIT_ASSERT_EQUAL(std::string_view("normal"), atspiValue); + + return true; + } + + // See Scale2String + bool checkScale(std::string_view atspiValue, const beans::PropertyValue& property, + const uno::Sequence<beans::PropertyValue>&) + { + double v; + char dummy; + + CPPUNIT_ASSERT_EQUAL(1, sscanf(atspiValue.data(), "%lg%c", &v, &dummy)); + CPPUNIT_ASSERT_EQUAL(property.Value.get<sal_Int16>(), sal_Int16(v * 100)); + + return true; + } + + // see Escapement2VerticalAlign + bool checkVerticalAlign(std::string_view atspiValue, const beans::PropertyValue& property, + const uno::Sequence<beans::PropertyValue>&) + { + const sal_Int16 n = property.Value.get<sal_Int16>(); + + if (n == 0) + CPPUNIT_ASSERT_EQUAL(std::string_view("baseline"), atspiValue); + else if (n == -101) + CPPUNIT_ASSERT_EQUAL(std::string_view("sub"), atspiValue); + else if (n == 101) + CPPUNIT_ASSERT_EQUAL(std::string_view("super"), atspiValue); + else + { + int v; + char dummy; + CPPUNIT_ASSERT_EQUAL(1, sscanf(atspiValue.data(), "%d%%%c", &v, &dummy)); + CPPUNIT_ASSERT_EQUAL(int(n), v); + } + + return true; + } + + bool checkColor(std::string_view atspiValue, const beans::PropertyValue& property, + const uno::Sequence<beans::PropertyValue>&) + { + auto color = property.Value.get<sal_Int32>(); + + if (color == -1) // automatic, use the component's color + { + uno::Reference<accessibility::XAccessibleComponent> xComponent(mxLOText, + uno::UNO_QUERY); + if (xComponent.is()) + { + if (property.Name == u"CharBackColor") + color = xComponent->getBackground(); + else if (property.Name == u"CharColor") + color = xComponent->getForeground(); + } + } + + if (color != -1) + { + unsigned int r, g, b; + char dummy; + + CPPUNIT_ASSERT_EQUAL(3, sscanf(atspiValue.data(), "%u,%u,%u%c", &r, &g, &b, &dummy)); + CPPUNIT_ASSERT_EQUAL((color & 0xFFFFFF), + (static_cast<sal_Int32>(r) << 16 | static_cast<sal_Int32>(g) << 8 + | static_cast<sal_Int32>(b))); + return true; + } + + return false; + } + + // See LineSpacing2LineHeight + bool checkLineHeight(std::string_view atspiValue, const beans::PropertyValue& property, + const uno::Sequence<beans::PropertyValue>&) + { + const auto lineSpacing = property.Value.get<style::LineSpacing>(); + char dummy; + + if (lineSpacing.Mode == style::LineSpacingMode::PROP) + { + int h; + + CPPUNIT_ASSERT_EQUAL(1, sscanf(atspiValue.data(), "%d%%%c", &h, &dummy)); + CPPUNIT_ASSERT_EQUAL(lineSpacing.Height, sal_Int16(h)); + } + else if (lineSpacing.Mode == style::LineSpacingMode::FIX) + { + double pt; + + CPPUNIT_ASSERT_EQUAL(1, sscanf(atspiValue.data(), "%lgpt%c", &pt, &dummy)); + CPPUNIT_ASSERT_DOUBLES_EQUAL(convertMm100ToPoint<double>(lineSpacing.Height), pt, 1e-4); + CPPUNIT_ASSERT_EQUAL(lineSpacing.Height, sal_Int16(convertPointToMm100(pt))); + } + else + return false; + + return true; + } + + bool checkStretch(std::string_view atspiValue, const beans::PropertyValue& property, + const uno::Sequence<beans::PropertyValue>&) + { + const auto n = property.Value.get<sal_Int16>(); + + if (n < 0) + CPPUNIT_ASSERT_EQUAL(std::string_view("condensed"), atspiValue); + else if (n > 0) + CPPUNIT_ASSERT_EQUAL(std::string_view("expanded"), atspiValue); + else + CPPUNIT_ASSERT_EQUAL(std::string_view("normal"), atspiValue); + + return true; + } + + bool checkStyle(std::string_view atspiValue, const beans::PropertyValue& property, + const uno::Sequence<beans::PropertyValue>&) + { + return implCheckMapping( + property.Value.get<awt::FontSlant>(), atspiValue, + { { awt::FontSlant_NONE, std::string_view("normal") }, + { awt::FontSlant_OBLIQUE, std::string_view("oblique") }, + { awt::FontSlant_ITALIC, std::string_view("italic") }, + { awt::FontSlant_REVERSE_OBLIQUE, std::string_view("reverse oblique") }, + { awt::FontSlant_REVERSE_ITALIC, std::string_view("reverse italic") } }); + } + + bool checkJustification(std::string_view atspiValue, const beans::PropertyValue& property, + const uno::Sequence<beans::PropertyValue>&) + { + return implCheckMapping(static_cast<style::ParagraphAdjust>(property.Value.get<short>()), + atspiValue, + { { style::ParagraphAdjust_LEFT, std::string_view("left") }, + { style::ParagraphAdjust_RIGHT, std::string_view("right") }, + { style::ParagraphAdjust_BLOCK, std::string_view("fill") }, + { style::ParagraphAdjust_STRETCH, std::string_view("fill") }, + { style::ParagraphAdjust_CENTER, std::string_view("center") } }); + } + + bool checkShadow(std::string_view atspiValue, const beans::PropertyValue& property, + const uno::Sequence<beans::PropertyValue>&) + { + if (property.Value.get<bool>()) + CPPUNIT_ASSERT_EQUAL(std::string_view("black"), atspiValue); + else + CPPUNIT_ASSERT_EQUAL(std::string_view("none"), atspiValue); + + return true; + } + + bool checkLanguage(std::string_view atspiValue, const beans::PropertyValue& property, + const uno::Sequence<beans::PropertyValue>&) + { + auto aLocale = property.Value.get<lang::Locale>(); + LanguageTag aLanguageTag(aLocale); + + CPPUNIT_ASSERT_EQUAL(OUString(aLanguageTag.getLanguage() + "-" + + aLanguageTag.getCountry().toAsciiLowerCase()), + OUString::fromUtf8(atspiValue)); + + return true; + } + + bool checkTextRotation(std::string_view atspiValue, const beans::PropertyValue& property, + const uno::Sequence<beans::PropertyValue>&) + { + return implCheckFloat(atspiValue, property.Value.get<sal_Int16>() / 10.0f); + } + + bool checkWeight(std::string_view atspiValue, const beans::PropertyValue& property, + const uno::Sequence<beans::PropertyValue>&) + { + return implCheckFloat(atspiValue, property.Value.get<float>() * 4); + } + + bool checkCMMValue(std::string_view atspiValue, const beans::PropertyValue& property, + const uno::Sequence<beans::PropertyValue>&) + { + double v; + char dummy; + + // CMM is 1/100th of a mm + CPPUNIT_ASSERT_EQUAL(1, sscanf(atspiValue.data(), "%lgmm%c", &v, &dummy)); + CPPUNIT_ASSERT_DOUBLES_EQUAL(property.Value.get<sal_Int32>() * 0.01, v, 1e-4); + + return true; + } + + bool checkDirection(std::string_view atspiValue, const beans::PropertyValue& property, + const uno::Sequence<beans::PropertyValue>&) + { + return implCheckMapping(property.Value.get<sal_Int16>(), atspiValue, + { { text::WritingMode2::TB_LR, std::string_view("ltr") }, + { text::WritingMode2::LR_TB, std::string_view("ltr") }, + { text::WritingMode2::TB_RL, std::string_view("rtl") }, + { text::WritingMode2::RL_TB, std::string_view("rtl") }, + { text::WritingMode2::PAGE, std::string_view("none") } }); + } + + bool checkWritingMode(std::string_view atspiValue, const beans::PropertyValue& property, + const uno::Sequence<beans::PropertyValue>&) + { + return implCheckMapping(property.Value.get<sal_Int16>(), atspiValue, + { { text::WritingMode2::TB_LR, std::string_view("tb-lr") }, + { text::WritingMode2::LR_TB, std::string_view("lr-tb") }, + { text::WritingMode2::TB_RL, std::string_view("tb-rl") }, + { text::WritingMode2::RL_TB, std::string_view("rl-tb") }, + { text::WritingMode2::PAGE, std::string_view("none") } }); + } + + static const beans::PropertyValue* + findProperty(const uno::Sequence<beans::PropertyValue>& properties, std::u16string_view name) + { + auto prop = std::find_if(properties.begin(), properties.end(), + [name](auto& p) { return p.Name == name; }); + if (prop == properties.end()) + prop = nullptr; + return prop; + } + + // same as findProperty() above, but with a fast path is @p property is a match + static const beans::PropertyValue* + findProperty(const beans::PropertyValue* property, + const uno::Sequence<beans::PropertyValue>& properties, std::u16string_view name) + { + if (property->Name == name) + return property; + return findProperty(properties, name); + } + + bool checkFontEffect(std::string_view atspiValue, const beans::PropertyValue& property, + const uno::Sequence<beans::PropertyValue>& loProperties) + { + if (auto charContoured = findProperty(&property, loProperties, u"CharContoured"); + charContoured && charContoured->Value.get<bool>()) + { + CPPUNIT_ASSERT_EQUAL(std::string_view("outline"), atspiValue); + return true; + } + + if (auto charRelief = findProperty(&property, loProperties, u"CharRelief")) + { + return implCheckMapping(charRelief->Value.get<sal_Int16>(), atspiValue, + { { text::FontRelief::NONE, std::string_view("none") }, + { text::FontRelief::EMBOSSED, std::string_view("emboss") }, + { text::FontRelief::ENGRAVED, std::string_view("engrave") } }, + true); + } + + return false; + } + + bool checkTextDecoration(std::string_view atspiValue, const beans::PropertyValue&, + const uno::Sequence<beans::PropertyValue>& loProperties) + { + if (atspiValue == "none") + { + if (auto prop = findProperty(loProperties, u"CharFlash")) + CPPUNIT_ASSERT_EQUAL(false, prop->Value.get<bool>()); + if (auto prop = findProperty(loProperties, u"CharUnderline")) + CPPUNIT_ASSERT_EQUAL(css::awt::FontUnderline::NONE, prop->Value.get<sal_Int16>()); + if (auto prop = findProperty(loProperties, u"CharStrikeout")) + CPPUNIT_ASSERT(prop->Value.get<sal_Int16>() == css::awt::FontStrikeout::NONE + || prop->Value.get<sal_Int16>() + == css::awt::FontStrikeout::DONTKNOW); + } + else + { + sal_Int32 nIndex = 0; + const auto atspiValueString = OUString::fromUtf8(atspiValue); + + do + { + OUString atspiToken = atspiValueString.getToken(0, ' ', nIndex); + const beans::PropertyValue* prop; + + if (atspiToken == "blink") + { + CPPUNIT_ASSERT((prop = findProperty(loProperties, u"CharFlash"))); + CPPUNIT_ASSERT_EQUAL(true, prop->Value.get<bool>()); + } + else if (atspiToken == "underline") + { + CPPUNIT_ASSERT((prop = findProperty(loProperties, u"CharUnderline"))); + CPPUNIT_ASSERT(prop->Value.get<sal_Int16>() != css::awt::FontUnderline::NONE); + } + else if (atspiToken == "underline") + { + CPPUNIT_ASSERT((prop = findProperty(loProperties, u"CharStrikeout"))); + CPPUNIT_ASSERT(prop->Value.get<sal_Int16>() != css::awt::FontStrikeout::NONE); + CPPUNIT_ASSERT(prop->Value.get<sal_Int16>() + != css::awt::FontStrikeout::DONTKNOW); + } + else + { + CPPUNIT_ASSERT_MESSAGE( + OUString("Unknown text decoration \"" + atspiToken).toUtf8().getStr(), + false); + } + } while (nIndex > 0); + } + + return true; + } + + static bool implCheckTabStops(std::string_view atspiValue, const beans::PropertyValue& property, + const bool defaultTabs) + { + uno::Sequence<style::TabStop> theTabStops; + + if (property.Value >>= theTabStops) + { + sal_Unicode lastFillChar = ' '; + const char* p = atspiValue.data(); + + for (const auto& rTabStop : std::as_const(theTabStops)) + { + if ((style::TabAlign_DEFAULT == rTabStop.Alignment) != defaultTabs) + continue; + + const char* tab_align = ""; + switch (rTabStop.Alignment) + { + case style::TabAlign_LEFT: + tab_align = "left "; + break; + case style::TabAlign_CENTER: + tab_align = "center "; + break; + case style::TabAlign_RIGHT: + tab_align = "right "; + break; + case style::TabAlign_DECIMAL: + tab_align = "decimal "; + break; + default: + break; + } + + const char* lead_char = ""; + if (rTabStop.FillChar != lastFillChar) + { + lastFillChar = rTabStop.FillChar; + switch (lastFillChar) + { + case ' ': + lead_char = "blank "; + break; + + case '.': + lead_char = "dotted "; + break; + + case '-': + lead_char = "dashed "; + break; + + case '_': + lead_char = "lined "; + break; + + default: + lead_char = "custom "; + break; + } + } + + // check this matches "<lead_char><tab_align><position>mm" + CPPUNIT_ASSERT_EQUAL(0, strncmp(p, lead_char, strlen(lead_char))); + p += strlen(lead_char); + CPPUNIT_ASSERT_EQUAL(0, strncmp(p, tab_align, strlen(tab_align))); + p += strlen(tab_align); + float atspiPosition; + int nConsumed; + CPPUNIT_ASSERT_EQUAL(1, sscanf(p, "%gmm%n", &atspiPosition, &nConsumed)); + CPPUNIT_ASSERT_DOUBLES_EQUAL(float(rTabStop.Position * 0.01f), atspiPosition, 1e-4); + p += nConsumed; + + if (*p) + { + CPPUNIT_ASSERT_EQUAL(' ', *p); + p++; + } + } + + // make sure there isn't garbage at the end + CPPUNIT_ASSERT_EQUAL(char(0), *p); + + return true; + } + + return false; + } + + bool checkDefaultTabStops(std::string_view atspiValue, const beans::PropertyValue& property, + const uno::Sequence<beans::PropertyValue>&) + { + return implCheckTabStops(atspiValue, property, true); + } + + bool checkTabStops(std::string_view atspiValue, const beans::PropertyValue& property, + const uno::Sequence<beans::PropertyValue>&) + { + return implCheckTabStops(atspiValue, property, false); + } + +public: + // runner code + bool check(const uno::Sequence<beans::PropertyValue>& xLOAttributeList, + const std::unordered_map<std::string, std::string>& xAtspiAttributeList) + { + const struct + { + const char* loName; + const char* atspiName; + bool (AttributesChecker::*checkValue)( + std::string_view atspiValue, const beans::PropertyValue& property, + const uno::Sequence<beans::PropertyValue>& loAttributeList); + } atspiMap[] + = { // LO name AT-SPI name check function + { "CharBackColor", "bg-color", &AttributesChecker::checkColor }, + { "CharCaseMap", "variant", &AttributesChecker::checkVariant }, + { "CharColor", "fg-color", &AttributesChecker::checkColor }, + { "CharContoured", "font-effect", &AttributesChecker::checkFontEffect }, + { "CharEscapement", "vertical-align", &AttributesChecker::checkVerticalAlign }, + { "CharFlash", "text-decoration", &AttributesChecker::checkTextDecoration }, + { "CharFontName", "family-name", &AttributesChecker::checkString }, + { "CharHeight", "size", &AttributesChecker::checkFloat }, + { "CharHidden", "invisible", &AttributesChecker::checkBoolean }, + { "CharKerning", "stretch", &AttributesChecker::checkStretch }, + { "CharLocale", "language", &AttributesChecker::checkLanguage }, + { "CharPosture", "style", &AttributesChecker::checkStyle }, + { "CharRelief", "font-effect", &AttributesChecker::checkFontEffect }, + { "CharRotation", "text-rotation", &AttributesChecker::checkTextRotation }, + { "CharScaleWidth", "scale", &AttributesChecker::checkScale }, + { "CharShadowed", "text-shadow", &AttributesChecker::checkShadow }, + { "CharStrikeout", "text-decoration", &AttributesChecker::checkTextDecoration }, + { "CharUnderline", "text-decoration", &AttributesChecker::checkTextDecoration }, + { "CharWeight", "weight", &AttributesChecker::checkWeight }, + { "MMToPixelRatio", "mm-to-pixel-ratio", &AttributesChecker::checkFloat }, + { "ParaAdjust", "justification", &AttributesChecker::checkJustification }, + { "ParaBottomMargin", "pixels-below-lines", &AttributesChecker::checkCMMValue }, + { "ParaFirstLineIndent", "indent", &AttributesChecker::checkCMMValue }, + { "ParaLeftMargin", "left-margin", &AttributesChecker::checkCMMValue }, + { "ParaLineSpacing", "line-height", &AttributesChecker::checkLineHeight }, + { "ParaRightMargin", "right-margin", &AttributesChecker::checkCMMValue }, + { "ParaStyleName", "paragraph-style", &AttributesChecker::checkString }, + { "ParaTabStops", "tab-interval", &AttributesChecker::checkDefaultTabStops }, + { "ParaTabStops", "tab-stops", &AttributesChecker::checkTabStops }, + { "ParaTopMargin", "pixels-above-lines", &AttributesChecker::checkCMMValue }, + { "WritingMode", "direction", &AttributesChecker::checkDirection }, + { "WritingMode", "writing-mode", &AttributesChecker::checkWritingMode } + }; + + for (const auto& prop : xLOAttributeList) + { + std::cout << "found run attribute: " << prop.Name << "=" << prop.Value << std::endl; + + /* we need to loop on all entries because there might be more than one for a single + * property */ + for (const auto& entry : atspiMap) + { + if (!prop.Name.equalsAscii(entry.loName)) + continue; + + const auto atspiIter = xAtspiAttributeList.find(entry.atspiName); + /* we use an empty value if there isn't one, which can happen if the value cannot + * be represented by Atspi, or if the actual LO value is also empty */ + std::string atspiValue; + if (atspiIter != xAtspiAttributeList.end()) + atspiValue = atspiIter->second; + + std::cout << " matching atspi attribute is: " << entry.atspiName << "=" + << atspiValue << std::endl; + CPPUNIT_ASSERT( + std::invoke(entry.checkValue, this, atspiValue, prop, xLOAttributeList)); + } + } + + return true; + } +}; +} + +/* LO doesn't implement it itself, but ATK provides a fallback. Add a test here merely for the + * future when we have a direct AT-SPI implementation for e.g. GTK4. + * Just like atk-adaptor, we compute the bounding box by combining extents for each character + * in the range */ +static awt::Rectangle getRangeBounds(const uno::Reference<accessibility::XAccessibleText>& xText, + sal_Int32 startOffset, sal_Int32 endOffset) +{ + awt::Rectangle bounds; + + for (auto offset = startOffset; offset < endOffset; offset++) + { + const auto chBounds = xText->getCharacterBounds(offset); + if (offset == 0) + bounds = chBounds; + else + { + const auto x = std::min(bounds.X, chBounds.X); + const auto y = std::min(bounds.Y, chBounds.Y); + bounds.Width = std::max(bounds.X + bounds.Width, chBounds.X + chBounds.Width) - x; + bounds.Height = std::max(bounds.Y + bounds.Height, chBounds.Y + chBounds.Height) - y; + bounds.X = x; + bounds.Y = y; + } + } + + return bounds; +} + +void Atspi2TestTree::compareTextObjects( + const uno::Reference<accessibility::XAccessibleText>& xLOText, const Atspi::Text& pAtspiText) +{ + CPPUNIT_ASSERT_EQUAL(xLOText->getCharacterCount(), sal_Int32(pAtspiText.getCharacterCount())); + CPPUNIT_ASSERT_EQUAL(xLOText->getCaretPosition(), sal_Int32(pAtspiText.getCaretOffset())); + CPPUNIT_ASSERT_EQUAL(xLOText->getText(), OUString::fromUtf8(pAtspiText.getText(0, -1))); + + const auto characterCount = xLOText->getCharacterCount(); + auto offset = decltype(characterCount){ 0 }; + auto atspiPosition = Atspi::Point{ 0, 0 }; + + AttributesChecker attributesChecker(xLOText, pAtspiText); + + auto xLOTextAttrs + = uno::Reference<accessibility::XAccessibleTextAttributes>(xLOText, uno::UNO_QUERY); + // default text attributes + if (xLOTextAttrs.is()) + { + const auto aAttributeList = xLOTextAttrs->getDefaultAttributes(uno::Sequence<OUString>()); + const auto atspiAttributeList = pAtspiText.getDefaultAttributes(); + + attributesChecker.check(aAttributeList, atspiAttributeList); + } + + if (characterCount > 0) + { + const auto atspiComponent = pAtspiText.queryComponent(); + atspiPosition = atspiComponent.getPosition(ATSPI_COORD_TYPE_WINDOW); + } + + // text run attributes + uno::Reference<accessibility::XAccessibleTextMarkup> xTextMarkup(xLOText, uno::UNO_QUERY); + while (offset < characterCount) + { + // message for the assertions so we know where it comes from + OString offsetMsg(OString::Concat("in ") + AccessibilityTools::debugString(xLOText).c_str() + + " at offset " + OString::number(offset)); + + uno::Sequence<beans::PropertyValue> aAttributeList; + + if (xLOTextAttrs.is()) + aAttributeList = xLOTextAttrs->getRunAttributes(offset, uno::Sequence<OUString>()); + else + aAttributeList = xLOText->getCharacterAttributes(offset, uno::Sequence<OUString>()); + + int atspiStartOffset = 0, atspiEndOffset = 0; + const auto atspiAttributeList + = pAtspiText.getAttributeRun(offset, false, &atspiStartOffset, &atspiEndOffset); + + accessibility::TextSegment aTextSegment + = xLOText->getTextAtIndex(offset, accessibility::AccessibleTextType::ATTRIBUTE_RUN); + + /* Handle misspelled text and tracked changes as atktext.cxx does as it affects the run + * boundaries. Also check the attributes are properly forwarded. */ + if (xTextMarkup.is()) + { + const struct + { + sal_Int32 markupType; + const char* atspiAttribute; + const char* atspiValue; + } aTextMarkupTypes[] + = { { text::TextMarkupType::SPELLCHECK, "text-spelling", "misspelled" }, + { text::TextMarkupType::TRACK_CHANGE_INSERTION, "text-tracked-change", + "insertion" }, + { text::TextMarkupType::TRACK_CHANGE_DELETION, "text-tracked-change", + "deletion" }, + { text::TextMarkupType::TRACK_CHANGE_FORMATCHANGE, "text-tracked-change", + "attribute-change" } }; + + for (const auto& aTextMarkupType : aTextMarkupTypes) + { + const auto nTextMarkupCount + = xTextMarkup->getTextMarkupCount(aTextMarkupType.markupType); + if (nTextMarkupCount <= 0) + continue; + + for (auto nTextMarkupIndex = decltype(nTextMarkupCount){ 0 }; + nTextMarkupIndex < nTextMarkupCount; ++nTextMarkupIndex) + { + const auto aMarkupTextSegment + = xTextMarkup->getTextMarkup(nTextMarkupIndex, aTextMarkupType.markupType); + if (aMarkupTextSegment.SegmentStart > offset) + { + aTextSegment.SegmentEnd + = ::std::min(aTextSegment.SegmentEnd, aMarkupTextSegment.SegmentStart); + break; // no further iteration. + } + else if (offset < aMarkupTextSegment.SegmentEnd) + { + // text markup at <offset> + aTextSegment.SegmentStart = ::std::max(aTextSegment.SegmentStart, + aMarkupTextSegment.SegmentStart); + aTextSegment.SegmentEnd + = ::std::min(aTextSegment.SegmentEnd, aMarkupTextSegment.SegmentEnd); + // check the attribute is set + const auto atspiIter + = atspiAttributeList.find(aTextMarkupType.atspiAttribute); + CPPUNIT_ASSERT_MESSAGE(offsetMsg.getStr(), + atspiIter != atspiAttributeList.end()); + CPPUNIT_ASSERT_EQUAL_MESSAGE(offsetMsg.getStr(), + std::string_view(aTextMarkupType.atspiValue), + std::string_view(atspiIter->second)); + break; // no further iteration needed. + } + else + { + aTextSegment.SegmentStart + = ::std::max(aTextSegment.SegmentStart, aMarkupTextSegment.SegmentEnd); + // continue iteration. + } + } + } + } + + CPPUNIT_ASSERT_EQUAL_MESSAGE(offsetMsg.getStr(), aTextSegment.SegmentStart, + sal_Int32(atspiStartOffset)); + CPPUNIT_ASSERT_EQUAL_MESSAGE(offsetMsg.getStr(), aTextSegment.SegmentEnd, + sal_Int32(atspiEndOffset)); + + attributesChecker.check(aAttributeList, atspiAttributeList); + + CPPUNIT_ASSERT_MESSAGE(offsetMsg.getStr(), aTextSegment.SegmentEnd > offset); + offset = aTextSegment.SegmentEnd; + } + + // loop over each character + for (offset = 0; offset < characterCount;) + { + const auto aTextSegment + = xLOText->getTextAtIndex(offset, accessibility::AccessibleTextType::CHARACTER); + OString offsetMsg(OString::Concat("in ") + AccessibilityTools::debugString(xLOText).c_str() + + " at offset " + OString::number(offset)); + + // getCharacterAtOffset() + sal_Int32 nChOffset = 0; + sal_Int32 cp = aTextSegment.SegmentText.iterateCodePoints(&nChOffset); + /* do not check unpaired surrogates, because they are unlikely to make any sense and LO's + * GTK VCL doesn't like them */ + if (!rtl::isSurrogate(cp)) + CPPUNIT_ASSERT_EQUAL_MESSAGE(offsetMsg.getStr(), cp, + pAtspiText.getCharacterAtOffset(offset)); + + // getTextAtOffset() + const struct + { + sal_Int16 loTextType; + AtspiTextBoundaryType atspiBoundaryType; + } textTypeMap[] = { + { accessibility::AccessibleTextType::CHARACTER, ATSPI_TEXT_BOUNDARY_CHAR }, + { accessibility::AccessibleTextType::WORD, ATSPI_TEXT_BOUNDARY_WORD_START }, + { accessibility::AccessibleTextType::SENTENCE, ATSPI_TEXT_BOUNDARY_SENTENCE_START }, + { accessibility::AccessibleTextType::LINE, ATSPI_TEXT_BOUNDARY_LINE_START }, + }; + for (const auto& pair : textTypeMap) + { + auto loTextSegment = xLOText->getTextAtIndex(offset, pair.loTextType); + const auto atspiTextRange = pAtspiText.getTextAtOffset(offset, pair.atspiBoundaryType); + + // for WORD there's adjustments to be made, see atktext.cxx:adjust_boundaries() + if (pair.loTextType == accessibility::AccessibleTextType::WORD + && !loTextSegment.SegmentText.isEmpty()) + { + // Determine the start index of the next segment + const auto loTextSegmentBehind + = xLOText->getTextBehindIndex(loTextSegment.SegmentEnd, pair.loTextType); + if (!loTextSegmentBehind.SegmentText.isEmpty()) + loTextSegment.SegmentEnd = loTextSegmentBehind.SegmentStart; + else + loTextSegment.SegmentEnd = xLOText->getCharacterCount(); + + loTextSegment.SegmentText + = xLOText->getTextRange(loTextSegment.SegmentStart, loTextSegment.SegmentEnd); + } + + OString boundaryMsg(offsetMsg + " with boundary type " + + Atspi::TextBoundaryType::getName(pair.atspiBoundaryType).c_str()); + CPPUNIT_ASSERT_EQUAL_MESSAGE(boundaryMsg.getStr(), loTextSegment.SegmentText, + OUString::fromUtf8(atspiTextRange.content)); + /* if the segment is empty, LO API gives -1 offsets, but maps to 0 for AT-SPI. This is + * fine, AT-SPI doesn't really say what the offsets should be when the text is empty */ + if (!loTextSegment.SegmentText.isEmpty()) + { + CPPUNIT_ASSERT_EQUAL_MESSAGE(boundaryMsg.getStr(), loTextSegment.SegmentStart, + sal_Int32(atspiTextRange.startOffset)); + CPPUNIT_ASSERT_EQUAL_MESSAGE(boundaryMsg.getStr(), loTextSegment.SegmentEnd, + sal_Int32(atspiTextRange.endOffset)); + } + } + + // character bounds + const auto loRect = xLOText->getCharacterBounds(offset); + auto atspiRect = pAtspiText.getCharacterExtents(offset, ATSPI_COORD_TYPE_WINDOW); + atspiRect.x -= atspiPosition.x; + atspiRect.y -= atspiPosition.y; + CPPUNIT_ASSERT_EQUAL_MESSAGE(offsetMsg.getStr(), loRect.Y, sal_Int32(atspiRect.y)); + CPPUNIT_ASSERT_EQUAL_MESSAGE(offsetMsg.getStr(), loRect.Height, + sal_Int32(atspiRect.height)); + /* for some reason getCharacterBounds() might return negative widths in some cases + * (including a space at the end of a right-justified line), and ATK will then then adjust + * the X and width values to positive to workaround RTL issues (see + * https://bugzilla.gnome.org/show_bug.cgi?id=102954), so we work around that */ + if (loRect.Width < 0) + { + /* ATK will make x += width; width *= -1, but we don't really want to depend on the + * ATK behavior so we allow it to match as well */ + CPPUNIT_ASSERT_MESSAGE(offsetMsg.getStr(), + loRect.X == sal_Int32(atspiRect.x) + || loRect.X + loRect.Width == sal_Int32(atspiRect.x)); + CPPUNIT_ASSERT_MESSAGE(offsetMsg.getStr(), + loRect.Width == sal_Int32(atspiRect.width) + || -loRect.Width == sal_Int32(atspiRect.width)); + } + else + { + // normal case + CPPUNIT_ASSERT_EQUAL_MESSAGE(offsetMsg.getStr(), loRect.X, sal_Int32(atspiRect.x)); + CPPUNIT_ASSERT_EQUAL_MESSAGE(offsetMsg.getStr(), loRect.Width, + sal_Int32(atspiRect.width)); + } + + // indexAtPoint() + CPPUNIT_ASSERT_EQUAL_MESSAGE( + offsetMsg.getStr(), xLOText->getIndexAtPoint(awt::Point(loRect.X, loRect.Y)), + sal_Int32(pAtspiText.getOffsetAtPoint( + atspiPosition.x + loRect.X, atspiPosition.y + loRect.Y, ATSPI_COORD_TYPE_WINDOW))); + + CPPUNIT_ASSERT_MESSAGE(offsetMsg.getStr(), aTextSegment.SegmentEnd > offset); + offset = aTextSegment.SegmentEnd; + } + + // getRangeExtents() -- ATK doesn't like empty ranges, so only test when not empty + if (characterCount > 0) + { + const auto loRangeBounds = getRangeBounds(xLOText, 0, characterCount); + const auto atspiRangeExtents + = pAtspiText.getRangeExtents(0, characterCount, ATSPI_COORD_TYPE_WINDOW); + CPPUNIT_ASSERT_EQUAL(loRangeBounds.X, sal_Int32(atspiRangeExtents.x - atspiPosition.x)); + CPPUNIT_ASSERT_EQUAL(loRangeBounds.Y, sal_Int32(atspiRangeExtents.y - atspiPosition.y)); + CPPUNIT_ASSERT_EQUAL(loRangeBounds.Width, sal_Int32(atspiRangeExtents.width)); + CPPUNIT_ASSERT_EQUAL(loRangeBounds.Height, sal_Int32(atspiRangeExtents.height)); + } + + // selection (LO only have one selection, so some of the API doesn't really make sense) + CPPUNIT_ASSERT_EQUAL(xLOText->getSelectionEnd() != xLOText->getSelectionStart() ? 1 : 0, + pAtspiText.getNSelections()); + + const auto atspiSelection = pAtspiText.getSelection(0); + CPPUNIT_ASSERT_EQUAL(xLOText->getSelectionStart(), sal_Int32(atspiSelection.startOffset)); + CPPUNIT_ASSERT_EQUAL(xLOText->getSelectionEnd(), sal_Int32(atspiSelection.endOffset)); + + /* We need to take extra care with setSelection() because it could result to scrolling, which + * might result in node destruction, which can mess up the parent's children enumeration. + * So we only test nodes that are neither the first nor last child in its parent, hoping that + * means it won't require scrolling to show the end of the selection. */ + uno::Reference<accessibility::XAccessibleContext> xLOContext(xLOText, uno::UNO_QUERY_THROW); + const auto nIndexInParent = xLOContext->getAccessibleIndexInParent(); + if (characterCount && nIndexInParent > 0 + && nIndexInParent + 1 < xLOContext->getAccessibleParent() + ->getAccessibleContext() + ->getAccessibleChildCount() + && pAtspiText.setSelection(0, 0, characterCount)) + { + CPPUNIT_ASSERT_EQUAL(sal_Int32(0), xLOText->getSelectionStart()); + CPPUNIT_ASSERT_EQUAL(characterCount, xLOText->getSelectionEnd()); + // try and restore previous selection, if any + CPPUNIT_ASSERT(xLOText->setSelection(std::max(0, atspiSelection.startOffset), + std::max(0, atspiSelection.endOffset))); + } + + // scrollSubstringTo() is tested in the parent, because it might dispose ourselves otherwise. + + // TODO: more checks here... +} + +#if HAVE_ATSPI2_SCROLL_TO +// like getFirstRelationTargetOfType() but for Atspi objects +static Atspi::Accessible +atspiGetFirstRelationTargetOfType(const Atspi::Accessible& pAtspiAccessible, + const AtspiRelationType relationType) +{ + for (const auto& rel : pAtspiAccessible.getRelationSet()) + { + if (rel.getRelationType() == relationType && rel.getNTargets() > 0) + return rel.getTarget(0); + } + + return nullptr; +} +#endif // HAVE_ATSPI2_SCROLL_TO + +/** + * @brief Gets the index of a Writer child hopping through flows-from relationships + * @param xContext The accessible context to locate + * @returns The index of @c xContext in the flows-from chain + * + * Gets the index of a child in its parent regardless of whether it is on screen or not. + * + * @warning This relying on the flows-from relationships, it only works for the connected nodes, + * and might not work for e.g. frames. + */ +sal_Int64 Atspi2TestTree::swChildIndex(uno::Reference<accessibility::XAccessibleContext> xContext) +{ + for (sal_Int64 n = 0;; n++) + { + auto xPrev = getFirstRelationTargetOfType( + xContext, accessibility::AccessibleRelationType::CONTENT_FLOWS_FROM); + if (!xPrev.is()) + return n; + xContext = xPrev; + } +} + +/** + * @brief tests scrolling in Writer. + * @param xLOContext The @c XAccessibleContext for the writer document + * @param xAtspiAccessible The AT-SPI2 equivalent of @c xLOContext. + * + * Test scrolling (currently XAccessibleText::scrollSubstringTo()) in Writer. + */ +void Atspi2TestTree::testSwScroll( + const uno::Reference<accessibility::XAccessibleContext>& xLOContext, + const Atspi::Accessible& xAtspiAccessible) +{ +#if HAVE_ATSPI2_SCROLL_TO + /* Currently LO only implements SCROLL_ANYWHERE, so to be sure we need to find something + * offscreen and try and bring it in. LO only has implementation for SwAccessibleParagraph, + * so we find the last child, and then try and find a FLOWS_TO relationship -- that's a hack + * based on how LO exposes offscreen children, e.g. not as "real" children. Once done so, we + * have to make sure the child is now on screen, so we should find it in the children list. We + * cannot rely on anything we had still being visible, as it could very well have scrolled it to + * the top. */ + assert(accessibility::AccessibleRole::DOCUMENT_TEXT == xLOContext->getAccessibleRole()); + + auto nLOChildCount = xLOContext->getAccessibleChildCount(); + if (nLOChildCount <= 0) + return; + + // find the first off-screen text child + auto xLONextContext = xLOContext->getAccessibleChild(nLOChildCount - 1)->getAccessibleContext(); + uno::Reference<accessibility::XAccessibleText> xLONextText; + unsigned int nAfterLast = 0; + do + { + xLONextContext = getFirstRelationTargetOfType( + xLONextContext, accessibility::AccessibleRelationType::CONTENT_FLOWS_TO); + xLONextText.set(xLONextContext, uno::UNO_QUERY); + nAfterLast++; + } while (xLONextContext.is() && !xLONextText.is()); + + if (!xLONextText.is()) + return; // we have nothing off-screen to scroll to + + // get the global index of the off-screen child so we can match it later + auto nLOChildIndex = swChildIndex(xLONextContext); + + // find the corresponding Atspi child to call the API on + auto xAtspiNextChild = xAtspiAccessible.getChildAtIndex(nLOChildCount - 1); + while (nAfterLast-- > 0 && xAtspiNextChild) + xAtspiNextChild + = atspiGetFirstRelationTargetOfType(xAtspiNextChild, ATSPI_RELATION_FLOWS_TO); + /* the child ought to be found and implement the same interfaces, otherwise there's a problem + * in LO <> Atspi child mapping */ + CPPUNIT_ASSERT(xAtspiNextChild); + const auto xAtspiNextText = xAtspiNextChild.queryText(); + + // scroll the child into view + CPPUNIT_ASSERT(xAtspiNextText.scrollSubstringTo(0, 1, ATSPI_SCROLL_ANYWHERE)); + + // now, check that the nLOChildIndex is in the visible area (among the regular children) + nLOChildCount = xLOContext->getAccessibleChildCount(); + CPPUNIT_ASSERT_GREATER(sal_Int64(0), nLOChildCount); + const auto nLOFirstChildIndex + = swChildIndex(xLOContext->getAccessibleChild(0)->getAccessibleContext()); + + CPPUNIT_ASSERT_LESSEQUAL(nLOChildIndex, nLOFirstChildIndex); + CPPUNIT_ASSERT_GREATER(nLOChildIndex, nLOFirstChildIndex + nLOChildCount); +#else // !HAVE_ATSPI2_SCROLL_TO + // unused + (void)xLOContext; + (void)xAtspiAccessible; +#endif // !HAVE_ATSPI2_SCROLL_TO +} diff --git a/vcl/qa/cppunit/a11y/atspi2/atspiwrapper.cxx b/vcl/qa/cppunit/a11y/atspi2/atspiwrapper.cxx new file mode 100644 index 000000000000..fd8017426810 --- /dev/null +++ b/vcl/qa/cppunit/a11y/atspi2/atspiwrapper.cxx @@ -0,0 +1,22 @@ +/* -*- 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 "atspiwrapper.hxx" + +using namespace Atspi; + +Accessible Relation::getTarget(int i) const { return invoke(atspi_relation_get_target, i); } + +Component Accessible::queryComponent() const +{ + return queryInterface<Component>(atspi_accessible_get_component_iface); +} +Text Accessible::queryText() const { return queryInterface<Text>(atspi_accessible_get_text_iface); } + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/vcl/qa/cppunit/a11y/atspi2/atspiwrapper.hxx b/vcl/qa/cppunit/a11y/atspi2/atspiwrapper.hxx new file mode 100644 index 000000000000..209495e826ae --- /dev/null +++ b/vcl/qa/cppunit/a11y/atspi2/atspiwrapper.hxx @@ -0,0 +1,784 @@ +/* -*- 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/. + */ + +/* C++ wrapper for libatspi, so to make it less obnoxious to use */ + +/** + * Adding a new wrapper + * + * To wrap a new Atspi type (let's say, AtspiCollection), you need to: + * + * 1. Add <tt>DEFINE_GOBJECT_CAST(AtspiCollection, ATSPI_TYPE_COLLECTION)</tt> near the similar + * ones. This creates <tt>Atspi::cast<AtspiCollection*>(p)</tt> so that such a cast based on the + * C++ type is checked using the GType type system. + * 2. Add a declaration for the new wrapper class above Atspi::Accessible + * (<tt>class Collection;</tt>) so it can be used in step 3. + * 3. Add <tt>Atspi::Accessible::queryCollection()</tt> method. Its definition has to be in the + * source file as it requires a complete type for the wrapper class. The body just calls + * <tt>queryInterface<Collection>(atspi_accessible_get_collection_iface);</tt> and returns + * its value. + * 4. Add the definition of the new wrapper class: + * <tt>class Collection : public Accessible { ... }</tt> + * Use the existing wrappers as inspiration, but basically: + * - Define the constructor that only chains up to the parent + * - Define each wrapper method, which generally only have to call one of the <tt>invoke()</tt> + * helpers to wrap the C calls. There are a few, depending on some details of the C call: + * - @c GObjectWrapperBase::invoke(): this is the most basic one, that just calls the C method + * on @c GObjectWrapperBase::get() with the given arguments. Use this for calls not + * throwing an exception and either returning a plain value, or something not handled by + * one of the others below. + * - @c AtspiWrapperBase::invokeThrow(): like @c GObjectWrapperBase::invoke(), but for C calls + * that take a @c GError argument for throwing exceptions. @c invokeThrow() will + * transform any C exception into a a C++ exception (@c css::uno::RuntimeException) + * - @c AtspiWrapperBase::strInvoke(): like @c AtspiWrapperBase::invokeThrow(), but manages a + * C string (@c char*) return value as an @c std::string. Use this for C calls returning + * a C string. + * - @c AtspiWrapperBase::garrayInvoke(): like @c AtspiWrapperBase::invokeThrow(), but manages + * a @c GArray return value as an @c std::vector. Use this for C calls returning a + * @p GArray. + * - @c AtspiWrapperBase::hashMapInvoke(): like @c AtspiWrapperBase::invokeThrow(), but manages + * a @c GHashTable return value as an @c std::unordered_map. Use this for C calls + * returning a @p GHashTable. + * - @c AtspiWrapperBase::strHashMapInvoke(): identical to @c AtspiWrapperBase::hashMapInvoke() + * using C strings for keys and values. + * . + * If none of those match the exact return type of the C call to wrap, use + * @c AtspiWrapperBase::invokeThrow() or even @c GObjectWrapperBase::invoke(), and manually + * manage the result value. You can use Atspi::gmem functions to help. Basically the idea is + * that you always return a self-managing object to make memory management easy (whereas it's + * obnoxiously hard with plain C Atspi API). + */ + +#pragma once + +#include <vector> +#include <unordered_map> +#include <boost/type_traits/function_traits.hpp> + +#include <atspi/atspi.h> +#include <com/sun/star/container/NoSuchElementException.hpp> +#include <com/sun/star/uno/RuntimeException.hpp> + +#include <cppunit/TestAssert.h> + +#include <config_atspi.h> + +namespace Atspi +{ +/** @brief Helpers for managing GLib memory in a more C++-style */ +namespace gmem +{ +/** @brief Wraps a pointer to free with @c g_free() in a @c std::unique_ptr */ +template <typename T> static inline auto unique_gmem(T* ptr) +{ + return std::unique_ptr<T, decltype(&g_free)>(ptr, &g_free); +} + +/** @brief Wraps a @c GArray to free with @c g_array_unref() in a @c std::unique_ptr */ +static inline auto unique_garray(GArray* p) +{ + return std::unique_ptr<GArray, decltype(&g_array_unref)>(p, &g_array_unref); +} + +/** @brief Wraps a @c GHashTable to free with @c g_hash_table_unref() in a @c std::unique_ptr */ +static inline auto unique_ghashtable(GHashTable* p) +{ + return std::unique_ptr<GHashTable, decltype(&g_hash_table_unref)>(p, &g_hash_table_unref); +} +} + +// --- GObject cast wrappers based on type: usage is cast<AtspiAccessible*>(pCInstance) +#define DEFINE_GOBJECT_CAST(CType, GType) \ + template <typename P, typename T, std::enable_if_t<std::is_same_v<P, CType*>, int> = 1> \ + P cast(T* pInstance) \ + { \ + return G_TYPE_CHECK_INSTANCE_CAST(pInstance, GType, std::remove_pointer_t<P>); \ + } + +DEFINE_GOBJECT_CAST(AtspiStateSet, ATSPI_TYPE_STATE_SET) +DEFINE_GOBJECT_CAST(AtspiRelation, ATSPI_TYPE_RELATION) +DEFINE_GOBJECT_CAST(AtspiAccessible, ATSPI_TYPE_ACCESSIBLE) +DEFINE_GOBJECT_CAST(AtspiComponent, ATSPI_TYPE_COMPONENT) +DEFINE_GOBJECT_CAST(AtspiText, ATSPI_TYPE_TEXT) + +#undef DEFINE_GOBJECT_CAST +// --- end GObject cast wrappers + +class GLibEnumBase +{ +protected: + /** + * @brief Retrieves the string representation of an enumeration value + * @param gt The @c GType for the enumeration + * @param value The enumeration value for which to get the name for + * @param fallback Fallback value in case @p values falls outside the enumeration + * @returns A string representing @p value + */ + static std::string glibEnumValueName(GType gt, gint value, + std::string_view fallback = "unknown") + { + auto klass = static_cast<GEnumClass*>(g_type_class_ref(gt)); + auto enum_value = g_enum_get_value(klass, value); + std::string ret(enum_value ? enum_value->value_name : fallback); + g_type_class_unref(klass); + return ret; + } +}; + +class Role : private GLibEnumBase +{ +public: + static std::string getName(AtspiRole role) + { + return glibEnumValueName(atspi_role_get_type(), role); + } +}; + +class State : private GLibEnumBase +{ +public: + static std::string getName(AtspiStateType state) + { + return glibEnumValueName(atspi_state_type_get_type(), state); + } +}; + +class TextGranularity : private GLibEnumBase +{ +public: + static std::string getName(AtspiTextGranularity granularity) + { + return glibEnumValueName(atspi_text_granularity_get_type(), granularity); + } +}; + +class TextBoundaryType : private GLibEnumBase +{ +public: + static std::string getName(AtspiTextBoundaryType boundaryType) + { + return glibEnumValueName(atspi_text_boundary_type_get_type(), boundaryType); + } +}; + +/** + * @brief Base class for GObject wrappers + * + * This leverages std::shared_ptr as a cheap way of wrapping a raw pointer, and its deleter as a + * mean of using g_object_unref() to cleanup. This is sub-optimal as it maintains a separate + * refcount to the GObject one, but it's easy. + */ +template <class T> class GObjectWrapperBase : public std::shared_ptr<T> +{ +public: + /* this is the boundary of C++ type safety, so we can have inheritance working + * properly with the C types. This should still be safe as it uses cast() which should be + * defined for each using type with DEFINE_GOBJECT_CAST(), which uses GType validation */ + template <typename P = T*> P get() const { return cast<P>(std::shared_ptr<T>::get()); } + +protected: + /** + * @brief Calls the C function @p f on the C object wrapped by @p this + * @param f The C function to call + * @param args Additional arguments to @p f + * @returns The return value from @p f + * + * Calls the C function @p f similar to <tt>f(get(), args...)</tt>. Care is taken of + * transforming @c get() to the type actually expected as the first argument of @p f, using + * @c get<TypeOfFsFirstArgument>(), which performs a runtime verification of the conversion. + * + * @note The type verification on whether @p f actually takes what get() returns is performed + * at runtime, so there will be no compilation error or warning if trying to use an + * incompatible C function. A check will however be performed at runtime, at least + * helping diagnose a possible invalid conversion. + */ + template <typename F, typename... Ts> inline auto invoke(F f, Ts... args) const + { + using FT = std::remove_pointer_t<F>; + const auto p = get<typename boost::function_traits<FT>::arg1_type>(); + return f(p, args...); + } + +private: + static void deleter(T* p) + { + if (p) + g_object_unref(p); + } + +public: + /** + * @param pObj The raw GObject to wrap + * @param takeRef Whether to take ownership of the object or not. If set to @c false, it will + * call @c g_object_ref(pAcc) to acquire a new reference to the GObject. + */ + GObjectWrapperBase(T* pObj = nullptr, bool takeRef = true) + : std::shared_ptr<T>(pObj, deleter) + { + if (pObj && !takeRef) + g_object_ref(pObj); + } +}; + +/** @brief AtspiStateSet C++ wrapper */ +class StateSet : public GObjectWrapperBase<AtspiStateSet> +{ +public: + using GObjectWrapperBase::GObjectWrapperBase; + + void add(const AtspiStateType t) { invoke(atspi_state_set_add, t); } + StateSet compare(const StateSet& other) const + { + return StateSet(invoke(atspi_state_set_compare, other.get())); + } + bool contains(const AtspiStateType t) const { return invoke(atspi_state_set_contains, t); } + bool operator==(const StateSet& other) const + { + return invoke(atspi_state_set_equals, other.get()); + } + std::vector<AtspiStateType> getStates() const + { + auto garray = gmem::unique_garray(invoke(atspi_state_set_get_states)); + std::vector<AtspiStateType> states; + for (auto i = decltype(garray->len){ 0 }; i < garray->len; i++) + { + states.push_back(g_array_index(garray, decltype(states)::value_type, i)); + } + return states; + } + bool empty() const { return invoke(atspi_state_set_is_empty); } + void remove(AtspiStateType t) { invoke(atspi_state_set_remove, t); } + void setByName(const std::string_view name, bool enable) + { + invoke(atspi_state_set_set_by_name, name.data(), enable); + } +}; + +class Accessible; + +/** @brief AtspiRelation C++ wrapper */ +class Relation : public GObjectWrapperBase<AtspiRelation> +{ +public: + using GObjectWrapperBase::GObjectWrapperBase; + + AtspiRelationType getRelationType() const { return invoke(atspi_relation_get_relation_type); } + int getNTargets() const { return invoke(atspi_relation_get_n_targets); } + Accessible getTarget(int i) const; +}; + +/* intermediate base just for splitting out the *invoke* helpers implementations, so the actual + * user-targeted class can hold only the actual API */ +template <class T> class AtspiWrapperBase : public GObjectWrapperBase<T> +{ +protected: + using GObjectWrapperBase<T>::invoke; + + /** + * @brief Calls the throwing C function @p f on the C object wrapped by @p this + * @param f The C function to call + * @param args Additional arguments to @p f + * @returns The raw return value from @p f + * @throws css::uno::RuntimeException Exception @c GError are translated to + * + * This wrapper calls @p f with parameters @p args and an additional @c GError parameter to + * catch C exception, transforming them into C++ exceptions of type + * @c css::uno::RuntimeException. + * + * @see invoke() + */ + template <typename F, typename... Ts> inline auto invokeThrow(F f, Ts... args) const + { + GError* err = nullptr; + auto ret = invoke(f, args..., &err); + if (err) + { + throw css::uno::RuntimeException(OUString::fromUtf8(err->message)); + } + return ret; + } + + /** + * @brief Calls the throwing C function @p f on the C object wrapped by @p this and returns a string + * @param f The C function to call + * @param args Additional arguments to @p f + * @tparam E the type of exception to throw if @p f returns @c null, defaults to + * @c css::uno::RuntimeException + * @returns A string holding the return value from @p f + * @throws css::uno::RuntimeException See invokeThrow() + * @throws E Exception to use if @p f returns null with no other error + * + * Just like @c invokeThrow(), but wraps the return value in an @c std::string and manages the + * lifetime of the C function return value. + * + * As @c std::string cannot represent a @c null value, if @p f returned such a value without + * throwing an exception, this method will throw an exception of type @p E + * + * @see invokeThrow() + */ + template <typename E = css::uno::RuntimeException, typename F, typename... Ts> + inline std::string strInvoke(F f, Ts... args) const + { + auto r = invokeThrow(f, args...); + + /* if the API returned NULL without throwing, use the specified exception because a nullptr + * std::string is not valid, and std::logic_error isn't gonna be very useful to the caller */ + if (!r) + throw E(); + + return gmem::unique_gmem(r).get(); + } + + /** + * @brief Calls the throwing C function @p f on the C object wrapped by @p this and returns a vector + * @param f The C function to call + * @param args Additional arguments to @p f + * @tparam Vi The type of the members of the @c GArray @p f returns + * @tparam Vo The type of the members of the returned vector + * @returns A vector holding the return value from @p f + * @throws css::uno::RuntimeException See invokeThrow() + * + * Just like @c invokeThrow(), but wraps the return in an @c std::vector<Vo> and manages the + * lifetime of the C function return value. + * + * @p Vi has to be implicitly convertible to @p Vo. A typical usage could be + * <tt>garrayInvoke<AtspiAccessible*, Accessible>(...)</tt>, which would transform a @c GArray + * of @c AtspiAccessible* to an @c std::vector of @c Atspi::Accessible. + * + * @warning You have to get @p Vi right, there is no way to validate this type is correct or + * not, so you won't get a compilation error nor even a warning if you give the wrong + * type here. + * + * @see invokeThrow() + */ + template <typename Vi, typename Vo, typename F, typename... Ts> + inline std::vector<Vo> garrayInvoke(F f, Ts... args) const + { + auto garray = gmem::unique_garray(invokeThrow(f, args...)); + std::vector<Vo> vec; + for (auto i = decltype(garray->len){ 0 }; i < garray->len; i++) + vec.push_back(g_array_index(garray, Vi, i)); + return vec; + } + + /** + * @brief Wraps an AT-SPI call returning a GHashTable + * @tparam Ki Type of the keys in the wrapped hash table + * @tparam Vi Type of the values in the wrapped hash table + * @tparam Ko Type of the keys in the wrapper map (this must be convertible from Ki) + * @tparam Vo Type of the values in the wrapper map (this must be convertible from Kv) + * @param f The function to call + * @param args Arguments to pass to @p f + * @returns A @c std::unordered_map holding the data returned by @p f. + * + * @see invokeThrow() + * @see strHashMapInvoke() + */ + template <typename Ki, typename Vi, typename Ko, typename Vo, typename F, typename... Ts> + inline std::unordered_map<Ko, Vo> hashMapInvoke(F f, Ts... args) const + { + auto ghash = gmem::unique_ghashtable(invokeThrow(f, args...)); + std::unordered_map<Ko, Vo> map; + GHashTableIter iter; + g_hash_table_iter_init(&iter, ghash.get()); + gpointer key, value; + while (g_hash_table_iter_next(&iter, &key, &value)) + { + map.emplace(static_cast<Ki>(key), static_cast<Vi>(value)); + } + return map; + } + + /** + * @brief Just like @c hashMapInvoke() but already specialized for strings + * @param f The C function to call + * @param args Arguments to @p f + * @returns A @c std::unordered_map holding the data returned by @p f + * + * This is exactly the same as + * <tt>hashMapInvoke<gchar*, gchar*, std::string, std::string>(f, args...)</tt> + */ + template <typename F, typename... Ts> inline auto strHashMapInvoke(F f, Ts... args) const + { + return hashMapInvoke<gchar*, gchar*, std::string, std::string>(f, args...); + } + +public: + using GObjectWrapperBase<T>::GObjectWrapperBase; +}; + +class Component; +class Text; + +/** + * @brief AtspiAccessible C++ wrapper + * + * This is a wrapper for the AtspiAccessible GObject class to make it a bit nicer to use in C++, + * including a proper class with methods, regular exceptions, easy memory management, and an + * iterator to enumerate children. + * + * As this class actually inherits from std::shared_ptr, you can easily use @c get() to retrieve + * the wrapped pointer in case you need to, e.g. if some specific API is missing. However, take + * care of memory management on that object not to have it destroyed early (e.g. don't let anyone + * call @c g_object_unref() on it if they didn't call g_object_ref() first). + * + * To use it, just wrap an initial AtspiAccessible using the class constructor. + * @code + * Atspi::Accessible desktop(atspi_get_desktop(0)); + * for (auto&& app: desktop) { + * std::cout << app->getName() << std::endl; + * } + * @endcode + * + * For details on the specific methods, see the C Atspi documentation. + */ +class Accessible : public AtspiWrapperBase<AtspiAccessible> +{ +public: + using AtspiWrapperBase<AtspiAccessible>::AtspiWrapperBase; + + void setCacheMask(AtspiCache mask) const { invoke(atspi_accessible_set_cache_mask, mask); } + void clearCache() const { invoke(atspi_accessible_clear_cache); } + + AtspiRole getRole() const { return invokeThrow(atspi_accessible_get_role); } + std::string getRoleName() const { return strInvoke(atspi_accessible_get_role_name); } + std::string getName() const { return strInvoke(atspi_accessible_get_name); } + std::string getDescription() const { return strInvoke(atspi_accessible_get_description); } + + int getChildCount() const { return invokeThrow(atspi_accessible_get_child_count); } + Accessible getChildAtIndex(int idx) const + { + return Accessible(invokeThrow(atspi_accessible_get_child_at_index, idx)); + } + int getIndexInParent() const { return invokeThrow(atspi_accessible_get_index_in_parent); } + Accessible getParent() const { return Accessible(invokeThrow(atspi_accessible_get_parent)); } + + StateSet getStateSet() const { return StateSet(invoke(atspi_accessible_get_state_set)); } + + std::unordered_map<std::string, std::string> getAttributes() const + { + return strHashMapInvoke(atspi_accessible_get_attributes); + } + + std::vector<Relation> getRelationSet() const + { + return garrayInvoke<AtspiRelation*, Relation>(atspi_accessible_get_relation_set); + } + +private: + template <class I, typename F> I queryInterface(F f) const + { + auto pIface = invoke(f); + if (pIface) + return I(pIface); + throw css::uno::RuntimeException("Not implemented"); + } + +public: + Component queryComponent() const; + Text queryText() const; + + // convenience extensions + class iterator + { + public: + using iterator_category = std::input_iterator_tag; + using value_type = Accessible; + using difference_type = std::ptrdiff_t; + using reference = value_type&; + using pointer = value_type*; + + private: + const Accessible* m_pAccessible; + int m_idx; + + public: + explicit iterator(const Accessible* pAccessible, int idx = 0) + : m_pAccessible(pAccessible) + , m_idx(idx) + { + } + + iterator(iterator& other) + : m_pAccessible(other.m_pAccessible) + , m_idx(other.m_idx) + { + } + + iterator& operator++() + { + m_idx++; + return *this; + } + + iterator operator++(int) + { + iterator other = *this; + ++(*this); + return other; + } + + bool operator==(iterator other) const + { + return m_idx == other.m_idx && m_pAccessible == other.m_pAccessible; + } + bool operator!=(iterator other) const { return !(*this == other); } + value_type operator*() const + { + assert(m_idx < m_pAccessible->getChildCount()); + return m_pAccessible->getChildAtIndex(m_idx); + } + }; + + iterator begin() const { return iterator(this); } + iterator end() const + { + if (!get()) + return iterator(this); + return iterator(this, std::max(0, getChildCount())); + } +}; + +// we just use ATSPI's own structure here, but pass it by value +using Rect = AtspiRect; +using Point = AtspiPoint; + +/** @brief AtspiComponent C++ wrapper */ +class Component : public Accessible +{ +public: + Component(AtspiComponent* pObj = nullptr, bool takeRef = true) + : Accessible(cast<AtspiAccessible*>(pObj), takeRef) + { + } + + bool contains(int x, int y, AtspiCoordType coordType) const + { + return invokeThrow(atspi_component_contains, x, y, coordType); + } + Accessible getAccessibleAtPoint(int x, int y, AtspiCoordType coordType) const + { + return invokeThrow(atspi_component_get_accessible_at_point, x, y, coordType); + } + Rect getExtents(AtspiCoordType coordType) const + { + return *gmem::unique_gmem(invokeThrow(atspi_component_get_extents, coordType)); + } + Point getPosition(AtspiCoordType coordType) const + { + return *gmem::unique_gmem(invokeThrow(atspi_component_get_position, coordType)); + } + Point getSize() const { return *gmem::unique_gmem(invokeThrow(atspi_component_get_size)); } + AtspiComponentLayer getLayer() const { return invokeThrow(atspi_component_get_layer); } + short getMdiZOrder() const { return invokeThrow(atspi_component_get_mdi_z_order); } + bool grabFocus() const { return invokeThrow(atspi_component_grab_focus); } + double getAlpha() const { return invokeThrow(atspi_component_get_alpha); } + +#if HAVE_ATSPI2_SCROLL_TO + bool scrollTo(AtspiScrollType scrollType) const + { + return invokeThrow(atspi_component_scroll_to, scrollType); + } + bool scrollToPoint(AtspiCoordType coordType, int x, int y) const + { + return invokeThrow(atspi_component_scroll_to_point, coordType, x, y); + } +#endif // HAVE_ATSPI2_SCROLL_TO +}; + +/** @brief AtspiText C++ wrapper */ +class Text : public Accessible +{ +public: + Text(AtspiText* pObj = nullptr, bool takeRef = true) + : Accessible(cast<AtspiAccessible*>(pObj), takeRef) + { + } + + /** Wrapper for AtspiRange + * + * This is not actually required, but helps make this more C++-y (by allowing TextRange to + * inherit it) and more LibreOffice-y (by having cameCase names) */ + struct Range + { + int startOffset; + int endOffset; + + Range(int startOffset_, int endOffset_) + : startOffset(startOffset_) + , endOffset(endOffset_) + { + } + + Range(const AtspiRange* r) + : startOffset(r->start_offset) + , endOffset(r->end_offset) + { + } + }; + + /** Wrapper for AtspiTextRange */ + struct TextRange : Range + { + std::string content; + + TextRange(int startOffset_, int endOffset_, std::string_view content_) + : Range(startOffset_, endOffset_) + , content(content_) + { + } + + TextRange(const AtspiTextRange* r) + : Range(r->start_offset, r->end_offset) + , content(r->content) + { + } + }; + + int getCharacterCount() const { return invokeThrow(atspi_text_get_character_count); } + std::string getText(int startOffset, int endOffset) const + { + return strInvoke(atspi_text_get_text, startOffset, endOffset); + } + int getCaretOffset() const { return invokeThrow(atspi_text_get_caret_offset); } + + std::unordered_map<std::string, std::string> getTextAttributes(int offset, int* startOffset, + int* endOffset) const + { + return strHashMapInvoke(atspi_text_get_text_attributes, offset, startOffset, endOffset); + } + + std::unordered_map<std::string, std::string> + getAttributeRun(int offset, bool includeDefaults, int* startOffset, int* endOffset) const + { + return strHashMapInvoke(atspi_text_get_attribute_run, offset, includeDefaults, startOffset, + endOffset); + } + + std::unordered_map<std::string, std::string> getDefaultAttributes() const + { + return strHashMapInvoke(atspi_text_get_default_attributes); + } + + std::string getTextAttributeValue(int offset, std::string_view name) const + { + return strInvoke<css::container::NoSuchElementException>( + atspi_text_get_text_attribute_value, offset, const_cast<char*>(name.data())); + } + +protected: + /** Like @c invokeThrow() on C calls returning an @c AtspiTextRange */ + template <typename F, typename... Ts> inline TextRange invokeTextRange(F f, Ts... args) const + { + struct deleter + { + void operator()(AtspiTextRange* ptr) + { + g_free(ptr->content); + g_free(ptr); + } + }; + + std::unique_ptr<AtspiTextRange, deleter> r(invokeThrow(f, args...)); + return r.get(); + } + +public: + TextRange getStringAtOffset(int offset, AtspiTextGranularity granularity) const + { + return invokeTextRange(atspi_text_get_string_at_offset, offset, granularity); + } + + /* the next 3 are deprecated, but LO doesn't implement getStringAtOffset() itself so it's a lot + * trickier to test for */ + TextRange getTextBeforeOffset(int offset, AtspiTextBoundaryType boundary) const + { + return invokeTextRange(atspi_text_get_text_before_offset, offset, boundary); + } + TextRange getTextAtOffset(int offset, AtspiTextBoundaryType boundary) const + { + return invokeTextRange(atspi_text_get_text_at_offset, offset, boundary); + } + TextRange getTextAfterOffset(int offset, AtspiTextBoundaryType boundary) const + { + return invokeTextRange(atspi_text_get_text_after_offset, offset, boundary); + } + + sal_Int32 getCharacterAtOffset(int offset) const + { + return invokeThrow(atspi_text_get_character_at_offset, offset); + } + + Rect getCharacterExtents(int offset, AtspiCoordType type) const + { + return *gmem::unique_gmem(invokeThrow(atspi_text_get_character_extents, offset, type)); + } + + int getOffsetAtPoint(int x, int y, AtspiCoordType type) const + { + return invokeThrow(atspi_text_get_offset_at_point, x, y, type); + } + + Rect getRangeExtents(int startOffset, int endOffset, AtspiCoordType type) const + { + return *gmem::unique_gmem( + invokeThrow(atspi_text_get_range_extents, startOffset, endOffset, type)); + } + + // getBoundedRanges() ? + + int getNSelections() const { return invokeThrow(atspi_text_get_n_selections); } + + Range getSelection(gint selectionNum) const + { + return gmem::unique_gmem(invokeThrow(atspi_text_get_selection, selectionNum)).get(); + } + + bool addSelection(int startOffset, int endOffset) const + { + return invokeThrow(atspi_text_add_selection, startOffset, endOffset); + } + + bool removeSelection(int selectionNum) const + { + return invokeThrow(atspi_text_remove_selection, selectionNum); + } + + bool setSelection(int selectionNum, int startOffset, int endOffset) const + { + return invokeThrow(atspi_text_set_selection, selectionNum, startOffset, endOffset); + } + +#if HAVE_ATSPI2_SCROLL_TO + bool scrollSubstringTo(int startOffset, int endOffset, AtspiScrollType type) const + { + return invokeThrow(atspi_text_scroll_substring_to, startOffset, endOffset, type); + } + + bool scrollSubstringToPoint(int startOffset, int endOffset, AtspiCoordType coords, gint x, + gint y) const + { + return invokeThrow(atspi_text_scroll_substring_to_point, startOffset, endOffset, coords, x, + y); + } +#endif // HAVE_ATSPI2_SCROLL_TO +}; +} + +// CppUnit integration +CPPUNIT_NS_BEGIN +template <> struct assertion_traits<AtspiRole> +{ + static bool equal(const AtspiRole a, const AtspiRole b) { return a == b; } + + static std::string toString(const AtspiRole role) { return Atspi::Role::getName(role); } +}; +CPPUNIT_NS_END + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/vcl/qa/cppunit/a11y/atspi2/testdocuments/ecclectic.fodt b/vcl/qa/cppunit/a11y/atspi2/testdocuments/ecclectic.fodt new file mode 100644 index 000000000000..e67ded542fa1 --- /dev/null +++ b/vcl/qa/cppunit/a11y/atspi2/testdocuments/ecclectic.fodt @@ -0,0 +1,258 @@ +<?xml version='1.0' encoding='UTF-8'?> +<office:document xmlns:css3t="http://www.w3.org/TR/css3-text/" xmlns:grddl="http://www.w3.org/2003/g/data-view#" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xforms="http://www.w3.org/2002/xforms" xmlns:dom="http://www.w3.org/2001/xml-events" xmlns:script="urn:oasis:names:tc:opendocument:xmlns:script:1.0" xmlns:form="urn:oasis:names:tc:opendocument:xmlns:form:1.0" xmlns:math="http://www.w3.org/1998/Math/MathML" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:ooo="http://openoffice.org/2004/office" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:config="urn:oasis:names:tc:opendocument:xmlns:config:1.0" xmlns:ooow="http://openoffice.org/2004/writer" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:drawooo="http://openoffice.org/2010/draw" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:oooc="http://openoffice.org/2004/calc" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:calcext="urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:of="urn:oasis:names:tc:opendocument:xmlns:of:1.2" xmlns:tableooo="http://openoffice.org/2009/table" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:dr3d="urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0" xmlns:rpt="http://openoffice.org/2005/report" xmlns:formx="urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:form:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" xmlns:chart="urn:oasis:names:tc:opendocument:xmlns:chart:1.0" xmlns:officeooo="http://openoffice.org/2009/office" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:field="urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text"> + <office:meta><meta:creation-date>2023-04-25T17:37:52.159526391</meta:creation-date><dc:date>2023-06-14T10:55:24.745981841</dc:date><meta:editing-duration>P6DT23H42M46S</meta:editing-duration><meta:editing-cycles>21</meta:editing-cycles><meta:generator>LibreOfficeDev/7.7.0.0.alpha0$Linux_X86_64 LibreOffice_project/713f814ae7d4ab409dfdaa4d432d2fb61122e811</meta:generator><meta:document-statistic meta:table-count="0" meta:image-count="0" meta:object-count="0" meta:page-count="2" meta:paragraph-count="11" meta:word-count="501" meta:character-count="3116" meta:non-whitespace-character-count="2626"/></office:meta> + <office:font-face-decls> + <style:font-face style:name="DejaVu Sans" svg:font-family="'DejaVu Sans'" style:font-family-generic="system" style:font-pitch="variable"/> + <style:font-face style:name="FreeSans1" svg:font-family="FreeSans" style:font-family-generic="system" style:font-pitch="variable"/> + <style:font-face style:name="Liberation Sans" svg:font-family="'Liberation Sans'" style:font-family-generic="swiss" style:font-pitch="variable"/> + <style:font-face style:name="Liberation Serif" svg:font-family="'Liberation Serif'" style:font-family-generic="roman"/> + <style:font-face style:name="Liberation Serif1" svg:font-family="'Liberation Serif'" style:font-family-generic="roman" style:font-pitch="variable"/> + </office:font-face-decls> + <office:styles> + <style:default-style style:family="graphic"> + <style:graphic-properties svg:stroke-color="#3465a4" draw:fill-color="#729fcf" fo:wrap-option="no-wrap" draw:shadow-offset-x="0.3cm" draw:shadow-offset-y="0.3cm" draw:start-line-spacing-horizontal="0.283cm" draw:start-line-spacing-vertical="0.283cm" draw:end-line-spacing-horizontal="0.283cm" draw:end-line-spacing-vertical="0.283cm" style:writing-mode="lr-tb" style:flow-with-text="false"/> + <style:paragraph-properties style:text-autospace="ideograph-alpha" style:line-break="strict" loext:tab-stop-distance="0cm" style:writing-mode="lr-tb" style:font-independent-line-spacing="false"> + <style:tab-stops/> + </style:paragraph-properties> + <style:text-properties style:use-window-font-color="true" loext:opacity="0%" style:font-name="Liberation Serif1" fo:font-size="12pt" fo:language="fr" fo:country="FR" style:letter-kerning="true" style:font-name-asian="DejaVu Sans" style:font-size-asian="10.5pt" style:language-asian="zh" style:country-asian="CN" style:font-name-complex="FreeSans1" style:font-size-complex="12pt" style:language-complex="hi" style:country-complex="IN"/> + </style:default-style> + <style:default-style style:family="paragraph"> + <style:paragraph-properties fo:orphans="2" fo:widows="2" fo:hyphenation-ladder-count="no-limit" style:text-autospace="ideograph-alpha" style:punctuation-wrap="hanging" style:line-break="strict" style:tab-stop-distance="1.251cm" style:writing-mode="page"/> + <style:text-properties style:use-window-font-color="true" loext:opacity="0%" style:font-name="Liberation Serif1" fo:font-size="12pt" fo:language="fr" fo:country="FR" style:letter-kerning="true" style:font-name-asian="DejaVu Sans" style:font-size-asian="10.5pt" style:language-asian="zh" style:country-asian="CN" style:font-name-complex="FreeSans1" style:font-size-complex="12pt" style:language-complex="hi" style:country-complex="IN" fo:hyphenate="false" fo:hyphenation-remain-char-count="2" fo:hyphenation-push-char-count="2" loext:hyphenation-no-caps="false" loext:hyphenation-no-last-word="false" loext:hyphenation-word-char-count="5" loext:hyphenation-zone="no-limit"/> + </style:default-style> + <style:default-style style:family="table"> + <style:table-properties table:border-model="collapsing"/> + </style:default-style> + <style:default-style style:family="table-row"> + <style:table-row-properties fo:keep-together="auto"/> + </style:default-style> + <style:style style:name="Standard" style:family="paragraph" style:class="text"/> + <style:style style:name="Heading" style:family="paragraph" style:parent-style-name="Standard" style:next-style-name="Text_20_body" style:class="text"> + <style:paragraph-properties fo:margin-top="0.423cm" fo:margin-bottom="0.212cm" style:contextual-spacing="false" fo:keep-with-next="always"/> + <style:text-properties style:font-name="Liberation Sans" fo:font-family="'Liberation Sans'" style:font-family-generic="swiss" style:font-pitch="variable" fo:font-size="14pt" style:font-name-asian="DejaVu Sans" style:font-family-asian="'DejaVu Sans'" style:font-family-generic-asian="system" style:font-pitch-asian="variable" style:font-size-asian="14pt" style:font-name-complex="FreeSans1" style:font-family-complex="FreeSans" style:font-family-generic-complex="system" style:font-pitch-complex="variable" style:font-size-complex="14pt"/> + </style:style> + <style:style style:name="Text_20_body" style:display-name="Text body" style:family="paragraph" style:parent-style-name="Standard" style:class="text"> + <style:paragraph-properties fo:margin-top="0cm" fo:margin-bottom="0.247cm" style:contextual-spacing="false" fo:line-height="115%"/> + </style:style> + <style:style style:name="Heading_20_3" style:display-name="Heading 3" style:family="paragraph" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:default-outline-level="3" style:list-style-name="" style:class="text"> + <style:paragraph-properties fo:margin-top="0.247cm" fo:margin-bottom="0.212cm" style:contextual-spacing="false"/> + <style:text-properties style:font-name="Liberation Serif1" fo:font-family="'Liberation Serif'" style:font-family-generic="roman" style:font-pitch="variable" fo:font-size="14pt" fo:font-weight="bold" style:font-name-asian="DejaVu Sans" style:font-family-asian="'DejaVu Sans'" style:font-family-generic-asian="system" style:font-pitch-asian="variable" style:font-size-asian="14pt" style:font-weight-asian="bold" style:font-name-complex="FreeSans1" style:font-family-complex="FreeSans" style:font-family-generic-complex="system" style:font-pitch-complex="variable" style:font-size-complex="14pt" style:font-weight-complex="bold"/> + </style:style> + <style:style style:name="Strong_20_Emphasis" style:display-name="Strong Emphasis" style:family="text"> + <style:text-properties fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold"/> + </style:style> + <style:style style:name="Line_20_numbering" style:display-name="Line numbering" style:family="text"/> + <text:outline-style style:name="Outline"> + <text:outline-level-style text:level="1" loext:num-list-format="%1%" style:num-format=""> + <style:list-level-properties text:list-level-position-and-space-mode="label-alignment"> + <style:list-level-label-alignment text:label-followed-by="listtab"/> + </style:list-level-properties> + </text:outline-level-style> + <text:outline-level-style text:level="2" loext:num-list-format="%2%" style:num-format=""> + <style:list-level-properties text:list-level-position-and-space-mode="label-alignment"> + <style:list-level-label-alignment text:label-followed-by="listtab"/> + </style:list-level-properties> + </text:outline-level-style> + <text:outline-level-style text:level="3" loext:num-list-format="%3%" style:num-format=""> + <style:list-level-properties text:list-level-position-and-space-mode="label-alignment"> + <style:list-level-label-alignment text:label-followed-by="listtab"/> + </style:list-level-properties> + </text:outline-level-style> + <text:outline-level-style text:level="4" loext:num-list-format="%4%" style:num-format=""> + <style:list-level-properties text:list-level-position-and-space-mode="label-alignment"> + <style:list-level-label-alignment text:label-followed-by="listtab"/> + </style:list-level-properties> + </text:outline-level-style> + <text:outline-level-style text:level="5" loext:num-list-format="%5%" style:num-format=""> + <style:list-level-properties text:list-level-position-and-space-mode="label-alignment"> + <style:list-level-label-alignment text:label-followed-by="listtab"/> + </style:list-level-properties> + </text:outline-level-style> + <text:outline-level-style text:level="6" loext:num-list-format="%6%" style:num-format=""> + <style:list-level-properties text:list-level-position-and-space-mode="label-alignment"> + <style:list-level-label-alignment text:label-followed-by="listtab"/> + </style:list-level-properties> + </text:outline-level-style> + <text:outline-level-style text:level="7" loext:num-list-format="%7%" style:num-format=""> + <style:list-level-properties text:list-level-position-and-space-mode="label-alignment"> + <style:list-level-label-alignment text:label-followed-by="listtab"/> + </style:list-level-properties> + </text:outline-level-style> + <text:outline-level-style text:level="8" loext:num-list-format="%8%" style:num-format=""> + <style:list-level-properties text:list-level-position-and-space-mode="label-alignment"> + <style:list-level-label-alignment text:label-followed-by="listtab"/> + </style:list-level-properties> + </text:outline-level-style> + <text:outline-level-style text:level="9" loext:num-list-format="%9%" style:num-format=""> + <style:list-level-properties text:list-level-position-and-space-mode="label-alignment"> + <style:list-level-label-alignment text:label-followed-by="listtab"/> + </style:list-level-properties> + </text:outline-level-style> + <text:outline-level-style text:level="10" loext:num-list-format="%10%" style:num-format=""> + <style:list-level-properties text:list-level-position-and-space-mode="label-alignment"> + <style:list-level-label-alignment text:label-followed-by="listtab"/> + </style:list-level-properties> + </text:outline-level-style> + </text:outline-style> + <text:notes-configuration text:note-class="footnote" style:num-format="1" text:start-value="0" text:footnotes-position="page" text:start-numbering-at="document"/> + <text:notes-configuration text:note-class="endnote" style:num-format="i" text:start-value="0"/> + <text:linenumbering-configuration text:style-name="Line_20_numbering" text:number-lines="false" text:offset="0.499cm" style:num-format="1" text:number-position="left" text:increment="5"/> + <loext:theme loext:name="Office Theme"> + <loext:theme-colors loext:name="LibreOffice"> + <loext:color loext:name="dark1" loext:color="#000000"/> + <loext:color loext:name="light1" loext:color="#ffffff"/> + <loext:color loext:name="dark2" loext:color="#000000"/> + <loext:color loext:name="light2" loext:color="#ffffff"/> + <loext:color loext:name="accent1" loext:color="#18a303"/> + <loext:color loext:name="accent2" loext:color="#0369a3"/> + <loext:color loext:name="accent3" loext:color="#a33e03"/> + <loext:color loext:name="accent4" loext:color="#8e03a3"/> + <loext:color loext:name="accent5" loext:color="#c99c00"/> + <loext:color loext:name="accent6" loext:color="#c9211e"/> + <loext:color loext:name="hyperlink" loext:color="#0000ee"/> + <loext:color loext:name="followed-hyperlink" loext:color="#551a8b"/> + </loext:theme-colors> + </loext:theme> + </office:styles> + <office:automatic-styles> + <style:style style:name="P1" style:family="paragraph" style:parent-style-name="Text_20_body"> + <style:paragraph-properties fo:margin-left="1cm" fo:margin-right="0cm" fo:line-height="200%" fo:text-align="justify" style:justify-single-word="false" fo:text-indent="1cm" style:auto-text-indent="false" style:writing-mode="lr-tb"/> + </style:style> + <style:style style:name="P2" style:family="paragraph" style:parent-style-name="Text_20_body"> + <style:text-properties style:font-name="Liberation Serif1"/> + </style:style> + <style:style style:name="T1" style:family="text"> + <style:text-properties fo:font-style="italic" style:font-style-asian="italic" style:font-style-complex="italic"/> + </style:style> + <style:style style:name="T2" style:family="text"> + <style:text-properties fo:font-variant="small-caps"/> + </style:style> + <style:style style:name="T3" style:family="text"> + <style:text-properties style:font-relief="engraved"/> + </style:style> + <style:style style:name="T4" style:family="text"> + <style:text-properties style:text-overline-style="solid" style:text-overline-type="double" style:text-overline-width="auto" style:text-overline-color="font-color"/> + </style:style> + <style:style style:name="T5" style:family="text"> + <style:text-properties style:text-outline="true" style:text-blinking="true"/> + </style:style> + <style:style style:name="T6" style:family="text"> + <style:text-properties style:text-rotation-angle="90" style:text-rotation-scale="line-height"/> + </style:style> + <style:style style:name="T7" style:family="text"> + <style:text-properties fo:letter-spacing="0.194cm"/> + </style:style> + <style:style style:name="T8" style:family="text"> + <style:text-properties style:text-position="super 58%" fo:background-color="transparent" loext:char-shading-value="0"/> + </style:style> + <style:style style:name="T9" style:family="text"> + <style:text-properties style:text-position="sub 58%"/> + </style:style> + <style:style style:name="T10" style:family="text"> + <style:text-properties fo:background-color="#ff6600" loext:char-shading-value="0"/> + </style:style> + <style:style style:name="T11" style:family="text"> + <style:text-properties fo:background-color="transparent" loext:char-shading-value="0" loext:shadow="#808080 0.176cm 0.176cm"/> + </style:style> + <style:style style:name="T12" style:family="text"> + <style:text-properties style:font-name="Liberation Serif"/> + </style:style> + <style:style style:name="T13" style:family="text"> + <style:text-properties style:font-name="Liberation Serif1"/> + </style:style> + <style:style style:name="T14" style:family="text"> + <style:text-properties/> + </style:style> + <style:style style:name="T15" style:family="text"> + <style:text-properties fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold"/> + </style:style> + <style:style style:name="Sect1" style:family="section"> + <style:section-properties fo:margin-left="0cm" fo:margin-right="0cm" style:editable="false"> + <style:columns fo:column-count="1" fo:column-gap="0cm"/> + </style:section-properties> + </style:style> + <style:page-layout style:name="pm1"> + <style:page-layout-properties fo:page-width="21.001cm" fo:page-height="29.7cm" style:num-format="1" style:print-orientation="portrait" fo:margin-top="2cm" fo:margin-bottom="2cm" fo:margin-left="2cm" fo:margin-right="2cm" style:writing-mode="lr-tb" style:layout-grid-color="#c0c0c0" style:layout-grid-lines="20" style:layout-grid-base-height="0.706cm" style:layout-grid-ruby-height="0.353cm" style:layout-grid-mode="none" style:layout-grid-ruby-below="false" style:layout-grid-print="false" style:layout-grid-display="false" style:footnote-max-height="0cm" loext:margin-gutter="0cm"> + <style:footnote-sep style:width="0.018cm" style:distance-before-sep="0.101cm" style:distance-after-sep="0.101cm" style:line-style="solid" style:adjustment="left" style:rel-width="25%" style:color="#000000"/> + </style:page-layout-properties> + <style:header-style/> + <style:footer-style/> + </style:page-layout> + <style:style style:name="dp1" style:family="drawing-page"> + <style:drawing-page-properties draw:background-size="full"/> + </style:style> + </office:automatic-styles> + <office:master-styles> + <style:master-page style:name="Standard" style:page-layout-name="pm1" draw:style-name="dp1"/> + </office:master-styles> + <office:body> + <office:text text:use-soft-page-breaks="true"> + <text:tracked-changes text:track-changes="false"> + <text:changed-region xml:id="ct93860566703600" text:id="ct93860566703600"> + <text:deletion> + <office:change-info> + <dc:creator>Unknown Author</dc:creator> + <dc:date>2023-06-14T10:49:34</dc:date> + </office:change-info> + </text:deletion> + </text:changed-region> + <text:changed-region xml:id="ct93860566704768" text:id="ct93860566704768"> + <text:insertion> + <office:change-info> + <dc:creator>Unknown Author</dc:creator> + <dc:date>2023-06-14T10:49:34</dc:date> + </office:change-info> + </text:insertion> + </text:changed-region> + <text:changed-region xml:id="ct93860566705104" text:id="ct93860566705104"> + <text:deletion> + <office:change-info> + <dc:creator>Unknown Author</dc:creator> + <dc:date>2023-06-14T10:49:46</dc:date> + </office:change-info> + </text:deletion> + </text:changed-region> + <text:changed-region xml:id="ct93860566691808" text:id="ct93860566691808"> + <text:insertion> + <office:change-info> + <dc:creator>Unknown Author</dc:creator> + <dc:date>2023-06-14T10:49:46</dc:date> + </office:change-info> + </text:insertion> + </text:changed-region> + <text:changed-region xml:id="ct93860566693104" text:id="ct93860566693104"> + <text:format-change> + <office:change-info> + <dc:creator>Unknown Author</dc:creator> + <dc:date>2023-06-14T10:49:59</dc:date> + </office:change-info> + </text:format-change> + </text:changed-region> + </text:tracked-changes> + <text:sequence-decls> + <text:sequence-decl text:display-outline-level="0" text:name="Illustration"/> + <text:sequence-decl text:display-outline-level="0" text:name="Table"/> + <text:sequence-decl text:display-outline-level="0" text:name="Text"/> + <text:sequence-decl text:display-outline-level="0" text:name="Drawing"/> + <text:sequence-decl text:display-outline-level="0" text:name="Figure"/> + </text:sequence-decls> + <text:h text:style-name="Heading_20_3" text:outline-level="3">The standard Lorem Ipsum passage, used since the 1500s</text:h> + <text:section text:style-name="Sect1" text:name="Translation"> + <text:p text:style-name="Text_20_body">"Lorem <text:span text:style-name="T1">ipsum dolor</text:span> sit <text:span text:style-name="Strong_20_Emphasis">amet</text:span>, consectetur <text:span text:style-name="T2">adipiscing</text:span> elit, <text:span text:style-name="T3">sed</text:span> do <text:span text:style-name="T4">eiusmod</text:span> tempor <text:span text:style-name="T5">incididunt</text:span> ut labore et <text:span text:style-name="T6">dolore</text:span> magna aliqua. Ut enim ad minim <text:span text:style-name="T7">veniam</text:span>, quis <text:span text:style-name="T8">nostrud</text:span> <text:span text:style-name="T9">exercitation</text:span> ullamco <text:span text:style-name="T10">laboris</text:span> nisi ut <text:span text:style-name="T11">aliquip</text:span> ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."</text:p> + <text:h text:style-name="Heading_20_3" text:outline-level="3">Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC</text:h> + <text:p text:style-name="P1">"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto qui dolorem eum fugiat quo voluptas nulla pariatur?"</text:p> + <text:h text:style-name="Heading_20_3" text:outline-level="3">1914 translation by H. Rackham</text:h> + <text:p text:style-name="Text_20_body">"But I must <text:change-start text:change-id="ct93860566703600"/>explain to<text:change-end text:change-id="ct93860566703600"/><text:change-start text:change-id="ct93860566704768"/><text:span text:style-name="T14">tell</text:span><text:change-end text:change-id="ct93860566704768"/> you how all this <text:change-start text:change-id="ct93860566705104"/>mistaken<text:change-end text:change-id="ct93860566705104"/><text:change-start text:change-id="ct93860566691808"/><text:span text:style-name="T14">absurd</text:span><text:change-end text:change-id="ct93860566691808"/> idea of denouncing pleasure and praising <text:change-start text:change-id="ct93860566693104"/><text:span text:style-name="T15">pain</text:span><text:change-end text:change-id="ct93860566693104"/> was born and I will give you a complete account of the system, and expound the actual teachings of the annoying consequences, or one who avoids a pain that produces no resultant pleasure?"</text:p> + </text:section> + <text:p text:style-name="Text_20_body"><text:span text:style-name="T12">Unicode < U+FFFF: fl</text:span><text:span text:style-name="T13"><text:line-break/>SMP Unicode (> U+FFFF): 🜂🜃</text:span></text:p> + <text:p text:style-name="P2"/> + <text:p text:style-name="P2"/> + <text:p text:style-name="P2"/> + <text:h text:style-name="Heading_20_3" text:outline-level="3">Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC</text:h> + <text:p text:style-name="Text_20_body">"At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat."</text:p> + <text:h text:style-name="Heading_20_3" text:outline-level="3"><text:soft-page-break/>1914 translation by H. Rackham</text:h> + <text:p text:style-name="Text_20_body">"On the other hand, we denounce with righteous indignation and dislike men who are so beguiled and demoralized by the charms of pleasure of the moment, so blinded by desire, that they cannot foresee the pain and trouble that are bound to ensue; and equal blame belongs to those who fail in their duty through weakness of will, which is the same as saying through shrinking from toil and pain. These cases are perfectly simple and easy to distinguish. In a free hour, when our power of choice is untrammelled and when nothing prevents our being able to do what we like best, every pleasure is to be welcomed and every pain avoided. But in certain circumstances and owing to the claims of duty or the obligations of business it will frequently occur that pleasures have to be repudiated and annoyances accepted. The wise man therefore always holds in these matters to this principle of selection: he rejects pleasures to secure other greater pleasures, or else he endures pains to avoid worse pains."</text:p> + <text:p text:style-name="Text_20_body"/> + </office:text> + </office:body> +</office:document>
\ No newline at end of file |