diff --git a/NEWS b/NEWS index a88b96d..c77ab6c 100644 --- a/NEWS +++ b/NEWS @@ -8452,7 +8452,7 @@ General: some areas, and has fixed a number of latent bugs. It also enables finer granularity for switching voices and helps set us up for incorporating audio cues. Please help us by testing with the latest - code and by reporting issues and suggestions at cthulhu-list@gnome.org. + code and by reporting issues and suggestions at https://groups.io/g/stormux. * Fix for bgo#583274 - portability for cthulhu script (Thomas Klausner) diff --git a/README-DEVELOPMENT.md b/README-DEVELOPMENT.md index 0025013..8df9733 100644 --- a/README-DEVELOPMENT.md +++ b/README-DEVELOPMENT.md @@ -83,7 +83,7 @@ busctl --user call org.stormux.Cthulhu.Service /org/stormux/Cthulhu/Service org. ## Git Repository Management The `.gitignore` file is configured to exclude: -- Build artifacts (`configure`, `Makefile`, etc.) +- Build artifacts (`_build`, `_build_releasecheck`, `meson-private`, etc.) - Generated Python files (`cthulhu_bin.py`, `cthulhu_i18n.py`, etc.) - Python bytecode (`*.pyc`, `__pycache__/`) diff --git a/README.md b/README.md index 883a3af..70a19d9 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Note -If you somehow stumbled across this while looking for a desktop screen reader for Linux, you most likely want [Orca](https://orca.gnome.org/) instead. Cthulhu is currently a supplemental screen reader that fills a nitch for some advanced users. E.g. some older QT based programs may work with Cthulhu, and if you use certain window managers like i3, Mozilla applications like Firefox and Thunderbird may work better. +Cthulhu is a fork of the Orca screen reader. Project home: https://git.stormux.org/storm/cthulhu. Cthulhu is currently a supplemental screen reader that fills a nitch for some advanced users. E.g. some older QT based programs may work with Cthulhu, and if you use certain window managers like i3, Mozilla applications like Firefox and Thunderbird may work better. ## Introduction @@ -37,7 +37,7 @@ toolkit, OpenOffice/LibreOffice, Gecko, WebKitGtk, and KDE Qt toolkit. ### Sleep Mode - **Application-specific**: Disable speech/events per application -- **Toggle keybinding**: `Cthulhu+Ctrl+Alt+Shift+Q` (matches Orca) +- **Toggle keybinding**: `Cthulhu+Ctrl+Alt+Shift+Q` - **Preserves exit key**: Only sleep toggle remains active ### Self-Voicing diff --git a/ci/build_and_install.sh b/ci/build_and_install.sh deleted file mode 100644 index 467b0cc..0000000 --- a/ci/build_and_install.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -set -eux - -meson setup _build --prefix=/usr --buildtype=debugoptimized -meson compile -C _build -meson install -C _build diff --git a/ci/container_builds.yml b/ci/container_builds.yml deleted file mode 100644 index 333a93d..0000000 --- a/ci/container_builds.yml +++ /dev/null @@ -1,55 +0,0 @@ -include: - - remote: "https://gitlab.freedesktop.org/freedesktop/ci-templates/-/raw/80f87b3058efb75a1faae11826211375fba77e7f/templates/opensuse.yml" - -variables: - # When branching change the suffix to avoid conflicts with images - # from the main branch - BASE_TAG: "2023-06-09.0-master" - FDO_UPSTREAM_REPO: "gnome/cthulhu" - -.cthulhu_opensuse_tumbleweed_x86_64: - variables: - FDO_DISTRIBUTION_VERSION: "tumbleweed" - FDO_DISTRIBUTION_TAG: "x86_64-${BASE_TAG}" - -opensuse-container@x86_64: - extends: - - .cthulhu_opensuse_tumbleweed_x86_64 - - .fdo.container-build@opensuse@x86_64 - stage: "container-build" - variables: - FDO_DISTRIBUTION_PACKAGES: >- - autoconf - automake - dbus-1 - dbus-1-devel - gcc - gettext - gettext-tools - git - glib2-devel - gobject-introspection-devel - gsettings-desktop-schemas - gstreamer-devel - itstool - libtool - libXi-devel - libXtst-devel - libxkbcommon-devel - libxml2-devel - libX11-devel - make - meson - ninja - python3 - python3-brlapi - python3-louis - python3-pip - python3-speechd - python310-gobject-devel - python310-simplejson - xvfb-run - yelp-devel - yelp-tools - FDO_DISTRIBUTION_EXEC: >- - pip3 install ruff diff --git a/ci/install_atspi.sh b/ci/install_atspi.sh deleted file mode 100644 index 1e4e430..0000000 --- a/ci/install_atspi.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh - -set -eux -o pipefail - -git clone --depth 1 https://gitlab.gnome.org/GNOME/at-spi2-core.git -cd at-spi2-core -mkdir _build - -meson setup --buildtype=debug --prefix=/usr _build . -meson compile -C _build -meson install -C _build diff --git a/ci/install_pyatspi2.sh b/ci/install_pyatspi2.sh deleted file mode 100644 index 141992e..0000000 --- a/ci/install_pyatspi2.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh - -set -eux -o pipefail - -git clone --depth 1 https://gitlab.gnome.org/GNOME/pyatspi2.git -cd pyatspi2 -mkdir _build -cd _build - -../autogen.sh --prefix=/usr -make -make install diff --git a/ci/pull-container-image.sh b/ci/pull-container-image.sh deleted file mode 100644 index 86b44d2..0000000 --- a/ci/pull-container-image.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/sh -# -# Utility script so you can pull the container image from CI for local development. -# Run this script and follow the instructions; the script will tell you how -# to run "podman run" to launch a container that has the same environment as the -# one used during CI pipelines. You can debug things at leisure there. - -set -eu -set -o pipefail - -CONTAINER_BUILDS=ci/container_builds.yml - -if [ ! -f $CONTAINER_BUILDS ] -then - echo "Please run this from the toplevel source directory in cthulhu" - exit 1 -fi - -tag=$(grep -e '^ BASE_TAG:' $CONTAINER_BUILDS | head -n 1 | sed -E 's/.*BASE_TAG: "(.+)"/\1/') -full_tag=x86_64-$tag -echo full_tag=\"$full_tag\" - -image_name=registry.gitlab.gnome.org/gnome/cthulhu/opensuse/tumbleweed:$full_tag - -echo pulling image $image_name -podman pull $image_name - -echo "" -echo "You can now run this:" -echo " podman run --rm -ti --cap-add=SYS_PTRACE -v \$(pwd):/srv/project -w /srv/project $image_name" diff --git a/cthulhu.doap b/cthulhu.doap index 7fb726b..fcdc304 100644 --- a/cthulhu.doap +++ b/cthulhu.doap @@ -7,10 +7,10 @@ cthulhu Screen reader for individuals who are blind or visually impaired Screen reader for individuals who are blind or visually impaired - - - - + + + + Python diff --git a/distro-packages/Arch-Linux/PKGBUILD b/distro-packages/Arch-Linux/PKGBUILD index 5eb1d42..9a4f9f0 100644 --- a/distro-packages/Arch-Linux/PKGBUILD +++ b/distro-packages/Arch-Linux/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Storm Dragon pkgname=cthulhu -pkgver=2025.12.12 +pkgver=2025.12.28 pkgrel=1 pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca" url="https://git.stormux.org/storm/cthulhu" diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 0f41e45..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,652 +0,0 @@ -# Makefile.in generated by automake 1.18.1 from Makefile.am. -# docs/Makefile. Generated from Makefile.in by configure. - -# Copyright (C) 1994-2025 Free Software Foundation, Inc. - -# This Makefile.in is free software; the Free Software Foundation -# gives unlimited permission to copy and/or distribute it, -# with or without modifications, as long as this notice is preserved. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY, to the extent permitted by law; without -# even the implied warranty of MERCHANTABILITY or FITNESS FOR A -# PARTICULAR PURPOSE. - - - -am__is_gnu_make = { \ - if test -z '$(MAKELEVEL)'; then \ - false; \ - elif test -n '$(MAKE_HOST)'; then \ - true; \ - elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \ - true; \ - else \ - false; \ - fi; \ -} -am__make_running_with_option = \ - case $${target_option-} in \ - ?) ;; \ - *) echo "am__make_running_with_option: internal error: invalid" \ - "target option '$${target_option-}' specified" >&2; \ - exit 1;; \ - esac; \ - has_opt=no; \ - sane_makeflags=$$MAKEFLAGS; \ - if $(am__is_gnu_make); then \ - sane_makeflags=$$MFLAGS; \ - else \ - case $$MAKEFLAGS in \ - *\\[\ \ ]*) \ - bs=\\; \ - sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \ - | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \ - esac; \ - fi; \ - skip_next=no; \ - strip_trailopt () \ - { \ - flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \ - }; \ - for flg in $$sane_makeflags; do \ - test $$skip_next = yes && { skip_next=no; continue; }; \ - case $$flg in \ - *=*|--*) continue;; \ - -*I) strip_trailopt 'I'; skip_next=yes;; \ - -*I?*) strip_trailopt 'I';; \ - -*O) strip_trailopt 'O'; skip_next=yes;; \ - -*O?*) strip_trailopt 'O';; \ - -*l) strip_trailopt 'l'; skip_next=yes;; \ - -*l?*) strip_trailopt 'l';; \ - -[dEDm]) skip_next=yes;; \ - -[JT]) skip_next=yes;; \ - esac; \ - case $$flg in \ - *$$target_option*) has_opt=yes; break;; \ - esac; \ - done; \ - test $$has_opt = yes -am__make_dryrun = (target_option=n; $(am__make_running_with_option)) -am__make_keepgoing = (target_option=k; $(am__make_running_with_option)) -am__rm_f = rm -f $(am__rm_f_notfound) -am__rm_rf = rm -rf $(am__rm_f_notfound) -pkgdatadir = $(datadir)/cthulhu -pkgincludedir = $(includedir)/cthulhu -pkglibdir = $(libdir)/cthulhu -pkglibexecdir = $(libexecdir)/cthulhu -am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd -install_sh_DATA = $(install_sh) -c -m 644 -install_sh_PROGRAM = $(install_sh) -c -install_sh_SCRIPT = $(install_sh) -c -INSTALL_HEADER = $(INSTALL_DATA) -transform = $(program_transform_name) -NORMAL_INSTALL = : -PRE_INSTALL = : -POST_INSTALL = : -NORMAL_UNINSTALL = : -PRE_UNINSTALL = : -POST_UNINSTALL = : -build_triplet = x86_64-pc-linux-gnu -host_triplet = x86_64-pc-linux-gnu -subdir = docs -ACLOCAL_M4 = $(top_srcdir)/aclocal.m4 -am__aclocal_m4_deps = $(top_srcdir)/m4/build-to-host.m4 \ - $(top_srcdir)/m4/gettext.m4 $(top_srcdir)/m4/host-cpu-c-abi.m4 \ - $(top_srcdir)/m4/iconv.m4 $(top_srcdir)/m4/intlmacosx.m4 \ - $(top_srcdir)/m4/lib-ld.m4 $(top_srcdir)/m4/lib-link.m4 \ - $(top_srcdir)/m4/lib-prefix.m4 $(top_srcdir)/m4/nls.m4 \ - $(top_srcdir)/m4/po.m4 $(top_srcdir)/m4/progtest.m4 \ - $(top_srcdir)/acinclude.m4 $(top_srcdir)/configure.ac -am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ - $(ACLOCAL_M4) -DIST_COMMON = $(srcdir)/Makefile.am $(am__DIST_COMMON) -mkinstalldirs = $(install_sh) -d -CONFIG_CLEAN_FILES = -CONFIG_CLEAN_VPATH_FILES = -AM_V_P = $(am__v_P_$(V)) -am__v_P_ = $(am__v_P_$(AM_DEFAULT_VERBOSITY)) -am__v_P_0 = false -am__v_P_1 = : -AM_V_GEN = $(am__v_GEN_$(V)) -am__v_GEN_ = $(am__v_GEN_$(AM_DEFAULT_VERBOSITY)) -am__v_GEN_0 = @echo " GEN " $@; -am__v_GEN_1 = -AM_V_at = $(am__v_at_$(V)) -am__v_at_ = $(am__v_at_$(AM_DEFAULT_VERBOSITY)) -am__v_at_0 = @ -am__v_at_1 = -SOURCES = -DIST_SOURCES = -RECURSIVE_TARGETS = all-recursive check-recursive cscopelist-recursive \ - ctags-recursive dvi-recursive html-recursive info-recursive \ - install-data-recursive install-dvi-recursive \ - install-exec-recursive install-html-recursive \ - install-info-recursive install-pdf-recursive \ - install-ps-recursive install-recursive installcheck-recursive \ - installdirs-recursive pdf-recursive ps-recursive \ - tags-recursive uninstall-recursive -am__can_run_installinfo = \ - case $$AM_UPDATE_INFO_DIR in \ - n|no|NO) false;; \ - *) (install-info --version) >/dev/null 2>&1;; \ - esac -RECURSIVE_CLEAN_TARGETS = mostlyclean-recursive clean-recursive \ - distclean-recursive maintainer-clean-recursive -am__recursive_targets = \ - $(RECURSIVE_TARGETS) \ - $(RECURSIVE_CLEAN_TARGETS) \ - $(am__extra_recursive_targets) -AM_RECURSIVE_TARGETS = $(am__recursive_targets:-recursive=) TAGS CTAGS \ - distdir distdir-am -am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP) -# Read a list of newline-separated strings from the standard input, -# and print each of them once, without duplicates. Input order is -# *not* preserved. -am__uniquify_input = $(AWK) '\ - BEGIN { nonempty = 0; } \ - { items[$$0] = 1; nonempty = 1; } \ - END { if (nonempty) { for (i in items) print i; }; } \ -' -# Make sure the list of sources is unique. This is necessary because, -# e.g., the same source file might be shared among _SOURCES variables -# for different programs/libraries. -am__define_uniq_tagged_files = \ - list='$(am__tagged_files)'; \ - unique=`for i in $$list; do \ - if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \ - done | $(am__uniquify_input)` -DIST_SUBDIRS = $(SUBDIRS) -am__DIST_COMMON = $(srcdir)/Makefile.in -DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) -am__relativize = \ - dir0=`pwd`; \ - sed_first='s,^\([^/]*\)/.*$$,\1,'; \ - sed_rest='s,^[^/]*/*,,'; \ - sed_last='s,^.*/\([^/]*\)$$,\1,'; \ - sed_butlast='s,/*[^/]*$$,,'; \ - while test -n "$$dir1"; do \ - first=`echo "$$dir1" | sed -e "$$sed_first"`; \ - if test "$$first" != "."; then \ - if test "$$first" = ".."; then \ - dir2=`echo "$$dir0" | sed -e "$$sed_last"`/"$$dir2"; \ - dir0=`echo "$$dir0" | sed -e "$$sed_butlast"`; \ - else \ - first2=`echo "$$dir2" | sed -e "$$sed_first"`; \ - if test "$$first2" = "$$first"; then \ - dir2=`echo "$$dir2" | sed -e "$$sed_rest"`; \ - else \ - dir2="../$$dir2"; \ - fi; \ - dir0="$$dir0"/"$$first"; \ - fi; \ - fi; \ - dir1=`echo "$$dir1" | sed -e "$$sed_rest"`; \ - done; \ - reldir="$$dir2" -ACLOCAL = ${SHELL} '/home/storm/devel/cthulhu/missing' aclocal-1.18 -AMTAR = $${TAR-tar} -AM_DEFAULT_VERBOSITY = 1 -ATKBRIDGE_CFLAGS = -I/usr/include/at-spi2-atk/2.0 -I/usr/include/at-spi-2.0 -I/usr/include/libmount -I/usr/include/blkid -I/usr/include/atk-1.0 -I/usr/include/dbus-1.0 -I/usr/lib/dbus-1.0/include -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -I/usr/include/sysprof-6 -pthread -ATKBRIDGE_LIBS = -latk-bridge-2.0 -ATSPI2_CFLAGS = -I/usr/include/at-spi-2.0 -I/usr/include/dbus-1.0 -I/usr/lib/dbus-1.0/include -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -I/usr/include/libmount -I/usr/include/blkid -I/usr/include/sysprof-6 -pthread -ATSPI2_LIBS = -latspi -ldbus-1 -lglib-2.0 -AUTOCONF = ${SHELL} '/home/storm/devel/cthulhu/missing' autoconf -AUTOHEADER = ${SHELL} '/home/storm/devel/cthulhu/missing' autoheader -AUTOMAKE = ${SHELL} '/home/storm/devel/cthulhu/missing' automake-1.18 -AWK = gawk -CC = gcc -CCDEPMODE = depmode=none -CFLAGS = -g -O2 -CPP = gcc -E -CPPFLAGS = -CSCOPE = cscope -CTAGS = ctags -CYGPATH_W = echo -DEFS = -DPACKAGE_NAME=\"cthulhu\" -DPACKAGE_TARNAME=\"cthulhu\" -DPACKAGE_VERSION=\"2025.08.06\" -DPACKAGE_STRING=\"cthulhu\ 2025.08.06\" -DPACKAGE_BUGREPORT=\"https://gitlab.gnome.org/GNOME/cthulhu/-/issues/\" -DPACKAGE_URL=\"\" -DPACKAGE=\"cthulhu\" -DVERSION=\"2025.08.06\" -DENABLE_NLS=1 -DHAVE_GETTEXT=1 -DHAVE_DCGETTEXT=1 -DGETTEXT_PACKAGE=\"cthulhu\" -DEPDIR = .deps -DESIRED_LINGUAS = $(ALL_LINGUAS) -ECHO_C = -ECHO_N = -n -ECHO_T = -ETAGS = etags -EXEEXT = -GETTEXT_MACRO_VERSION = 0.24 -GETTEXT_PACKAGE = cthulhu -GMSGFMT = /usr/bin/msgfmt -GMSGFMT_015 = /usr/bin/msgfmt -GSTREAMER_CFLAGS = -I/usr/include/gstreamer-1.0 -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -I/usr/include/sysprof-6 -pthread -GSTREAMER_LIBS = -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -INSTALL = /usr/bin/install -c -INSTALL_DATA = ${INSTALL} -m 644 -INSTALL_PROGRAM = ${INSTALL} -INSTALL_SCRIPT = ${INSTALL} -INSTALL_STRIP_PROGRAM = $(install_sh) -c -s -INTLLIBS = -INTL_MACOSX_LIBS = -LDFLAGS = -LIBICONV = -liconv -LIBINTL = -LIBOBJS = -LIBPEAS_CFLAGS = -I/usr/include/libpeas-1.0 -I/usr/include/libmount -I/usr/include/blkid -I/usr/include/gobject-introspection-1.0 -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -I/usr/include/sysprof-6 -pthread -LIBPEAS_LIBS = -lpeas-1.0 -Wl,--export-dynamic -lgio-2.0 -lgmodule-2.0 -pthread -lgirepository-1.0 -lgobject-2.0 -lglib-2.0 -LIBS = -LOUIS_TABLE_DIR = /usr/share/liblouis/tables -LTLIBICONV = -liconv -LTLIBINTL = -LTLIBOBJS = -MAINT = -MAKEINFO = ${SHELL} '/home/storm/devel/cthulhu/missing' makeinfo -MKDIR_P = /usr/bin/mkdir -p -MSGFMT = /usr/bin/msgfmt -MSGMERGE = /usr/bin/msgmerge -MSGMERGE_FOR_MSGFMT_OPTION = --for-msgfmt -OBJEXT = o -PACKAGE = cthulhu -PACKAGE_BUGREPORT = https://gitlab.gnome.org/GNOME/cthulhu/-/issues/ -PACKAGE_NAME = cthulhu -PACKAGE_STRING = cthulhu 2025.08.06 -PACKAGE_TARNAME = cthulhu -PACKAGE_URL = -PACKAGE_VERSION = 2025.08.06 -PATH_SEPARATOR = : -PKG_CONFIG = /usr/bin/pkg-config -PKG_CONFIG_LIBDIR = -PKG_CONFIG_PATH = -PLATFORM_PATH = :/usr/bin:/usr/sbin:/bin -POSUB = po -PYGOBJECT_CFLAGS = -I/usr/include/pygobject-3.0 -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -I/usr/include/sysprof-6 -pthread -PYGOBJECT_LIBS = -lgobject-2.0 -lglib-2.0 -PYTHON = /home/storm/.pyenv/shims/python -PYTHON_EXEC_PREFIX = ${exec_prefix} -PYTHON_PLATFORM = linux -PYTHON_PREFIX = ${prefix} -PYTHON_VERSION = 3.13 -REVISION = 89df899 -SED = /usr/bin/sed -SET_MAKE = -SHELL = /bin/sh -STRIP = -USE_NLS = yes -VERSION = 2025.08.06 -XGETTEXT = /usr/bin/xgettext -XGETTEXT_015 = /usr/bin/xgettext -XGETTEXT_EXTRA_OPTIONS = -abs_builddir = /home/storm/devel/cthulhu/docs -abs_srcdir = /home/storm/devel/cthulhu/docs -abs_top_builddir = /home/storm/devel/cthulhu -abs_top_srcdir = /home/storm/devel/cthulhu -ac_ct_CC = gcc -am__include = include -am__leading_dot = . -am__quote = -am__rm_f_notfound = -am__tar = tar --format=ustar -chf - "$$tardir" -am__untar = tar -xf - -am__xargs_n = xargs -n -bindir = ${exec_prefix}/bin -build = x86_64-pc-linux-gnu -build_alias = -build_cpu = x86_64 -build_os = linux-gnu -build_vendor = pc -builddir = . -datadir = ${datarootdir} -datarootdir = ${prefix}/share -docdir = ${datarootdir}/doc/${PACKAGE_TARNAME} -dvidir = ${docdir} -exec_prefix = ${prefix} -host = x86_64-pc-linux-gnu -host_alias = -host_cpu = x86_64 -host_os = linux-gnu -host_vendor = pc -htmldir = ${docdir} -includedir = ${prefix}/include -infodir = ${datarootdir}/info -install_sh = ${SHELL} /home/storm/devel/cthulhu/install-sh -libdir = ${exec_prefix}/lib -libexecdir = ${exec_prefix}/libexec -localedir = ${datarootdir}/locale -localedir_c = "/home/storm/.local/share/locale" -localedir_c_make = \"$(localedir)\" -localstatedir = /home/storm/.local/var -mandir = ${datarootdir}/man -mkdir_p = $(MKDIR_P) -oldincludedir = /usr/include -pdfdir = ${docdir} -pkgpyexecdir = ${pyexecdir}/cthulhu -pkgpythondir = ${pythondir}/cthulhu -prefix = /home/storm/.local -program_transform_name = s,x,x, -psdir = ${docdir} -pyexecdir = ${PYTHON_EXEC_PREFIX}/lib/python3.13/site-packages -pythondir = ${PYTHON_PREFIX}/lib/python3.13/site-packages -runstatedir = ${localstatedir}/run -sbindir = ${exec_prefix}/sbin -sharedstatedir = ${prefix}/com -srcdir = . -sysconfdir = /home/storm/.local/etc -target_alias = -top_build_prefix = ../ -top_builddir = .. -top_srcdir = .. -SUBDIRS = man -all: all-recursive - -.SUFFIXES: -$(srcdir)/Makefile.in: $(srcdir)/Makefile.am $(am__configure_deps) - @for dep in $?; do \ - case '$(am__configure_deps)' in \ - *$$dep*) \ - ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \ - && { if test -f $@; then exit 0; else break; fi; }; \ - exit 1;; \ - esac; \ - done; \ - echo ' cd $(top_srcdir) && $(AUTOMAKE) --gnu docs/Makefile'; \ - $(am__cd) $(top_srcdir) && \ - $(AUTOMAKE) --gnu docs/Makefile -Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status - @case '$?' in \ - *config.status*) \ - cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \ - *) \ - echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \ - cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \ - esac; - -$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES) - cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh - -$(top_srcdir)/configure: $(am__configure_deps) - cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh -$(ACLOCAL_M4): $(am__aclocal_m4_deps) - cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh -$(am__aclocal_m4_deps): - -# This directory's subdirectories are mostly independent; you can cd -# into them and run 'make' without going through this Makefile. -# To change the values of 'make' variables: instead of editing Makefiles, -# (1) if the variable is set in 'config.status', edit 'config.status' -# (which will cause the Makefiles to be regenerated when you run 'make'); -# (2) otherwise, pass the desired values on the 'make' command line. -$(am__recursive_targets): - @fail=; \ - if $(am__make_keepgoing); then \ - failcom='fail=yes'; \ - else \ - failcom='exit 1'; \ - fi; \ - dot_seen=no; \ - target=`echo $@ | sed s/-recursive//`; \ - case "$@" in \ - distclean-* | maintainer-clean-*) list='$(DIST_SUBDIRS)' ;; \ - *) list='$(SUBDIRS)' ;; \ - esac; \ - for subdir in $$list; do \ - echo "Making $$target in $$subdir"; \ - if test "$$subdir" = "."; then \ - dot_seen=yes; \ - local_target="$$target-am"; \ - else \ - local_target="$$target"; \ - fi; \ - ($(am__cd) $$subdir && $(MAKE) $(AM_MAKEFLAGS) $$local_target) \ - || eval $$failcom; \ - done; \ - if test "$$dot_seen" = "no"; then \ - $(MAKE) $(AM_MAKEFLAGS) "$$target-am" || exit 1; \ - fi; test -z "$$fail" - -ID: $(am__tagged_files) - $(am__define_uniq_tagged_files); mkid -fID $$unique -tags: tags-recursive -TAGS: tags - -tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files) - set x; \ - here=`pwd`; \ - if ($(ETAGS) --etags-include --version) >/dev/null 2>&1; then \ - include_option=--etags-include; \ - empty_fix=.; \ - else \ - include_option=--include; \ - empty_fix=; \ - fi; \ - list='$(SUBDIRS)'; for subdir in $$list; do \ - if test "$$subdir" = .; then :; else \ - test ! -f $$subdir/TAGS || \ - set "$$@" "$$include_option=$$here/$$subdir/TAGS"; \ - fi; \ - done; \ - $(am__define_uniq_tagged_files); \ - shift; \ - if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \ - test -n "$$unique" || unique=$$empty_fix; \ - if test $$# -gt 0; then \ - $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \ - "$$@" $$unique; \ - else \ - $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \ - $$unique; \ - fi; \ - fi -ctags: ctags-recursive - -CTAGS: ctags -ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files) - $(am__define_uniq_tagged_files); \ - test -z "$(CTAGS_ARGS)$$unique" \ - || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \ - $$unique - -GTAGS: - here=`$(am__cd) $(top_builddir) && pwd` \ - && $(am__cd) $(top_srcdir) \ - && gtags -i $(GTAGS_ARGS) "$$here" -cscopelist: cscopelist-recursive - -cscopelist-am: $(am__tagged_files) - list='$(am__tagged_files)'; \ - case "$(srcdir)" in \ - [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \ - *) sdir=$(subdir)/$(srcdir) ;; \ - esac; \ - for i in $$list; do \ - if test -f "$$i"; then \ - echo "$(subdir)/$$i"; \ - else \ - echo "$$sdir/$$i"; \ - fi; \ - done >> $(top_builddir)/cscope.files - -distclean-tags: - -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags - -distdir: $(BUILT_SOURCES) - $(MAKE) $(AM_MAKEFLAGS) distdir-am - -distdir-am: $(DISTFILES) - @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ - topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ - list='$(DISTFILES)'; \ - dist_files=`for file in $$list; do echo $$file; done | \ - sed -e "s|^$$srcdirstrip/||;t" \ - -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \ - case $$dist_files in \ - */*) $(MKDIR_P) `echo "$$dist_files" | \ - sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \ - sort -u` ;; \ - esac; \ - for file in $$dist_files; do \ - if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \ - if test -d $$d/$$file; then \ - dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \ - if test -d "$(distdir)/$$file"; then \ - find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ - fi; \ - if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \ - cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \ - find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ - fi; \ - cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \ - else \ - test -f "$(distdir)/$$file" \ - || cp -p $$d/$$file "$(distdir)/$$file" \ - || exit 1; \ - fi; \ - done - @list='$(DIST_SUBDIRS)'; for subdir in $$list; do \ - if test "$$subdir" = .; then :; else \ - $(am__make_dryrun) \ - || test -d "$(distdir)/$$subdir" \ - || $(MKDIR_P) "$(distdir)/$$subdir" \ - || exit 1; \ - dir1=$$subdir; dir2="$(distdir)/$$subdir"; \ - $(am__relativize); \ - new_distdir=$$reldir; \ - dir1=$$subdir; dir2="$(top_distdir)"; \ - $(am__relativize); \ - new_top_distdir=$$reldir; \ - echo " (cd $$subdir && $(MAKE) $(AM_MAKEFLAGS) top_distdir="$$new_top_distdir" distdir="$$new_distdir" \\"; \ - echo " am__remove_distdir=: am__skip_length_check=: am__skip_mode_fix=: distdir)"; \ - ($(am__cd) $$subdir && \ - $(MAKE) $(AM_MAKEFLAGS) \ - top_distdir="$$new_top_distdir" \ - distdir="$$new_distdir" \ - am__remove_distdir=: \ - am__skip_length_check=: \ - am__skip_mode_fix=: \ - distdir) \ - || exit 1; \ - fi; \ - done -check-am: all-am -check: check-recursive -all-am: Makefile -installdirs: installdirs-recursive -installdirs-am: -install: install-recursive -install-exec: install-exec-recursive -install-data: install-data-recursive -uninstall: uninstall-recursive - -install-am: all-am - @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am - -installcheck: installcheck-recursive -install-strip: - if test -z '$(STRIP)'; then \ - $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ - install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ - install; \ - else \ - $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ - install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ - "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \ - fi -mostlyclean-generic: - -clean-generic: - -distclean-generic: - -$(am__rm_f) $(CONFIG_CLEAN_FILES) - -test . = "$(srcdir)" || $(am__rm_f) $(CONFIG_CLEAN_VPATH_FILES) - -maintainer-clean-generic: - @echo "This command is intended for maintainers to use" - @echo "it deletes files that may require special tools to rebuild." -clean: clean-recursive - -clean-am: clean-generic mostlyclean-am - -distclean: distclean-recursive - -rm -f Makefile -distclean-am: clean-am distclean-generic distclean-tags - -dvi: dvi-recursive - -dvi-am: - -html: html-recursive - -html-am: - -info: info-recursive - -info-am: - -install-data-am: - -install-dvi: install-dvi-recursive - -install-dvi-am: - -install-exec-am: - -install-html: install-html-recursive - -install-html-am: - -install-info: install-info-recursive - -install-info-am: - -install-man: - -install-pdf: install-pdf-recursive - -install-pdf-am: - -install-ps: install-ps-recursive - -install-ps-am: - -installcheck-am: - -maintainer-clean: maintainer-clean-recursive - -rm -f Makefile -maintainer-clean-am: distclean-am maintainer-clean-generic - -mostlyclean: mostlyclean-recursive - -mostlyclean-am: mostlyclean-generic - -pdf: pdf-recursive - -pdf-am: - -ps: ps-recursive - -ps-am: - -uninstall-am: - -.MAKE: $(am__recursive_targets) install-am install-strip - -.PHONY: $(am__recursive_targets) CTAGS GTAGS TAGS all all-am check \ - check-am clean clean-generic cscopelist-am ctags ctags-am \ - distclean distclean-generic distclean-tags distdir dvi dvi-am \ - html html-am info info-am install install-am install-data \ - install-data-am install-dvi install-dvi-am install-exec \ - install-exec-am install-html install-html-am install-info \ - install-info-am install-man install-pdf install-pdf-am \ - install-ps install-ps-am install-strip installcheck \ - installcheck-am installdirs installdirs-am maintainer-clean \ - maintainer-clean-generic mostlyclean mostlyclean-generic pdf \ - pdf-am ps ps-am tags tags-am uninstall uninstall-am - -.PRECIOUS: Makefile - - -# Tell versions [3.59,3.63) of GNU make to not export all variables. -# Otherwise a system limit (for SysV at least) may be exceeded. -.NOEXPORT: - -# Tell GNU make to disable its built-in pattern rules. -%:: %,v -%:: RCS/%,v -%:: RCS/% -%:: s.% -%:: SCCS/s.% diff --git a/docs/man/Makefile b/docs/man/Makefile deleted file mode 100644 index ee06b9f..0000000 --- a/docs/man/Makefile +++ /dev/null @@ -1,554 +0,0 @@ -# Makefile.in generated by automake 1.18.1 from Makefile.am. -# docs/man/Makefile. Generated from Makefile.in by configure. - -# Copyright (C) 1994-2025 Free Software Foundation, Inc. - -# This Makefile.in is free software; the Free Software Foundation -# gives unlimited permission to copy and/or distribute it, -# with or without modifications, as long as this notice is preserved. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY, to the extent permitted by law; without -# even the implied warranty of MERCHANTABILITY or FITNESS FOR A -# PARTICULAR PURPOSE. - - - -am__is_gnu_make = { \ - if test -z '$(MAKELEVEL)'; then \ - false; \ - elif test -n '$(MAKE_HOST)'; then \ - true; \ - elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \ - true; \ - else \ - false; \ - fi; \ -} -am__make_running_with_option = \ - case $${target_option-} in \ - ?) ;; \ - *) echo "am__make_running_with_option: internal error: invalid" \ - "target option '$${target_option-}' specified" >&2; \ - exit 1;; \ - esac; \ - has_opt=no; \ - sane_makeflags=$$MAKEFLAGS; \ - if $(am__is_gnu_make); then \ - sane_makeflags=$$MFLAGS; \ - else \ - case $$MAKEFLAGS in \ - *\\[\ \ ]*) \ - bs=\\; \ - sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \ - | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \ - esac; \ - fi; \ - skip_next=no; \ - strip_trailopt () \ - { \ - flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \ - }; \ - for flg in $$sane_makeflags; do \ - test $$skip_next = yes && { skip_next=no; continue; }; \ - case $$flg in \ - *=*|--*) continue;; \ - -*I) strip_trailopt 'I'; skip_next=yes;; \ - -*I?*) strip_trailopt 'I';; \ - -*O) strip_trailopt 'O'; skip_next=yes;; \ - -*O?*) strip_trailopt 'O';; \ - -*l) strip_trailopt 'l'; skip_next=yes;; \ - -*l?*) strip_trailopt 'l';; \ - -[dEDm]) skip_next=yes;; \ - -[JT]) skip_next=yes;; \ - esac; \ - case $$flg in \ - *$$target_option*) has_opt=yes; break;; \ - esac; \ - done; \ - test $$has_opt = yes -am__make_dryrun = (target_option=n; $(am__make_running_with_option)) -am__make_keepgoing = (target_option=k; $(am__make_running_with_option)) -am__rm_f = rm -f $(am__rm_f_notfound) -am__rm_rf = rm -rf $(am__rm_f_notfound) -pkgdatadir = $(datadir)/cthulhu -pkgincludedir = $(includedir)/cthulhu -pkglibdir = $(libdir)/cthulhu -pkglibexecdir = $(libexecdir)/cthulhu -am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd -install_sh_DATA = $(install_sh) -c -m 644 -install_sh_PROGRAM = $(install_sh) -c -install_sh_SCRIPT = $(install_sh) -c -INSTALL_HEADER = $(INSTALL_DATA) -transform = $(program_transform_name) -NORMAL_INSTALL = : -PRE_INSTALL = : -POST_INSTALL = : -NORMAL_UNINSTALL = : -PRE_UNINSTALL = : -POST_UNINSTALL = : -build_triplet = x86_64-pc-linux-gnu -host_triplet = x86_64-pc-linux-gnu -subdir = docs/man -ACLOCAL_M4 = $(top_srcdir)/aclocal.m4 -am__aclocal_m4_deps = $(top_srcdir)/m4/build-to-host.m4 \ - $(top_srcdir)/m4/gettext.m4 $(top_srcdir)/m4/host-cpu-c-abi.m4 \ - $(top_srcdir)/m4/iconv.m4 $(top_srcdir)/m4/intlmacosx.m4 \ - $(top_srcdir)/m4/lib-ld.m4 $(top_srcdir)/m4/lib-link.m4 \ - $(top_srcdir)/m4/lib-prefix.m4 $(top_srcdir)/m4/nls.m4 \ - $(top_srcdir)/m4/po.m4 $(top_srcdir)/m4/progtest.m4 \ - $(top_srcdir)/acinclude.m4 $(top_srcdir)/configure.ac -am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ - $(ACLOCAL_M4) -DIST_COMMON = $(srcdir)/Makefile.am $(am__DIST_COMMON) -mkinstalldirs = $(install_sh) -d -CONFIG_CLEAN_FILES = -CONFIG_CLEAN_VPATH_FILES = -AM_V_P = $(am__v_P_$(V)) -am__v_P_ = $(am__v_P_$(AM_DEFAULT_VERBOSITY)) -am__v_P_0 = false -am__v_P_1 = : -AM_V_GEN = $(am__v_GEN_$(V)) -am__v_GEN_ = $(am__v_GEN_$(AM_DEFAULT_VERBOSITY)) -am__v_GEN_0 = @echo " GEN " $@; -am__v_GEN_1 = -AM_V_at = $(am__v_at_$(V)) -am__v_at_ = $(am__v_at_$(AM_DEFAULT_VERBOSITY)) -am__v_at_0 = @ -am__v_at_1 = -SOURCES = -DIST_SOURCES = -am__can_run_installinfo = \ - case $$AM_UPDATE_INFO_DIR in \ - n|no|NO) false;; \ - *) (install-info --version) >/dev/null 2>&1;; \ - esac -am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`; -am__vpath_adj = case $$p in \ - $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \ - *) f=$$p;; \ - esac; -am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`; -am__install_max = 40 -am__nobase_strip_setup = \ - srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'` -am__nobase_strip = \ - for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||" -am__nobase_list = $(am__nobase_strip_setup); \ - for p in $$list; do echo "$$p $$p"; done | \ - sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \ - $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \ - if (++n[$$2] == $(am__install_max)) \ - { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \ - END { for (dir in files) print dir, files[dir] }' -am__base_list = \ - sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \ - sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g' -am__uninstall_files_from_dir = { \ - { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \ - || { echo " ( cd '$$dir' && rm -f" $$files ")"; \ - $(am__cd) "$$dir" && echo $$files | $(am__xargs_n) 40 $(am__rm_f); }; \ - } -man1dir = $(mandir)/man1 -am__installdirs = "$(DESTDIR)$(man1dir)" -NROFF = nroff -MANS = $(man1_MANS) -am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP) -am__DIST_COMMON = $(srcdir)/Makefile.in -DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) -ACLOCAL = ${SHELL} '/home/storm/devel/cthulhu/missing' aclocal-1.18 -AMTAR = $${TAR-tar} -AM_DEFAULT_VERBOSITY = 1 -ATKBRIDGE_CFLAGS = -I/usr/include/at-spi2-atk/2.0 -I/usr/include/at-spi-2.0 -I/usr/include/libmount -I/usr/include/blkid -I/usr/include/atk-1.0 -I/usr/include/dbus-1.0 -I/usr/lib/dbus-1.0/include -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -I/usr/include/sysprof-6 -pthread -ATKBRIDGE_LIBS = -latk-bridge-2.0 -ATSPI2_CFLAGS = -I/usr/include/at-spi-2.0 -I/usr/include/dbus-1.0 -I/usr/lib/dbus-1.0/include -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -I/usr/include/libmount -I/usr/include/blkid -I/usr/include/sysprof-6 -pthread -ATSPI2_LIBS = -latspi -ldbus-1 -lglib-2.0 -AUTOCONF = ${SHELL} '/home/storm/devel/cthulhu/missing' autoconf -AUTOHEADER = ${SHELL} '/home/storm/devel/cthulhu/missing' autoheader -AUTOMAKE = ${SHELL} '/home/storm/devel/cthulhu/missing' automake-1.18 -AWK = gawk -CC = gcc -CCDEPMODE = depmode=none -CFLAGS = -g -O2 -CPP = gcc -E -CPPFLAGS = -CSCOPE = cscope -CTAGS = ctags -CYGPATH_W = echo -DEFS = -DPACKAGE_NAME=\"cthulhu\" -DPACKAGE_TARNAME=\"cthulhu\" -DPACKAGE_VERSION=\"2025.08.06\" -DPACKAGE_STRING=\"cthulhu\ 2025.08.06\" -DPACKAGE_BUGREPORT=\"https://gitlab.gnome.org/GNOME/cthulhu/-/issues/\" -DPACKAGE_URL=\"\" -DPACKAGE=\"cthulhu\" -DVERSION=\"2025.08.06\" -DENABLE_NLS=1 -DHAVE_GETTEXT=1 -DHAVE_DCGETTEXT=1 -DGETTEXT_PACKAGE=\"cthulhu\" -DEPDIR = .deps -DESIRED_LINGUAS = $(ALL_LINGUAS) -ECHO_C = -ECHO_N = -n -ECHO_T = -ETAGS = etags -EXEEXT = -GETTEXT_MACRO_VERSION = 0.24 -GETTEXT_PACKAGE = cthulhu -GMSGFMT = /usr/bin/msgfmt -GMSGFMT_015 = /usr/bin/msgfmt -GSTREAMER_CFLAGS = -I/usr/include/gstreamer-1.0 -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -I/usr/include/sysprof-6 -pthread -GSTREAMER_LIBS = -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -INSTALL = /usr/bin/install -c -INSTALL_DATA = ${INSTALL} -m 644 -INSTALL_PROGRAM = ${INSTALL} -INSTALL_SCRIPT = ${INSTALL} -INSTALL_STRIP_PROGRAM = $(install_sh) -c -s -INTLLIBS = -INTL_MACOSX_LIBS = -LDFLAGS = -LIBICONV = -liconv -LIBINTL = -LIBOBJS = -LIBPEAS_CFLAGS = -I/usr/include/libpeas-1.0 -I/usr/include/libmount -I/usr/include/blkid -I/usr/include/gobject-introspection-1.0 -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -I/usr/include/sysprof-6 -pthread -LIBPEAS_LIBS = -lpeas-1.0 -Wl,--export-dynamic -lgio-2.0 -lgmodule-2.0 -pthread -lgirepository-1.0 -lgobject-2.0 -lglib-2.0 -LIBS = -LOUIS_TABLE_DIR = /usr/share/liblouis/tables -LTLIBICONV = -liconv -LTLIBINTL = -LTLIBOBJS = -MAINT = -MAKEINFO = ${SHELL} '/home/storm/devel/cthulhu/missing' makeinfo -MKDIR_P = /usr/bin/mkdir -p -MSGFMT = /usr/bin/msgfmt -MSGMERGE = /usr/bin/msgmerge -MSGMERGE_FOR_MSGFMT_OPTION = --for-msgfmt -OBJEXT = o -PACKAGE = cthulhu -PACKAGE_BUGREPORT = https://gitlab.gnome.org/GNOME/cthulhu/-/issues/ -PACKAGE_NAME = cthulhu -PACKAGE_STRING = cthulhu 2025.08.06 -PACKAGE_TARNAME = cthulhu -PACKAGE_URL = -PACKAGE_VERSION = 2025.08.06 -PATH_SEPARATOR = : -PKG_CONFIG = /usr/bin/pkg-config -PKG_CONFIG_LIBDIR = -PKG_CONFIG_PATH = -PLATFORM_PATH = :/usr/bin:/usr/sbin:/bin -POSUB = po -PYGOBJECT_CFLAGS = -I/usr/include/pygobject-3.0 -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -I/usr/include/sysprof-6 -pthread -PYGOBJECT_LIBS = -lgobject-2.0 -lglib-2.0 -PYTHON = /home/storm/.pyenv/shims/python -PYTHON_EXEC_PREFIX = ${exec_prefix} -PYTHON_PLATFORM = linux -PYTHON_PREFIX = ${prefix} -PYTHON_VERSION = 3.13 -REVISION = 89df899 -SED = /usr/bin/sed -SET_MAKE = -SHELL = /bin/sh -STRIP = -USE_NLS = yes -VERSION = 2025.08.06 -XGETTEXT = /usr/bin/xgettext -XGETTEXT_015 = /usr/bin/xgettext -XGETTEXT_EXTRA_OPTIONS = -abs_builddir = /home/storm/devel/cthulhu/docs/man -abs_srcdir = /home/storm/devel/cthulhu/docs/man -abs_top_builddir = /home/storm/devel/cthulhu -abs_top_srcdir = /home/storm/devel/cthulhu -ac_ct_CC = gcc -am__include = include -am__leading_dot = . -am__quote = -am__rm_f_notfound = -am__tar = tar --format=ustar -chf - "$$tardir" -am__untar = tar -xf - -am__xargs_n = xargs -n -bindir = ${exec_prefix}/bin -build = x86_64-pc-linux-gnu -build_alias = -build_cpu = x86_64 -build_os = linux-gnu -build_vendor = pc -builddir = . -datadir = ${datarootdir} -datarootdir = ${prefix}/share -docdir = ${datarootdir}/doc/${PACKAGE_TARNAME} -dvidir = ${docdir} -exec_prefix = ${prefix} -host = x86_64-pc-linux-gnu -host_alias = -host_cpu = x86_64 -host_os = linux-gnu -host_vendor = pc -htmldir = ${docdir} -includedir = ${prefix}/include -infodir = ${datarootdir}/info -install_sh = ${SHELL} /home/storm/devel/cthulhu/install-sh -libdir = ${exec_prefix}/lib -libexecdir = ${exec_prefix}/libexec -localedir = ${datarootdir}/locale -localedir_c = "/home/storm/.local/share/locale" -localedir_c_make = \"$(localedir)\" -localstatedir = /home/storm/.local/var -mandir = ${datarootdir}/man -mkdir_p = $(MKDIR_P) -oldincludedir = /usr/include -pdfdir = ${docdir} -pkgpyexecdir = ${pyexecdir}/cthulhu -pkgpythondir = ${pythondir}/cthulhu -prefix = /home/storm/.local -program_transform_name = s,x,x, -psdir = ${docdir} -pyexecdir = ${PYTHON_EXEC_PREFIX}/lib/python3.13/site-packages -pythondir = ${PYTHON_PREFIX}/lib/python3.13/site-packages -runstatedir = ${localstatedir}/run -sbindir = ${exec_prefix}/sbin -sharedstatedir = ${prefix}/com -srcdir = . -sysconfdir = /home/storm/.local/etc -target_alias = -top_build_prefix = ../../ -top_builddir = ../.. -top_srcdir = ../.. -man1_MANS = cthulhu.1 -EXTRA_DIST = \ - $(man1_MANS) - -all: all-am - -.SUFFIXES: -$(srcdir)/Makefile.in: $(srcdir)/Makefile.am $(am__configure_deps) - @for dep in $?; do \ - case '$(am__configure_deps)' in \ - *$$dep*) \ - ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \ - && { if test -f $@; then exit 0; else break; fi; }; \ - exit 1;; \ - esac; \ - done; \ - echo ' cd $(top_srcdir) && $(AUTOMAKE) --gnu docs/man/Makefile'; \ - $(am__cd) $(top_srcdir) && \ - $(AUTOMAKE) --gnu docs/man/Makefile -Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status - @case '$?' in \ - *config.status*) \ - cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \ - *) \ - echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \ - cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \ - esac; - -$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES) - cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh - -$(top_srcdir)/configure: $(am__configure_deps) - cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh -$(ACLOCAL_M4): $(am__aclocal_m4_deps) - cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh -$(am__aclocal_m4_deps): -install-man1: $(man1_MANS) - @$(NORMAL_INSTALL) - @list1='$(man1_MANS)'; \ - list2=''; \ - test -n "$(man1dir)" \ - && test -n "`echo $$list1$$list2`" \ - || exit 0; \ - echo " $(MKDIR_P) '$(DESTDIR)$(man1dir)'"; \ - $(MKDIR_P) "$(DESTDIR)$(man1dir)" || exit 1; \ - { for i in $$list1; do echo "$$i"; done; \ - if test -n "$$list2"; then \ - for i in $$list2; do echo "$$i"; done \ - | sed -n '/\.1[a-z]*$$/p'; \ - fi; \ - } | while read p; do \ - if test -f $$p; then d=; else d="$(srcdir)/"; fi; \ - echo "$$d$$p"; echo "$$p"; \ - done | \ - sed -e 'n;s,.*/,,;p;h;s,.*\.,,;s,^[^1][0-9a-z]*$$,1,;x' \ - -e 's,\.[0-9a-z]*$$,,;$(transform);G;s,\n,.,' | \ - sed 'N;N;s,\n, ,g' | { \ - list=; while read file base inst; do \ - if test "$$base" = "$$inst"; then list="$$list $$file"; else \ - echo " $(INSTALL_DATA) '$$file' '$(DESTDIR)$(man1dir)/$$inst'"; \ - $(INSTALL_DATA) "$$file" "$(DESTDIR)$(man1dir)/$$inst" || exit $$?; \ - fi; \ - done; \ - for i in $$list; do echo "$$i"; done | $(am__base_list) | \ - while read files; do \ - test -z "$$files" || { \ - echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(man1dir)'"; \ - $(INSTALL_DATA) $$files "$(DESTDIR)$(man1dir)" || exit $$?; }; \ - done; } - -uninstall-man1: - @$(NORMAL_UNINSTALL) - @list='$(man1_MANS)'; test -n "$(man1dir)" || exit 0; \ - files=`{ for i in $$list; do echo "$$i"; done; \ - } | sed -e 's,.*/,,;h;s,.*\.,,;s,^[^1][0-9a-z]*$$,1,;x' \ - -e 's,\.[0-9a-z]*$$,,;$(transform);G;s,\n,.,'`; \ - dir='$(DESTDIR)$(man1dir)'; $(am__uninstall_files_from_dir) -tags TAGS: - -ctags CTAGS: - -cscope cscopelist: - - -distdir: $(BUILT_SOURCES) - $(MAKE) $(AM_MAKEFLAGS) distdir-am - -distdir-am: $(DISTFILES) - @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ - topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ - list='$(DISTFILES)'; \ - dist_files=`for file in $$list; do echo $$file; done | \ - sed -e "s|^$$srcdirstrip/||;t" \ - -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \ - case $$dist_files in \ - */*) $(MKDIR_P) `echo "$$dist_files" | \ - sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \ - sort -u` ;; \ - esac; \ - for file in $$dist_files; do \ - if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \ - if test -d $$d/$$file; then \ - dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \ - if test -d "$(distdir)/$$file"; then \ - find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ - fi; \ - if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \ - cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \ - find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ - fi; \ - cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \ - else \ - test -f "$(distdir)/$$file" \ - || cp -p $$d/$$file "$(distdir)/$$file" \ - || exit 1; \ - fi; \ - done -check-am: all-am -check: check-am -all-am: Makefile $(MANS) -installdirs: - for dir in "$(DESTDIR)$(man1dir)"; do \ - test -z "$$dir" || $(MKDIR_P) "$$dir"; \ - done -install: install-am -install-exec: install-exec-am -install-data: install-data-am -uninstall: uninstall-am - -install-am: all-am - @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am - -installcheck: installcheck-am -install-strip: - if test -z '$(STRIP)'; then \ - $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ - install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ - install; \ - else \ - $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ - install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ - "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \ - fi -mostlyclean-generic: - -clean-generic: - -distclean-generic: - -$(am__rm_f) $(CONFIG_CLEAN_FILES) - -test . = "$(srcdir)" || $(am__rm_f) $(CONFIG_CLEAN_VPATH_FILES) - -maintainer-clean-generic: - @echo "This command is intended for maintainers to use" - @echo "it deletes files that may require special tools to rebuild." -clean: clean-am - -clean-am: clean-generic mostlyclean-am - -distclean: distclean-am - -rm -f Makefile -distclean-am: clean-am distclean-generic - -dvi: dvi-am - -dvi-am: - -html: html-am - -html-am: - -info: info-am - -info-am: - -install-data-am: install-man - -install-dvi: install-dvi-am - -install-dvi-am: - -install-exec-am: - -install-html: install-html-am - -install-html-am: - -install-info: install-info-am - -install-info-am: - -install-man: install-man1 - -install-pdf: install-pdf-am - -install-pdf-am: - -install-ps: install-ps-am - -install-ps-am: - -installcheck-am: - -maintainer-clean: maintainer-clean-am - -rm -f Makefile -maintainer-clean-am: distclean-am maintainer-clean-generic - -mostlyclean: mostlyclean-am - -mostlyclean-am: mostlyclean-generic - -pdf: pdf-am - -pdf-am: - -ps: ps-am - -ps-am: - -uninstall-am: uninstall-man - -uninstall-man: uninstall-man1 - -.MAKE: install-am install-strip - -.PHONY: all all-am check check-am clean clean-generic cscopelist-am \ - ctags-am distclean distclean-generic distdir dvi dvi-am html \ - html-am info info-am install install-am install-data \ - install-data-am install-dvi install-dvi-am install-exec \ - install-exec-am install-html install-html-am install-info \ - install-info-am install-man install-man1 install-pdf \ - install-pdf-am install-ps install-ps-am install-strip \ - installcheck installcheck-am installdirs maintainer-clean \ - maintainer-clean-generic mostlyclean mostlyclean-generic pdf \ - pdf-am ps ps-am tags-am uninstall uninstall-am uninstall-man \ - uninstall-man1 - -.PRECIOUS: Makefile - - -# Tell versions [3.59,3.63) of GNU make to not export all variables. -# Otherwise a system limit (for SysV at least) may be exceeded. -.NOEXPORT: - -# Tell GNU make to disable its built-in pattern rules. -%:: %,v -%:: RCS/%,v -%:: RCS/% -%:: s.% -%:: SCCS/s.% diff --git a/docs/man/cthulhu.1 b/docs/man/cthulhu.1 index 284681f..dc3fc48 100644 --- a/docs/man/cthulhu.1 +++ b/docs/man/cthulhu.1 @@ -331,4 +331,4 @@ mailing list To post a message to all .B cthulhu -list, send a email to cthulhu-list@gnome.org +list, send a email to https://groups.io/g/stormux diff --git a/m4/.gitignore b/m4/.gitignore deleted file mode 100644 index 680584b..0000000 --- a/m4/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -codeset.m4 -extern-inline.m4 -fcntl-o.m4 -gettext.m4 -glibc2.m4 -glibc21.m4 -iconv.m4 -intdiv0.m4 -intl.m4 -intldir.m4 -intlmacosx.m4 -intmax.m4 -inttypes-pri.m4 -inttypes_h.m4 -lcmessage.m4 -lib-ld.m4 -lib-link.m4 -lib-prefix.m4 -lock.m4 -longlong.m4 -nls.m4 -pkg.m4 -po.m4 -printf-posix.m4 -progtest.m4 -size_max.m4 -stdint_h.m4 -threadlib.m4 -uintmax_t.m4 -visibility.m4 -wchar_t.m4 -wint_t.m4 -xsize.m4 -yelp.m4 diff --git a/m4/build-to-host.m4 b/m4/build-to-host.m4 deleted file mode 100644 index 01bff8f..0000000 --- a/m4/build-to-host.m4 +++ /dev/null @@ -1,274 +0,0 @@ -# build-to-host.m4 -# serial 5 -dnl Copyright (C) 2023-2025 Free Software Foundation, Inc. -dnl This file is free software; the Free Software Foundation -dnl gives unlimited permission to copy and/or distribute it, -dnl with or without modifications, as long as this notice is preserved. -dnl This file is offered as-is, without any warranty. - -dnl Written by Bruno Haible. - -dnl When the build environment ($build_os) is different from the target runtime -dnl environment ($host_os), file names may need to be converted from the build -dnl environment syntax to the target runtime environment syntax. This is -dnl because the Makefiles are executed (mostly) by build environment tools and -dnl therefore expect file names in build environment syntax, whereas the runtime -dnl expects file names in target runtime environment syntax. -dnl -dnl For example, if $build_os = cygwin and $host_os = mingw32, filenames need -dnl be converted from Cygwin syntax to native Windows syntax: -dnl /cygdrive/c/foo/bar -> C:\foo\bar -dnl /usr/local/share -> C:\cygwin64\usr\local\share -dnl -dnl gl_BUILD_TO_HOST([somedir]) -dnl This macro takes as input an AC_SUBSTed variable 'somedir', which must -dnl already have its final value assigned, and produces two additional -dnl AC_SUBSTed variables 'somedir_c' and 'somedir_c_make', that designate the -dnl same file name value, just in different syntax: -dnl - somedir_c is the file name in target runtime environment syntax, -dnl as a C string (starting and ending with a double-quote, -dnl and with escaped backslashes and double-quotes in -dnl between). -dnl - somedir_c_make is the same thing, escaped for use in a Makefile. - -AC_DEFUN([gl_BUILD_TO_HOST], -[ - AC_REQUIRE([AC_CANONICAL_BUILD]) - AC_REQUIRE([AC_CANONICAL_HOST]) - AC_REQUIRE([gl_BUILD_TO_HOST_INIT]) - - dnl Define somedir_c. - gl_final_[$1]="$[$1]" - dnl Translate it from build syntax to host syntax. - case "$build_os" in - cygwin*) - case "$host_os" in - mingw* | windows*) - gl_final_[$1]=`cygpath -w "$gl_final_[$1]"` ;; - esac - ;; - esac - dnl Convert it to C string syntax. - [$1]_c=`printf '%s\n' "$gl_final_[$1]" | sed -e "$gl_sed_double_backslashes" -e "$gl_sed_escape_doublequotes" | tr -d "$gl_tr_cr"` - [$1]_c='"'"$[$1]_c"'"' - AC_SUBST([$1_c]) - - dnl Define somedir_c_make. - [$1]_c_make=`printf '%s\n' "$[$1]_c" | sed -e "$gl_sed_escape_for_make_1" -e "$gl_sed_escape_for_make_2" | tr -d "$gl_tr_cr"` - dnl Use the substituted somedir variable, when possible, so that the user - dnl may adjust somedir a posteriori when there are no special characters. - if test "$[$1]_c_make" = '\"'"${gl_final_[$1]}"'\"'; then - [$1]_c_make='\"$([$1])\"' - fi - AC_SUBST([$1_c_make]) -]) - -dnl Some initializations for gl_BUILD_TO_HOST. -AC_DEFUN([gl_BUILD_TO_HOST_INIT], -[ - gl_sed_double_backslashes='s/\\/\\\\/g' - gl_sed_escape_doublequotes='s/"/\\"/g' -changequote(,)dnl - gl_sed_escape_for_make_1="s,\\([ \"&'();<>\\\\\`|]\\),\\\\\\1,g" -changequote([,])dnl - gl_sed_escape_for_make_2='s,\$,\\$$,g' - dnl Find out how to remove carriage returns from output. Solaris /usr/ucb/tr - dnl does not understand '\r'. - case `echo r | tr -d '\r'` in - '') gl_tr_cr='\015' ;; - *) gl_tr_cr='\r' ;; - esac -]) - - -dnl The following macros are convenience invocations of gl_BUILD_TO_HOST -dnl for some of the variables that are defined by Autoconf. -dnl To do so for _all_ the possible variables, use the module 'configmake'. - -dnl Defines bindir_c and bindir_c_make. -AC_DEFUN_ONCE([gl_BUILD_TO_HOST_BINDIR], -[ - dnl Find the final value of bindir. - gl_saved_prefix="${prefix}" - gl_saved_exec_prefix="${exec_prefix}" - gl_saved_bindir="${bindir}" - dnl Unfortunately, prefix and exec_prefix get only finally determined - dnl at the end of configure. - if test "X$prefix" = "XNONE"; then - prefix="$ac_default_prefix" - fi - if test "X$exec_prefix" = "XNONE"; then - exec_prefix='${prefix}' - fi - eval exec_prefix="$exec_prefix" - eval bindir="$bindir" - gl_BUILD_TO_HOST([bindir]) - bindir="${gl_saved_bindir}" - exec_prefix="${gl_saved_exec_prefix}" - prefix="${gl_saved_prefix}" -]) - -dnl Defines datadir_c and datadir_c_make, -dnl where datadir = $(datarootdir) -AC_DEFUN_ONCE([gl_BUILD_TO_HOST_DATADIR], -[ - dnl Find the final value of datadir. - gl_saved_prefix="${prefix}" - gl_saved_datarootdir="${datarootdir}" - gl_saved_datadir="${datadir}" - dnl Unfortunately, prefix gets only finally determined at the end of - dnl configure. - if test "X$prefix" = "XNONE"; then - prefix="$ac_default_prefix" - fi - eval datarootdir="$datarootdir" - eval datadir="$datadir" - gl_BUILD_TO_HOST([datadir]) - datadir="${gl_saved_datadir}" - datarootdir="${gl_saved_datarootdir}" - prefix="${gl_saved_prefix}" -]) - -dnl Defines libdir_c and libdir_c_make. -AC_DEFUN_ONCE([gl_BUILD_TO_HOST_LIBDIR], -[ - dnl Find the final value of libdir. - gl_saved_prefix="${prefix}" - gl_saved_exec_prefix="${exec_prefix}" - gl_saved_libdir="${libdir}" - dnl Unfortunately, prefix and exec_prefix get only finally determined - dnl at the end of configure. - if test "X$prefix" = "XNONE"; then - prefix="$ac_default_prefix" - fi - if test "X$exec_prefix" = "XNONE"; then - exec_prefix='${prefix}' - fi - eval exec_prefix="$exec_prefix" - eval libdir="$libdir" - gl_BUILD_TO_HOST([libdir]) - libdir="${gl_saved_libdir}" - exec_prefix="${gl_saved_exec_prefix}" - prefix="${gl_saved_prefix}" -]) - -dnl Defines libexecdir_c and libexecdir_c_make. -AC_DEFUN_ONCE([gl_BUILD_TO_HOST_LIBEXECDIR], -[ - dnl Find the final value of libexecdir. - gl_saved_prefix="${prefix}" - gl_saved_exec_prefix="${exec_prefix}" - gl_saved_libexecdir="${libexecdir}" - dnl Unfortunately, prefix and exec_prefix get only finally determined - dnl at the end of configure. - if test "X$prefix" = "XNONE"; then - prefix="$ac_default_prefix" - fi - if test "X$exec_prefix" = "XNONE"; then - exec_prefix='${prefix}' - fi - eval exec_prefix="$exec_prefix" - eval libexecdir="$libexecdir" - gl_BUILD_TO_HOST([libexecdir]) - libexecdir="${gl_saved_libexecdir}" - exec_prefix="${gl_saved_exec_prefix}" - prefix="${gl_saved_prefix}" -]) - -dnl Defines localedir_c and localedir_c_make. -AC_DEFUN_ONCE([gl_BUILD_TO_HOST_LOCALEDIR], -[ - dnl Find the final value of localedir. - gl_saved_prefix="${prefix}" - gl_saved_datarootdir="${datarootdir}" - gl_saved_localedir="${localedir}" - dnl Unfortunately, prefix gets only finally determined at the end of - dnl configure. - if test "X$prefix" = "XNONE"; then - prefix="$ac_default_prefix" - fi - eval datarootdir="$datarootdir" - eval localedir="$localedir" - gl_BUILD_TO_HOST([localedir]) - localedir="${gl_saved_localedir}" - datarootdir="${gl_saved_datarootdir}" - prefix="${gl_saved_prefix}" -]) - -dnl Defines pkgdatadir_c and pkgdatadir_c_make, -dnl where pkgdatadir = $(datadir)/$(PACKAGE) -AC_DEFUN_ONCE([gl_BUILD_TO_HOST_PKGDATADIR], -[ - dnl Find the final value of pkgdatadir. - gl_saved_prefix="${prefix}" - gl_saved_datarootdir="${datarootdir}" - gl_saved_datadir="${datadir}" - gl_saved_pkgdatadir="${pkgdatadir}" - dnl Unfortunately, prefix gets only finally determined at the end of - dnl configure. - if test "X$prefix" = "XNONE"; then - prefix="$ac_default_prefix" - fi - eval datarootdir="$datarootdir" - eval datadir="$datadir" - eval pkgdatadir="$pkgdatadir" - gl_BUILD_TO_HOST([pkgdatadir]) - pkgdatadir="${gl_saved_pkgdatadir}" - datadir="${gl_saved_datadir}" - datarootdir="${gl_saved_datarootdir}" - prefix="${gl_saved_prefix}" -]) - -dnl Defines pkglibdir_c and pkglibdir_c_make, -dnl where pkglibdir = $(libdir)/$(PACKAGE) -AC_DEFUN_ONCE([gl_BUILD_TO_HOST_PKGLIBDIR], -[ - dnl Find the final value of pkglibdir. - gl_saved_prefix="${prefix}" - gl_saved_exec_prefix="${exec_prefix}" - gl_saved_libdir="${libdir}" - gl_saved_pkglibdir="${pkglibdir}" - dnl Unfortunately, prefix and exec_prefix get only finally determined - dnl at the end of configure. - if test "X$prefix" = "XNONE"; then - prefix="$ac_default_prefix" - fi - if test "X$exec_prefix" = "XNONE"; then - exec_prefix='${prefix}' - fi - eval exec_prefix="$exec_prefix" - eval libdir="$libdir" - eval pkglibdir="$pkglibdir" - gl_BUILD_TO_HOST([pkglibdir]) - pkglibdir="${gl_saved_pkglibdir}" - libdir="${gl_saved_libdir}" - exec_prefix="${gl_saved_exec_prefix}" - prefix="${gl_saved_prefix}" -]) - -dnl Defines pkglibexecdir_c and pkglibexecdir_c_make, -dnl where pkglibexecdir = $(libexecdir)/$(PACKAGE) -AC_DEFUN_ONCE([gl_BUILD_TO_HOST_PKGLIBEXECDIR], -[ - dnl Find the final value of pkglibexecdir. - gl_saved_prefix="${prefix}" - gl_saved_exec_prefix="${exec_prefix}" - gl_saved_libexecdir="${libexecdir}" - gl_saved_pkglibexecdir="${pkglibexecdir}" - dnl Unfortunately, prefix and exec_prefix get only finally determined - dnl at the end of configure. - if test "X$prefix" = "XNONE"; then - prefix="$ac_default_prefix" - fi - if test "X$exec_prefix" = "XNONE"; then - exec_prefix='${prefix}' - fi - eval exec_prefix="$exec_prefix" - eval libexecdir="$libexecdir" - eval pkglibexecdir="$pkglibexecdir" - gl_BUILD_TO_HOST([pkglibexecdir]) - pkglibexecdir="${gl_saved_pkglibexecdir}" - libexecdir="${gl_saved_libexecdir}" - exec_prefix="${gl_saved_exec_prefix}" - prefix="${gl_saved_prefix}" -]) diff --git a/m4/host-cpu-c-abi.m4 b/m4/host-cpu-c-abi.m4 deleted file mode 100644 index 6ca7721..0000000 --- a/m4/host-cpu-c-abi.m4 +++ /dev/null @@ -1,532 +0,0 @@ -# host-cpu-c-abi.m4 -# serial 20 -dnl Copyright (C) 2002-2025 Free Software Foundation, Inc. -dnl This file is free software; the Free Software Foundation -dnl gives unlimited permission to copy and/or distribute it, -dnl with or without modifications, as long as this notice is preserved. -dnl This file is offered as-is, without any warranty. - -dnl From Bruno Haible and Sam Steingold. - -dnl Sets the HOST_CPU variable to the canonical name of the CPU. -dnl Sets the HOST_CPU_C_ABI variable to the canonical name of the CPU with its -dnl C language ABI (application binary interface). -dnl Also defines __${HOST_CPU}__ and __${HOST_CPU_C_ABI}__ as C macros in -dnl config.h. -dnl -dnl This canonical name can be used to select a particular assembly language -dnl source file that will interoperate with C code on the given host. -dnl -dnl For example: -dnl * 'i386' and 'sparc' are different canonical names, because code for i386 -dnl will not run on SPARC CPUs and vice versa. They have different -dnl instruction sets. -dnl * 'sparc' and 'sparc64' are different canonical names, because code for -dnl 'sparc' and code for 'sparc64' cannot be linked together: 'sparc' code -dnl contains 32-bit instructions, whereas 'sparc64' code contains 64-bit -dnl instructions. A process on a SPARC CPU can be in 32-bit mode or in 64-bit -dnl mode, but not both. -dnl * 'mips' and 'mipsn32' are different canonical names, because they use -dnl different argument passing and return conventions for C functions, and -dnl although the instruction set of 'mips' is a large subset of the -dnl instruction set of 'mipsn32'. -dnl * 'mipsn32' and 'mips64' are different canonical names, because they use -dnl different sizes for the C types like 'int' and 'void *', and although -dnl the instruction sets of 'mipsn32' and 'mips64' are the same. -dnl * The same canonical name is used for different endiannesses. You can -dnl determine the endianness through preprocessor symbols: -dnl - 'arm': test __ARMEL__. -dnl - 'mips', 'mipsn32', 'mips64': test _MIPSEB vs. _MIPSEL. -dnl - 'powerpc64': test __BIG_ENDIAN__ vs. __LITTLE_ENDIAN__. -dnl * The same name 'i386' is used for CPUs of type i386, i486, i586 -dnl (Pentium), AMD K7, Pentium II, Pentium IV, etc., because -dnl - Instructions that do not exist on all of these CPUs (cmpxchg, -dnl MMX, SSE, SSE2, 3DNow! etc.) are not frequently used. If your -dnl assembly language source files use such instructions, you will -dnl need to make the distinction. -dnl - Speed of execution of the common instruction set is reasonable across -dnl the entire family of CPUs. If you have assembly language source files -dnl that are optimized for particular CPU types (like GNU gmp has), you -dnl will need to make the distinction. -dnl See . -AC_DEFUN([gl_HOST_CPU_C_ABI], -[ - AC_REQUIRE([AC_CANONICAL_HOST]) - AC_REQUIRE([gl_C_ASM]) - AC_CACHE_CHECK([host CPU and C ABI], [gl_cv_host_cpu_c_abi], - [case "$host_cpu" in - -changequote(,)dnl - i[34567]86 ) -changequote([,])dnl - gl_cv_host_cpu_c_abi=i386 - ;; - - x86_64 ) - # On x86_64 systems, the C compiler may be generating code in one of - # these ABIs: - # - 64-bit instruction set, 64-bit pointers, 64-bit 'long': x86_64. - # - 64-bit instruction set, 64-bit pointers, 32-bit 'long': x86_64 - # with native Windows (mingw, MSVC). - # - 64-bit instruction set, 32-bit pointers, 32-bit 'long': x86_64-x32. - # - 32-bit instruction set, 32-bit pointers, 32-bit 'long': i386. - AC_COMPILE_IFELSE( - [AC_LANG_SOURCE( - [[#if (defined __x86_64__ || defined __amd64__ \ - || defined _M_X64 || defined _M_AMD64) - int ok; - #else - error fail - #endif - ]])], - [AC_COMPILE_IFELSE( - [AC_LANG_SOURCE( - [[#if defined __ILP32__ || defined _ILP32 - int ok; - #else - error fail - #endif - ]])], - [gl_cv_host_cpu_c_abi=x86_64-x32], - [gl_cv_host_cpu_c_abi=x86_64])], - [gl_cv_host_cpu_c_abi=i386]) - ;; - -changequote(,)dnl - alphaev[4-8] | alphaev56 | alphapca5[67] | alphaev6[78] ) -changequote([,])dnl - gl_cv_host_cpu_c_abi=alpha - ;; - - arm* | aarch64 ) - # Assume arm with EABI. - # On arm64 systems, the C compiler may be generating code in one of - # these ABIs: - # - aarch64 instruction set, 64-bit pointers, 64-bit 'long': arm64. - # - aarch64 instruction set, 32-bit pointers, 32-bit 'long': arm64-ilp32. - # - 32-bit instruction set, 32-bit pointers, 32-bit 'long': arm or armhf. - AC_COMPILE_IFELSE( - [AC_LANG_SOURCE( - [[#ifdef __aarch64__ - int ok; - #else - error fail - #endif - ]])], - [AC_COMPILE_IFELSE( - [AC_LANG_SOURCE( - [[#if defined __ILP32__ || defined _ILP32 - int ok; - #else - error fail - #endif - ]])], - [gl_cv_host_cpu_c_abi=arm64-ilp32], - [gl_cv_host_cpu_c_abi=arm64])], - [# Don't distinguish little-endian and big-endian arm, since they - # don't require different machine code for simple operations and - # since the user can distinguish them through the preprocessor - # defines __ARMEL__ vs. __ARMEB__. - # But distinguish arm which passes floating-point arguments and - # return values in integer registers (r0, r1, ...) - this is - # gcc -mfloat-abi=soft or gcc -mfloat-abi=softfp - from arm which - # passes them in float registers (s0, s1, ...) and double registers - # (d0, d1, ...) - this is gcc -mfloat-abi=hard. GCC 4.6 or newer - # sets the preprocessor defines __ARM_PCS (for the first case) and - # __ARM_PCS_VFP (for the second case), but older GCC does not. - echo 'double ddd; void func (double dd) { ddd = dd; }' > conftest.c - # Look for a reference to the register d0 in the .s file. - AC_TRY_COMMAND(${CC-cc} $CFLAGS $CPPFLAGS $gl_c_asm_opt conftest.c) >/dev/null 2>&1 - if LC_ALL=C grep 'd0,' conftest.$gl_asmext >/dev/null; then - gl_cv_host_cpu_c_abi=armhf - else - gl_cv_host_cpu_c_abi=arm - fi - rm -fr conftest* - ]) - ;; - - hppa1.0 | hppa1.1 | hppa2.0* | hppa64 ) - # On hppa, the C compiler may be generating 32-bit code or 64-bit - # code. In the latter case, it defines _LP64 and __LP64__. - AC_COMPILE_IFELSE( - [AC_LANG_SOURCE( - [[#ifdef __LP64__ - int ok; - #else - error fail - #endif - ]])], - [gl_cv_host_cpu_c_abi=hppa64], - [gl_cv_host_cpu_c_abi=hppa]) - ;; - - ia64* ) - # On ia64 on HP-UX, the C compiler may be generating 64-bit code or - # 32-bit code. In the latter case, it defines _ILP32. - AC_COMPILE_IFELSE( - [AC_LANG_SOURCE( - [[#ifdef _ILP32 - int ok; - #else - error fail - #endif - ]])], - [gl_cv_host_cpu_c_abi=ia64-ilp32], - [gl_cv_host_cpu_c_abi=ia64]) - ;; - - mips* ) - # We should also check for (_MIPS_SZPTR == 64), but gcc keeps this - # at 32. - AC_COMPILE_IFELSE( - [AC_LANG_SOURCE( - [[#if defined _MIPS_SZLONG && (_MIPS_SZLONG == 64) - int ok; - #else - error fail - #endif - ]])], - [gl_cv_host_cpu_c_abi=mips64], - [# In the n32 ABI, _ABIN32 is defined, _ABIO32 is not defined (but - # may later get defined by ), and _MIPS_SIM == _ABIN32. - # In the 32 ABI, _ABIO32 is defined, _ABIN32 is not defined (but - # may later get defined by ), and _MIPS_SIM == _ABIO32. - AC_COMPILE_IFELSE( - [AC_LANG_SOURCE( - [[#if (_MIPS_SIM == _ABIN32) - int ok; - #else - error fail - #endif - ]])], - [gl_cv_host_cpu_c_abi=mipsn32], - [gl_cv_host_cpu_c_abi=mips])]) - ;; - - powerpc* ) - # Different ABIs are in use on AIX vs. Mac OS X vs. Linux,*BSD. - # No need to distinguish them here; the caller may distinguish - # them based on the OS. - # On powerpc64 systems, the C compiler may still be generating - # 32-bit code. And on powerpc-ibm-aix systems, the C compiler may - # be generating 64-bit code. - AC_COMPILE_IFELSE( - [AC_LANG_SOURCE( - [[#if defined __powerpc64__ || defined __LP64__ - int ok; - #else - error fail - #endif - ]])], - [# On powerpc64, there are two ABIs on Linux: The AIX compatible - # one and the ELFv2 one. The latter defines _CALL_ELF=2. - AC_COMPILE_IFELSE( - [AC_LANG_SOURCE( - [[#if defined _CALL_ELF && _CALL_ELF == 2 - int ok; - #else - error fail - #endif - ]])], - [gl_cv_host_cpu_c_abi=powerpc64-elfv2], - [gl_cv_host_cpu_c_abi=powerpc64]) - ], - [gl_cv_host_cpu_c_abi=powerpc]) - ;; - - rs6000 ) - gl_cv_host_cpu_c_abi=powerpc - ;; - - riscv32 | riscv64 ) - # There are 2 architectures (with variants): rv32* and rv64*. - AC_COMPILE_IFELSE( - [AC_LANG_SOURCE( - [[#if __riscv_xlen == 64 - int ok; - #else - error fail - #endif - ]])], - [cpu=riscv64], - [cpu=riscv32]) - # There are 6 ABIs: ilp32, ilp32f, ilp32d, lp64, lp64f, lp64d. - # Size of 'long' and 'void *': - AC_COMPILE_IFELSE( - [AC_LANG_SOURCE( - [[#if defined __LP64__ - int ok; - #else - error fail - #endif - ]])], - [main_abi=lp64], - [main_abi=ilp32]) - # Float ABIs: - # __riscv_float_abi_double: - # 'float' and 'double' are passed in floating-point registers. - # __riscv_float_abi_single: - # 'float' are passed in floating-point registers. - # __riscv_float_abi_soft: - # No values are passed in floating-point registers. - AC_COMPILE_IFELSE( - [AC_LANG_SOURCE( - [[#if defined __riscv_float_abi_double - int ok; - #else - error fail - #endif - ]])], - [float_abi=d], - [AC_COMPILE_IFELSE( - [AC_LANG_SOURCE( - [[#if defined __riscv_float_abi_single - int ok; - #else - error fail - #endif - ]])], - [float_abi=f], - [float_abi='']) - ]) - gl_cv_host_cpu_c_abi="${cpu}-${main_abi}${float_abi}" - ;; - - s390* ) - # On s390x, the C compiler may be generating 64-bit (= s390x) code - # or 31-bit (= s390) code. - AC_COMPILE_IFELSE( - [AC_LANG_SOURCE( - [[#if defined __LP64__ || defined __s390x__ - int ok; - #else - error fail - #endif - ]])], - [gl_cv_host_cpu_c_abi=s390x], - [gl_cv_host_cpu_c_abi=s390]) - ;; - - sparc | sparc64 ) - # UltraSPARCs running Linux have `uname -m` = "sparc64", but the - # C compiler still generates 32-bit code. - AC_COMPILE_IFELSE( - [AC_LANG_SOURCE( - [[#if defined __sparcv9 || defined __arch64__ - int ok; - #else - error fail - #endif - ]])], - [gl_cv_host_cpu_c_abi=sparc64], - [gl_cv_host_cpu_c_abi=sparc]) - ;; - - *) - gl_cv_host_cpu_c_abi="$host_cpu" - ;; - esac - ]) - - dnl In most cases, $HOST_CPU and $HOST_CPU_C_ABI are the same. - HOST_CPU=`echo "$gl_cv_host_cpu_c_abi" | sed -e 's/-.*//'` - HOST_CPU_C_ABI="$gl_cv_host_cpu_c_abi" - AC_SUBST([HOST_CPU]) - AC_SUBST([HOST_CPU_C_ABI]) - - # This was - # AC_DEFINE_UNQUOTED([__${HOST_CPU}__]) - # AC_DEFINE_UNQUOTED([__${HOST_CPU_C_ABI}__]) - # earlier, but KAI C++ 3.2d doesn't like this. - sed -e 's/-/_/g' >> confdefs.h <= 1.0.0', ) @@ -119,6 +119,7 @@ summary += {'Install dir': python.find_installation('python3').get_install_dir() subdir('docs') subdir('icons') subdir('po') +subdir('sounds') subdir('src') summary(summary) diff --git a/po/Makevars b/po/Makevars index 3d57a59..c0da25e 100644 --- a/po/Makevars +++ b/po/Makevars @@ -41,7 +41,7 @@ PACKAGE_GNU = no # It can be your email address, or a mailing list address where translators # can write to without being subscribed, or the URL of a web page through # which the translators can contact you. -MSGID_BUGS_ADDRESS = https://gitlab.gnome.org/GNOME/cthulhu/issues +MSGID_BUGS_ADDRESS = https://groups.io/g/stormux # This is the list of locale categories, beyond LC_MESSAGES, for which the # message catalogs shall be used. It is usually empty. diff --git a/po/ab.po b/po/ab.po index 56f5f94..465cb03 100644 --- a/po/ab.po +++ b/po/ab.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2022-10-31 09:46+0000\n" "Last-Translator: Нанба Наала \n" "Language-Team: Abkhazian \n" @@ -8201,7 +8201,7 @@ msgstr "" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:297 -msgid "Report bugs to cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." msgstr "" #. Translators: Cthulhu normal speaks the text which was just deleted from a diff --git a/po/an.po b/po/an.po index 7a6ca06..95d4663 100644 --- a/po/an.po +++ b/po/an.po @@ -6047,8 +6047,8 @@ msgstr "" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: ../src/cthulhu/messages.py:281 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Informe d'errors a cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Informe d'errors a https://groups.io/g/stormux." #. Translators: In chat applications, it is often possible to see that a "buddy" #. is typing currently (e.g. via a keyboard icon or status text). Some users like diff --git a/po/ar.po b/po/ar.po index a5d531d..368e4dc 100644 --- a/po/ar.po +++ b/po/ar.po @@ -2802,8 +2802,8 @@ msgstr "" #. Translators: this text is the description displayed when Cthulhu is #. launched from the command line and the help text is displayed. #: ../src/cthulhu/cthulhu_bin.py.in:93 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "تقرير العلل لـ cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "تقرير العلل لـ https://groups.io/g/stormux." #. Translators: this is the description of the command line option #. '-r, --replace' which tells Cthulhu to replace any existing Cthulhu diff --git a/po/ast.po b/po/ast.po index 2070b83..fd2fc92 100644 --- a/po/ast.po +++ b/po/ast.po @@ -3363,8 +3363,8 @@ msgstr "" "s'use la opción -n o --no-setup." #: ../src/cthulhu/cthulhu.py:514 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Informe de fallos a cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Informe de fallos a https://groups.io/g/stormux." #. Translators: this is what Cthulhu speaks and brailles when it quits. #. diff --git a/po/be.po b/po/be.po index 3859ac4..9f5987c 100644 --- a/po/be.po +++ b/po/be.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu master\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2023-08-31 16:51+0000\n" "PO-Revision-Date: 2023-09-01 01:43+0300\n" "Last-Translator: Yuras Shumovich \n" @@ -9106,8 +9106,8 @@ msgstr "Наставіць налады карыстальніка (тэкста #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Паведамляйце аб хібах у праграме на адрас: cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Паведамляйце аб хібах у праграме на адрас: https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/bg.po b/po/bg.po index 71edf67..0eff454 100644 --- a/po/bg.po +++ b/po/bg.po @@ -12,7 +12,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu master\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2022-09-11 19:28+0000\n" "PO-Revision-Date: 2022-09-12 14:22+0200\n" "Last-Translator: Alexander Shopov \n" @@ -8242,8 +8242,8 @@ msgstr "Задаване на потребителски настройки (г #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:297 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Докладвайте грешките на адрес: cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Докладвайте грешките на адрес: https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/bn.po b/po/bn.po index e0801ba..3c409e5 100644 --- a/po/bn.po +++ b/po/bn.po @@ -4386,8 +4386,8 @@ msgstr "" "suspend the desktop until Cthulhu is killed." #: ../src/cthulhu/cthulhu.py:1565 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "বাগ cthulhu-list@gnome.org এ প্রতিবেদন করুন।" +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "বাগ https://groups.io/g/stormux এ প্রতিবেদন করুন।" #: ../src/cthulhu/cthulhu.py:1750 msgid "Welcome to Cthulhu." diff --git a/po/bn_IN.po b/po/bn_IN.po index dbed4e8..1b4feb2 100644 --- a/po/bn_IN.po +++ b/po/bn_IN.po @@ -863,8 +863,8 @@ msgid "" msgstr "" #: src/cthulhu/cthulhu.py:1136 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "ত্রুটি ও অন্যান্য সমস্যা cthulhu-list@gnome.org ঠিকানায় জমা দিন।" +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "ত্রুটি ও অন্যান্য সমস্যা https://groups.io/g/stormux ঠিকানায় জমা দিন।" #: src/cthulhu/cthulhu_console_prefs.py:93 src/cthulhu/cthulhu_console_prefs.py:109 msgid "Speech is unavailable." diff --git a/po/bs.po b/po/bs.po index 7a74f24..7872205 100644 --- a/po/bs.po +++ b/po/bs.po @@ -7793,8 +7793,8 @@ msgstr "Postavi korisničke postavke (GUI verzija)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command linije and the help text is displayed. #: ../src/cthulhu/messages.py:281 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Izvještava bugove na cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Izvještava bugove na https://groups.io/g/stormux." #. Translators: In chat applications, it is often possible to seje that a "buddy" #. is typing currently (e.g. via a keyboard icon or status text). Some users liki diff --git a/po/ca.po b/po/ca.po index 0e0f264..5310994 100644 --- a/po/ca.po +++ b/po/ca.po @@ -15,7 +15,7 @@ msgid "" msgstr "" "Project-Id-Version: ca\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2023-08-11 17:30+0000\n" "PO-Revision-Date: 2023-09-11 13:18+0200\n" "Last-Translator: Bàrbara Vidal \n" @@ -8505,8 +8505,8 @@ msgstr "Configura les preferències de l'usuari (versió gràfica)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Informeu d'errors a cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Informeu d'errors a https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/ca@valencia.po b/po/ca@valencia.po index c8f729b..060ccc6 100644 --- a/po/ca@valencia.po +++ b/po/ca@valencia.po @@ -8082,8 +8082,8 @@ msgstr "Configura les preferències de l'usuari (versió gràfica)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: ../src/cthulhu/messages.py:267 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Informeu d'errors a cthulhu-list@gnome.org" +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Informeu d'errors a https://groups.io/g/stormux" #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/ckb.po b/po/ckb.po index 9b2a8f8..6ce7226 100644 --- a/po/ckb.po +++ b/po/ckb.po @@ -8175,7 +8175,7 @@ msgstr "" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:293 -msgid "Report bugs to cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." msgstr "" #. Translators: Cthulhu normal speaks the text which was just deleted from a diff --git a/po/cs.po b/po/cs.po index 300dcd0..af46a75 100644 --- a/po/cs.po +++ b/po/cs.po @@ -12,7 +12,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2023-09-13 16:23+0000\n" "PO-Revision-Date: 2023-09-14 14:52+0200\n" "Last-Translator: Daniel Rusek \n" @@ -8516,8 +8516,8 @@ msgstr "Nastavit uživatelské předvolby (grafická verze)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Chyby hlašte na cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Chyby hlašte na https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/cy.po b/po/cy.po index 266c41a..304e72a 100644 --- a/po/cy.po +++ b/po/cy.po @@ -893,8 +893,8 @@ msgstr "" "oni bai fod yr opsiwn -n neu --no-setup wedi ei ddefnyddio." #: src/cthulhu/cthulhu.py:1136 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Adroddwch namau at cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Adroddwch namau at https://groups.io/g/stormux." #: src/cthulhu/cthulhu_console_prefs.py:93 src/cthulhu/cthulhu_console_prefs.py:109 msgid "Speech is unavailable." diff --git a/po/da.po b/po/da.po index 48a99f2..20d8519 100644 --- a/po/da.po +++ b/po/da.po @@ -20,7 +20,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu master\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2023-08-11 17:30+0000\n" "PO-Revision-Date: 2023-09-05 07:41+0200\n" "Last-Translator: Alan Mortensen \n" @@ -8530,8 +8530,8 @@ msgstr "Opsætning af brugerindstillinger (grafisk version)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Indrapporter fejl til cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Indrapporter fejl til https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/de.po b/po/de.po index 9196f3d..28b270a 100644 --- a/po/de.po +++ b/po/de.po @@ -24,7 +24,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2023-08-22 18:47+0000\n" "PO-Revision-Date: 2023-08-29 20:13+0200\n" "Last-Translator: Philipp Kiemle \n" @@ -8549,8 +8549,8 @@ msgstr "Benutzereinstellungen einrichten (Graphische Version)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Programmfehler an cthulhu-list@gnome.org melden." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Programmfehler an https://groups.io/g/stormux melden." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/dz.po b/po/dz.po index 6fdfeb0..7eb123f 100644 --- a/po/dz.po +++ b/po/dz.po @@ -5092,8 +5092,8 @@ msgstr "རྣམ་གྲངས་ %d ལས་ %d" #~ "ལག་ལེན་པ་གིས་ ཧེ་མ་ཨོར་ཀ་གཞི་སྒྲིག་མ་འབད་འབདཝ་མེདཔ་དང་\n" #~ " རང་བཞིན་གྱིས་ -ཨེན་ ཡང་ན་ --གཞི་སྒྲིག་མེད་པའི་གདམ་ཁ་དེ་ ལག་ལེན་མ་འཐཔ་ཅིན་\n" #~ "ཨོར་ཀ་གིས་རང་བཞིན་གྱིས་ གདའ་གདམ་ཚུ་གཞི་སྒྲིག་འབད་འོང་།" -#~ msgid "Report bugs to cthulhu-list@gnome.org." -#~ msgstr "རྐྱེན་གྱི་སྙན་ཞུ་ཚུ་ cthulhu-list@gnome.org ལུ་གཏང་།" +#~ msgid "Report bugs to https://groups.io/g/stormux." +#~ msgstr "རྐྱེན་གྱི་སྙན་ཞུ་ཚུ་ https://groups.io/g/stormux ལུ་གཏང་།" #~ msgid "Do you really want to quit Cthulhu?" #~ msgstr "ཁྱོད་ཀྱིས་ཐད་རི་འབའ་རི་ ཨོར་ཀ་སྤང་ནི་ཨིན་ན?" #~ msgid "Question" diff --git a/po/el.po b/po/el.po index f95baf1..4265b08 100644 --- a/po/el.po +++ b/po/el.po @@ -15,7 +15,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu.HEAD\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2020-06-30 15:04+0000\n" "PO-Revision-Date: 2020-08-08 18:56+0300\n" "Last-Translator: Efstathios Iosifidis \n" @@ -8548,8 +8548,8 @@ msgstr "Ρύθμιση των προτιμήσεων χρήστη (έκδοση #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:297 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Αναφέρετε σφάλματα στο cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Αναφέρετε σφάλματα στο https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a @@ -15048,12 +15048,12 @@ msgstr "" #~ "will automatically launch the preferences set up unless\n" #~ "the -n or --no-setup option is used.\n" #~ "\n" -#~ "Report bugs to cthulhu-list@gnome.org." +#~ "Report bugs to https://groups.io/g/stormux." #~ msgstr "" #~ "Αν το Cthulhu δεν έχει ρυθμιστεί προηγουμένως από το χρήστη, τότε\n" #~ "θα πραγματοποιήσει αυτόματη ρύθμιση των προτιμήσεων εκτός κι αν\n" #~ "χρησιμοποιείται η ρύθμιση -n ή --no-setup.\n" -#~ "Παρακαλώ αναφέρετε τα σφάλματα στο cthulhu-list@gnome.org." +#~ "Παρακαλώ αναφέρετε τα σφάλματα στο https://groups.io/g/stormux." #~ msgid "Set up user preferences" #~ msgstr "Προτιμήσεις ρυθμίσεων χρήστη" diff --git a/po/en_CA.po b/po/en_CA.po index 8afcc32..c94e726 100644 --- a/po/en_CA.po +++ b/po/en_CA.po @@ -901,7 +901,7 @@ msgid "" msgstr "" #: src/cthulhu/cthulhu.py:1136 -msgid "Report bugs to cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." msgstr "" #: src/cthulhu/cthulhu_console_prefs.py:93 src/cthulhu/cthulhu_console_prefs.py:109 diff --git a/po/en_GB.po b/po/en_GB.po index f8d91ee..a2dd980 100644 --- a/po/en_GB.po +++ b/po/en_GB.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2023-08-11 17:30+0000\n" "PO-Revision-Date: 2023-12-06 13:18+0000\n" "Last-Translator: Bruce Cowan \n" @@ -8484,8 +8484,8 @@ msgstr "Set up user preferences (GUI version)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Report bugs to cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Report bugs to https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a @@ -15027,13 +15027,13 @@ msgstr "" #~ "will automatically launch the preferences set up unless\n" #~ "the -n or --no-setup option is used.\n" #~ "\n" -#~ "Report bugs to cthulhu-list@gnome.org." +#~ "Report bugs to https://groups.io/g/stormux." #~ msgstr "" #~ "If Cthulhu has not been previously set up by the user, Cthulhu\n" #~ "will automatically launch the preferences set up unless\n" #~ "the -n or --no-setup option is used.\n" #~ "\n" -#~ "Report bugs to cthulhu-list@gnome.org." +#~ "Report bugs to https://groups.io/g/stormux." #~ msgid "Set up user preferences" #~ msgstr "Set up user preferences" diff --git a/po/eo.po b/po/eo.po index 52ff6c2..fff54cb 100644 --- a/po/eo.po +++ b/po/eo.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: gnome-cthulhu\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2023-09-15 13:44+0000\n" "PO-Revision-Date: 2023-09-15 20:08+0200\n" "Last-Translator: Kristjan SCHMIDT \n" @@ -8926,8 +8926,8 @@ msgstr "" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Raporti cimojn al cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Raporti cimojn al https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/es.po b/po/es.po index 5096a51..e3717d0 100644 --- a/po/es.po +++ b/po/es.po @@ -1,22 +1,22 @@ -# translation of cthulhu.HEAD.po to Español -# Traducción de Cthulhu al Español -# This file is distributed under the same license as the CTHULHU package. -# Copyright (C) 2005 GNOME Foundation. -# -# Francisco Javier F. Serrador , 2004, 2006. -# Maria Majadas , 2005. -# Jorge González , 2007, 2008, 2010, 2011. -# -# -# Miguel Rodríguez Núñez , 2015. -# +# translation of cthulhu.HEAD.po to Español +# Traducción de Cthulhu al Español +# This file is distributed under the same license as the CTHULHU package. +# Copyright (C) 2005 GNOME Foundation. +# +# Francisco Javier F. Serrador , 2004, 2006. +# Maria Majadas , 2005. +# Jorge González , 2007, 2008, 2010, 2011. +# +# +# Miguel Rodríguez Núñez , 2015. +# # Francisco Javier Dorado Martínez , 2007-2022. # Daniel Mustieles , 2022-2023. # msgid "" msgstr "" "Project-Id-Version: cthulhu.master\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2023-08-18 09:51+0000\n" "PO-Revision-Date: 2023-08-09 12:42+0200\n" "Last-Translator: Daniel Mustieles \n" @@ -2250,7 +2250,7 @@ msgstr "Marcar el comienzo de una selección de texto" msgid "Mark the end of a text selection" msgstr "Marcar el final de una selección de texto" -# Escape es hablado +# Escape es hablado #. Translators: Cthulhu has a "Learn Mode" that will allow the user to type any key #. on the keyboard and hear what the effects of that key would be. The effects #. might be what Cthulhu would do if it had a handler for the particular key @@ -5691,16 +5691,16 @@ msgstr "Hablar _celdas dentro de otra celda" msgid "Attribute Name" msgstr "Nombre del atributo" -# Notas: -# Añadir una nota -# -# Comentarios extraídos: -# Translators: Gecko native caret navigation is where Firefox itself controls -# how the arrow keys move the caret around HTML content. It's often broken, so -# Cthulhu needs to provide its own support. As such, Cthulhu offers the user the -# ability to switch between the Firefox mode and the Cthulhu mode. This is the -# label of a checkbox in which users can indicate their default preference. -# +# Notas: +# Añadir una nota +# +# Comentarios extraídos: +# Translators: Gecko native caret navigation is where Firefox itself controls +# how the arrow keys move the caret around HTML content. It's often broken, so +# Cthulhu needs to provide its own support. As such, Cthulhu offers the user the +# ability to switch between the Firefox mode and the Cthulhu mode. This is the +# label of a checkbox in which users can indicate their default preference. +# #. Translators: Gecko native caret navigation is where Firefox itself controls #. how the arrow keys move the caret around HTML content. It's often broken, so #. Cthulhu needs to provide its own support. As such, Cthulhu offers the user the @@ -8518,8 +8518,8 @@ msgstr "Configurar las preferencias del usuario (versión IGU)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Informe de errores a cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Informe de errores a https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a @@ -14979,7 +14979,7 @@ msgstr "" #~ "Presione 1 para los atajos predeterminados de Cthulhu. Presione 2 para los " #~ "atajos de Cthulhu de la aplicación actual. Presione Escape para salir." -# Escape es hablado +# Escape es hablado #~ msgid "" #~ "Enters list shortcuts mode. Press escape to exit list shortcuts mode." #~ msgstr "" @@ -15150,14 +15150,14 @@ msgstr "" #~ "will automatically launch the preferences set up unless\n" #~ "the -n or --no-setup option is used.\n" #~ "\n" -#~ "Report bugs to cthulhu-list@gnome.org." +#~ "Report bugs to https://groups.io/g/stormux." #~ msgstr "" #~ "Si el usuario no ha configurado Cthulhu previamente,\n" #~ "se lanzará automáticamente la configuración de las preferencias a menos " #~ "que\n" #~ "se use la opción -n o --no-setup.\n" #~ "\n" -#~ "Informe de errores en cthulhu-list@gnome.org." +#~ "Informe de errores en https://groups.io/g/stormux." #~ msgid "Set up user preferences" #~ msgstr "Configurar las preferencias de usuario" diff --git a/po/et.po b/po/et.po index fd0f3bf..c3a65a3 100644 --- a/po/et.po +++ b/po/et.po @@ -4551,8 +4551,8 @@ msgid "" "suspend the desktop until Cthulhu is killed." msgstr "" -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Vigadest palun teatada aadressil cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Vigadest palun teatada aadressil https://groups.io/g/stormux." #. Translators: This message is displayed when the user tries #. to start Cthulhu and includes an invalid option as an argument. diff --git a/po/eu.po b/po/eu.po index 704e96c..3c25a97 100644 --- a/po/eu.po +++ b/po/eu.po @@ -8,7 +8,7 @@ # msgid "" msgstr "Project-Id-Version: cthulhu master\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2023-08-11 17:30+0000\n" "PO-Revision-Date: 2023-08-13 10:00+0100\n" "Last-Translator: Asier Sarasua Garmendia \n" @@ -8473,8 +8473,8 @@ msgstr "Konfiguratu erabiltzailearen hobespenak (GUI bertsioa)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Bidali arazoen berri hona: cthulhu-list@gnome.org" +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Bidali arazoen berri hona: https://groups.io/g/stormux" #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/fa.po b/po/fa.po index a360ab7..3ffa5ad 100644 --- a/po/fa.po +++ b/po/fa.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu master\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2023-10-13 21:15+0000\n" "PO-Revision-Date: 2023-10-23 15:17+0330\n" "Last-Translator: Danial Behzadi \n" @@ -8464,7 +8464,7 @@ msgstr "" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." msgstr "" #. Translators: Cthulhu normal speaks the text which was just deleted from a diff --git a/po/fi.po b/po/fi.po index 936c54c..ed5c212 100644 --- a/po/fi.po +++ b/po/fi.po @@ -13,7 +13,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2023-08-11 17:30+0000\n" "PO-Revision-Date: 2023-08-17 22:23+0300\n" "Last-Translator: Jiri Grönroos \n" @@ -8784,8 +8784,8 @@ msgstr "Määrittele käyttäjän asetukset (graafisen käyttöliittymän versio #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Ilmoita vioista sähköpostitse osoitteeseen cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Ilmoita vioista sähköpostitse osoitteeseen https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/fr.po b/po/fr.po index 657720d..33ecc7a 100644 --- a/po/fr.po +++ b/po/fr.po @@ -19,7 +19,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu master\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2023-09-17 04:09+0000\n" "PO-Revision-Date: 2023-10-23 17:52+0200\n" "Last-Translator: Guillaume Bernard \n" @@ -8523,8 +8523,8 @@ msgstr "Définit les préférences utilisateur (mode graphique)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Rapportez les bogues à cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Rapportez les bogues à https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/fur.po b/po/fur.po index dca4a85..1fa2fab 100644 --- a/po/fur.po +++ b/po/fur.po @@ -8048,7 +8048,7 @@ msgstr "" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: ../src/cthulhu/messages.py:267 -msgid "Report bugs to cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." msgstr "" #. Translators: Cthulhu normal speaks the text which was just deleted from a diff --git a/po/ga.po b/po/ga.po index 6ea2a53..c8136fd 100644 --- a/po/ga.po +++ b/po/ga.po @@ -4250,8 +4250,8 @@ msgid "" msgstr "" #: ../src/cthulhu/cthulhu.py:1553 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Seol tuairiscí faoi fhabhtanna chuig cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Seol tuairiscí faoi fhabhtanna chuig https://groups.io/g/stormux." #: ../src/cthulhu/cthulhu.py:1736 msgid "Welcome to Cthulhu." diff --git a/po/gl.po b/po/gl.po index 5876bda..0b9d184 100644 --- a/po/gl.po +++ b/po/gl.po @@ -16,7 +16,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu-master-po-gl-70969.merged\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2023-08-11 19:30+0000\n" "PO-Revision-Date: 2023-09-03 20:55+0200\n" "Last-Translator: Fran Dieguez \n" @@ -8511,8 +8511,8 @@ msgstr "Configura as preferencias do usuario (versión GUI)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Informe de fallos a cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Informe de fallos a https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/gu.po b/po/gu.po index 5a0065a..cbcbd52 100644 --- a/po/gu.po +++ b/po/gu.po @@ -4400,8 +4400,8 @@ msgid "" msgstr "" #: ../src/cthulhu/cthulhu.py:1522 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "cthulhu-list@gnome.org ને ભૂલોનો અહેવાલ આપો." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "https://groups.io/g/stormux ને ભૂલોનો અહેવાલ આપો." #: ../src/cthulhu/cthulhu.py:1693 msgid "Welcome to Cthulhu." diff --git a/po/he.po b/po/he.po index 5ed0988..5d464fb 100644 --- a/po/he.po +++ b/po/he.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2024-01-14 07:04+0000\n" "PO-Revision-Date: \n" "Last-Translator: Yaron Shahrabani \n" @@ -8587,8 +8587,8 @@ msgstr "הגדרת העדפות משתמש (גרסה חזותית)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Report bugs to cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Report bugs to https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a @@ -14834,8 +14834,8 @@ msgid "" "minimum press home, and for maximum press end." msgstr "" -#~ msgid "Report bugs on https://gitlab.gnome.org/GNOME/cthulhu/-/issues." -#~ msgstr "יש לדווח על תקלות ב־https://gitlab.gnome.org/GNOME/cthulhu/-/issues." +#~ msgid "Report bugs on https://groups.io/g/stormux" +#~ msgstr "יש לדווח על תקלות ב־https://groups.io/g/stormux" #~ msgid "opens popup" #~ msgstr "פותח חלונית צצה" diff --git a/po/hi.po b/po/hi.po index 059ca00..f1f37ad 100644 --- a/po/hi.po +++ b/po/hi.po @@ -6381,8 +6381,8 @@ msgstr "ओर्का - स्क्रिप्ट स्क्रीन र #. Translators: this text is the description displayed when Cthulhu is #. launched from the command line and the help text is displayed. #: ../src/cthulhu/cthulhu_bin.py.in:93 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "cthulhu-list@gnome.org में बग रिपोर्ट करें." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "https://groups.io/g/stormux में बग रिपोर्ट करें." #. Translators: this is the description of the command line option #. '-r, --replace' which tells Cthulhu to replace any existing Cthulhu diff --git a/po/hr.po b/po/hr.po index a0993b2..d022a31 100644 --- a/po/hr.po +++ b/po/hr.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: gnome-cthulhu\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2019-09-24 11:20+0000\n" "PO-Revision-Date: 2019-09-26 22:11+0100\n" "Last-Translator: yvonimir stanecic \n" @@ -8201,8 +8201,8 @@ msgstr "Namještanje korisničkih postavki (GUI inačica)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:293 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Prijavite greške na cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Prijavite greške na https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/hu.po b/po/hu.po index 857eb98..6d95494 100644 --- a/po/hu.po +++ b/po/hu.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu master\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2023-08-08 11:20+0000\n" "PO-Revision-Date: 2023-08-08 17:45+0200\n" "Last-Translator: Attila Hammer \n" @@ -8551,8 +8551,8 @@ msgstr "Felhasználói beállítások (grafikus változat)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "A hibákat az cthulhu-list@gnome.org levelezőlistán jelezheti." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "A hibákat az https://groups.io/g/stormux levelezőlistán jelezheti." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/id.po b/po/id.po index cff709d..9234f9e 100644 --- a/po/id.po +++ b/po/id.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu gnome-45\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2023-08-15 11:25+0000\n" "PO-Revision-Date: 2023-09-07 13:54+0700\n" "Last-Translator: Andika Triwidada \n" @@ -8482,8 +8482,8 @@ msgstr "Atur preferensi pengguna (versi GUI)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Laporkan kutu ke cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Laporkan kutu ke https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/is.po b/po/is.po index f80b378..350b3c6 100644 --- a/po/is.po +++ b/po/is.po @@ -7843,7 +7843,7 @@ msgstr "" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: ../src/cthulhu/messages.py:281 -msgid "Report bugs to cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." msgstr "" #. Translators: In chat applications, it is often possible to see that a "buddy" diff --git a/po/it.po b/po/it.po index f1b38fc..700c9cf 100644 --- a/po/it.po +++ b/po/it.po @@ -27,7 +27,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2023-05-05 12:35+0000\n" "PO-Revision-Date: 2023-05-11 12:43+0200\n" "Last-Translator: Gianvito Cavasoli \n" @@ -8349,8 +8349,8 @@ msgstr "Imposta le preferenze utente (versione grafica)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:297 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Segnalare i bug a cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Segnalare i bug a https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/ja.po b/po/ja.po index 92fb169..1ada79d 100644 --- a/po/ja.po +++ b/po/ja.po @@ -3618,7 +3618,7 @@ msgid "" "will automatically launch the preferences set up unless\n" "the -n or --no-setup option is used.\n" "\n" -"Report bugs to cthulhu-list@gnome.org." +"Report bugs to https://groups.io/g/stormux." msgstr "" "未だ設定が完了していない場合は\n" "-n または --no-setup を指定しない限り\n" @@ -9100,8 +9100,8 @@ msgstr "" #~ msgid "Usage: cthulhu [OPTION...]" #~ msgstr "用法: cthulhu [オプション...]" -#~ msgid "Report bugs to cthulhu-list@gnome.org." -#~ msgstr "バグの報告は cthulhu-list@gnome.org まで" +#~ msgid "Report bugs to https://groups.io/g/stormux." +#~ msgstr "バグの報告は https://groups.io/g/stormux まで" #~ msgid "Invalid" #~ msgstr "無効" diff --git a/po/ka.po b/po/ka.po index 8094da5..8c6ab68 100644 --- a/po/ka.po +++ b/po/ka.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2024-03-28 17:33+0000\n" "PO-Revision-Date: 2024-04-13 03:05+0200\n" "Last-Translator: Ekaterine Papava \n" @@ -8488,8 +8488,8 @@ msgstr "მომხმარებლის პარამეტრები #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "შეცდომების შესახებ მოიწერეთ cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "შეცდომების შესახებ მოიწერეთ https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/kab.po b/po/kab.po index f70da36..8485f13 100644 --- a/po/kab.po +++ b/po/kab.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu master\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2021-11-19 13:57+0000\n" "PO-Revision-Date: 2021-11-24 07:27+0100\n" "Language-Team: Kabyle \n" @@ -8202,7 +8202,7 @@ msgstr "" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:297 -msgid "Report bugs to cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." msgstr "" #. Translators: Cthulhu normal speaks the text which was just deleted from a diff --git a/po/kk.po b/po/kk.po index e4a1b2b..19b6ecf 100644 --- a/po/kk.po +++ b/po/kk.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu gnome-3-22\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2023-08-11 17:30+0000\n" "PO-Revision-Date: 2023-08-19 13:06+0600\n" "Last-Translator: Baurzhan Muftakhidinov \n" @@ -8473,7 +8473,7 @@ msgstr "" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." msgstr "" #. Translators: Cthulhu normal speaks the text which was just deleted from a diff --git a/po/kn.po b/po/kn.po index 9978dee..1a582f4 100644 --- a/po/kn.po +++ b/po/kn.po @@ -4900,7 +4900,7 @@ msgid "" msgstr "ಅಪ್ ಅಪ್ n ಆಯ್ಕೆ ." #: ../src/cthulhu/cthulhu.py:1345 -msgid "Report bugs to cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." msgstr "" #: ../src/cthulhu/cthulhu.py:1538 diff --git a/po/ko.po b/po/ko.po index 1ddeced..3bdfbda 100644 --- a/po/ko.po +++ b/po/ko.po @@ -4120,8 +4120,8 @@ msgid "" msgstr "" #: ../src/cthulhu/cthulhu.py:1450 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "문제점을 cthulhu-list@gnome.org로 알려주십시오." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "문제점을 https://groups.io/g/stormux로 알려주십시오." #: ../src/cthulhu/cthulhu.py:1621 msgid "Welcome to Cthulhu." diff --git a/po/lt.po b/po/lt.po index abee5f0..7725eb4 100644 --- a/po/lt.po +++ b/po/lt.po @@ -12,7 +12,7 @@ msgid "" msgstr "" "Project-Id-Version: lt\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2023-08-11 17:30+0000\n" "PO-Revision-Date: 2023-08-15 22:23+0300\n" "Last-Translator: Aurimas Černius \n" @@ -8494,8 +8494,8 @@ msgstr "Nustatyti naudotojo nuostatas (grafinė versija)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Praneškite apie klaida adresu cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Praneškite apie klaida adresu https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/lv.po b/po/lv.po index 4478e94..9bc2f37 100644 --- a/po/lv.po +++ b/po/lv.po @@ -10,7 +10,7 @@ msgid "" msgstr "" "Project-Id-Version: lv\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2022-07-14 19:12+0000\n" "PO-Revision-Date: 2022-09-11 22:27+0300\n" "Last-Translator: Rūdolfs Mazurs \n" @@ -8231,8 +8231,8 @@ msgstr "Iestatīt lietotāja iestatījumus (grafiskā versija)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:297 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Ziņojiet par kļūdām uz cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Ziņojiet par kļūdām uz https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/mai.po b/po/mai.po index a3037af..ae68ebd 100644 --- a/po/mai.po +++ b/po/mai.po @@ -3982,8 +3982,8 @@ msgid "" msgstr "" #: ../src/cthulhu/cthulhu.py:1450 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "cthulhu-list@gnome.orgमे बग रिपोर्ट करू ." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "https://groups.io/g/stormuxमे बग रिपोर्ट करू ." #: ../src/cthulhu/cthulhu.py:1621 msgid "Welcome to Cthulhu." diff --git a/po/mk.po b/po/mk.po index d119614..63f6a79 100644 --- a/po/mk.po +++ b/po/mk.po @@ -3455,8 +3455,8 @@ msgstr "" "освен ако не се користат опциите -n или --no-setup." #: ../src/cthulhu/cthulhu.py:507 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Пријавете бубачки на cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Пријавете бубачки на https://groups.io/g/stormux." #. Translators: this is what Cthulhu speaks and brailles when it quits. #. diff --git a/po/ml.po b/po/ml.po index cab6558..a10d1fb 100644 --- a/po/ml.po +++ b/po/ml.po @@ -8548,8 +8548,8 @@ msgstr "ഉപയോക്താവ്‌ അഭിരുചി തിര‍ #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: ../src/cthulhu/messages.py:267 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "cthulhu-list@gnome.org ബഗുകള്‍ റിപോര്‍ട്ട് ചെയ്യുക." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "https://groups.io/g/stormux ബഗുകള്‍ റിപോര്‍ട്ട് ചെയ്യുക." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/mr.po b/po/mr.po index 1b0fd40..884873d 100644 --- a/po/mr.po +++ b/po/mr.po @@ -902,8 +902,8 @@ msgid "" msgstr "आहे." #: ../src/cthulhu/cthulhu.py:1104 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "त्रुटी अहवाल येथे द्या cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "त्रुटी अहवाल येथे द्या https://groups.io/g/stormux." #: ../src/cthulhu/cthulhu_console_prefs.py:93 ../src/cthulhu/cthulhu_console_prefs.py:109 msgid "Speech is unavailable." diff --git a/po/ms.po b/po/ms.po index d62da53..ccdf1a5 100644 --- a/po/ms.po +++ b/po/ms.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu master\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2020-01-23 19:57+0000\n" "PO-Revision-Date: 2020-01-24 23:26+0800\n" "Last-Translator: abuyop \n" @@ -8189,8 +8189,8 @@ msgstr "Persediaan keutamaan pengguna (versi GUI)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:293 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Lapor pepijat ke cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Lapor pepijat ke https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/nb.po b/po/nb.po index d886965..ea07d24 100644 --- a/po/nb.po +++ b/po/nb.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu 4.0\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2021-04-19 13:34+0000\n" "PO-Revision-Date: 2021-05-11 13:04+0200\n" "Last-Translator: Kjartan Maraas \n" @@ -8253,8 +8253,8 @@ msgstr "Sett brukervalg (GUI-versjon)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:297 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Rapporter feil til cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Rapporter feil til https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/ne.po b/po/ne.po index f2c41ce..c2c611a 100644 --- a/po/ne.po +++ b/po/ne.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: Gnome Nepali Translation Project\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2022-07-14 19:12+0000\n" "PO-Revision-Date: 2022-09-11 05:59+0545\n" "Last-Translator: Pawan Chitrakar \n" @@ -8415,8 +8415,8 @@ msgstr "उपभोक्ता प्राथमिकता पातो स #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:297 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "उडुस पाएमा cthulhu-list@gnome.org मा जानकारी दिनु होस् ।" +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "उडुस पाएमा https://groups.io/g/stormux मा जानकारी दिनु होस् ।" #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/nl.po b/po/nl.po index d1662c8..56b20e4 100644 --- a/po/nl.po +++ b/po/nl.po @@ -17,7 +17,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2022-11-02 00:05+0000\n" "PO-Revision-Date: 2022-12-19 19:37+0100\n" "Last-Translator: Nathan Follens \n" @@ -8346,8 +8346,8 @@ msgstr "Stel gebruikersvoorkeuren in (GUI-versie)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:297 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Rapporteer fouten bij cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Rapporteer fouten bij https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/nn.po b/po/nn.po index 2ae8ae8..42d1de4 100644 --- a/po/nn.po +++ b/po/nn.po @@ -4785,8 +4785,8 @@ msgid "" msgstr "" #: ../src/cthulhu/cthulhu.py:1362 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Rapportar feil til cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Rapportar feil til https://groups.io/g/stormux." #: ../src/cthulhu/cthulhu.py:1555 msgid "Welcome to Cthulhu." diff --git a/po/oc.po b/po/oc.po index 42070fb..8813868 100644 --- a/po/oc.po +++ b/po/oc.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: oc\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2021-04-19 13:34+0000\n" "PO-Revision-Date: 2021-05-11 20:57+0200\n" "Last-Translator: Quentin PAGÈS\n" @@ -8629,8 +8629,8 @@ msgstr "Definís las preferéncias d'utilizaire (mòde tèxte)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:297 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Raportar las anomalias a cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Raportar las anomalias a https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/or.po b/po/or.po index e8ad004..d4603b5 100644 --- a/po/or.po +++ b/po/or.po @@ -4100,8 +4100,8 @@ msgid "" msgstr "" #: ../src/cthulhu/cthulhu.py:1450 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr " cthulhu-list@gnome.org. କୁ ବଗ୍ ଗୁଡିକ ସୂଚନା ଜଣାଅ" +msgid "Report bugs to https://groups.io/g/stormux." +msgstr " https://groups.io/g/stormux. କୁ ବଗ୍ ଗୁଡିକ ସୂଚନା ଜଣାଅ" #: ../src/cthulhu/cthulhu.py:1621 msgid "Welcome to Cthulhu." diff --git a/po/pa.po b/po/pa.po index b62fda4..12cbbf0 100644 --- a/po/pa.po +++ b/po/pa.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu.HEAD\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2023-08-11 17:30+0000\n" "PO-Revision-Date: 2023-08-24 19:34-0700\n" "Last-Translator: A S Alam \n" @@ -9421,8 +9421,8 @@ msgstr "ਯੂਜ਼ਰ ਪਸੰਦ ਸੈੱਟਅੱਪ (ਪਾਠ ਵਰਜ #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "ਬੱਗ ਜਾਣਕਾਰੀ cthulhu-list@gnome.org ਉੱਤੇ ਭੇਜੋ।" +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "ਬੱਗ ਜਾਣਕਾਰੀ https://groups.io/g/stormux ਉੱਤੇ ਭੇਜੋ।" #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/pl.po b/po/pl.po index 4784cf7..ce13cb4 100644 --- a/po/pl.po +++ b/po/pl.po @@ -12,7 +12,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2023-08-11 17:30+0000\n" "PO-Revision-Date: 2023-08-15 17:05+0200\n" "Last-Translator: Piotr Drąg \n" @@ -8521,9 +8521,9 @@ msgstr "Preferencje użytkownika (wersja graficzna)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." msgstr "" -"Prosimy zgłaszać błędy na adres cthulhu-list@gnome.org (w języku angielskim)." +"Prosimy zgłaszać błędy na adres https://groups.io/g/stormux (w języku angielskim)." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/pt.po b/po/pt.po index b701055..46e47b0 100644 --- a/po/pt.po +++ b/po/pt.po @@ -13,7 +13,7 @@ msgid "" msgstr "" "Project-Id-Version: 3.10\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2024-04-13 01:06+0000\n" "PO-Revision-Date: 2024-04-27 23:37+0100\n" "Last-Translator: Hugo Carvalho \n" @@ -8508,8 +8508,8 @@ msgstr "Definir as preferências do utilizador (versão em GUI)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Reportar erros (em inglês) para cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Reportar erros (em inglês) para https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/pt_BR.po b/po/pt_BR.po index 90a1477..8a269f6 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -31,7 +31,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2024-02-03 19:59+0000\n" "PO-Revision-Date: 2024-03-08 08:48-0300\n" "Last-Translator: Leônidas Araújo \n" @@ -8582,8 +8582,8 @@ msgstr "Configura as preferências do usuário (versão em interface gráfica)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Relate erros para cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Relate erros para https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/ro.po b/po/ro.po index cf566e8..fb32d90 100644 --- a/po/ro.po +++ b/po/ro.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: gnome-cthulhu\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2023-10-09 16:00+0000\n" "PO-Revision-Date: 2023-08-08 17:22+0300\n" "Last-Translator: Florentina Mușat \n" @@ -8497,8 +8497,8 @@ msgstr "Задать параметры пользователя (графиче #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Об ошибках сообщайте в список рассылки cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Об ошибках сообщайте в список рассылки https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/rw.po b/po/rw.po index 7820703..0e96189 100644 --- a/po/rw.po +++ b/po/rw.po @@ -971,7 +971,7 @@ msgid "" msgstr "" #: src/cthulhu/cthulhu.py:1136 -msgid "Report bugs to cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." msgstr "" #: src/cthulhu/cthulhu_console_prefs.py:93 src/cthulhu/cthulhu_console_prefs.py:109 diff --git a/po/sk.po b/po/sk.po index 9598064..041ac23 100644 --- a/po/sk.po +++ b/po/sk.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu master\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhucthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux" "POT-Creation-Date: 2018-07-26 14:39+0000\n" "PO-Revision-Date: 2018-09-12 12:18+0200\n" "Last-Translator: Peter Vágner \n" @@ -8207,8 +8207,8 @@ msgstr "Používateľské nastavenia (grafická verzia)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: ../src/cthulhu/messages.py:293 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Chyby oznamujte na adresu cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Chyby oznamujte na adresu https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/sl.po b/po/sl.po index 9bd7410..56202e9 100644 --- a/po/sl.po +++ b/po/sl.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu master\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2023-09-10 17:36+0000\n" "PO-Revision-Date: 2023-09-10 23:33+0200\n" "Last-Translator: Matej Urbančič \n" @@ -8493,8 +8493,8 @@ msgstr "Nastavitve možnosti uporabnika (grafična različica)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Pošiljanje poročil o napakah na cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Pošiljanje poročil o napakah na https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/sr.po b/po/sr.po index 83fa375..2cb51cd 100644 --- a/po/sr.po +++ b/po/sr.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2022-07-14 19:12+0000\n" "PO-Revision-Date: 2022-08-09 10:28+0200\n" "Last-Translator: Марко М. Костић \n" @@ -8236,8 +8236,8 @@ msgstr "Подешава поставке корисника (издање гр #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:297 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Пријавите грешке на „cthulhu-list@gnome.org“." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Пријавите грешке на „https://groups.io/g/stormux“." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/sr@latin.po b/po/sr@latin.po index c24b6ca..21afca2 100644 --- a/po/sr@latin.po +++ b/po/sr@latin.po @@ -8072,8 +8072,8 @@ msgstr "Podešava postavke korisnika (izdanje grafičkog sučelja)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: ../src/cthulhu/messages.py:267 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Prijavite greške na „cthulhu-list@gnome.org“." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Prijavite greške na „https://groups.io/g/stormux“." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/sv.po b/po/sv.po index 1fdef07..d277500 100644 --- a/po/sv.po +++ b/po/sv.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2023-08-11 10:37+0000\n" "PO-Revision-Date: 2023-08-08 15:58+0200\n" "Last-Translator: Anders Jonsson \n" @@ -8512,9 +8512,9 @@ msgstr "Ställ in användarinställningar (användargränssnittsversion)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." msgstr "" -"Rapportera fel till cthulhu-list@gnome.org\n" +"Rapportera fel till https://groups.io/g/stormux\n" "Skicka synpunkter på översättningen till tp-sv@listor.tp-sv.se" #. Translators: Cthulhu normal speaks the text which was just deleted from a diff --git a/po/ta.po b/po/ta.po index 80bd766..3a57962 100644 --- a/po/ta.po +++ b/po/ta.po @@ -6250,8 +6250,8 @@ msgstr "ஆர்கா குறுநிரல் திரை படிப் #. Translators: this text is the description displayed when Cthulhu is #. launched from the command line and the help text is displayed. #: ../src/cthulhu/cthulhu_bin.py.in:93 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "பிழைகளை cthulhu-list@gnome.orgல் அறிக்கையிடவும்." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "பிழைகளை https://groups.io/g/stormuxல் அறிக்கையிடவும்." #. Translators: this is the description of the command line option #. '-r, --replace' which tells Cthulhu to replace any existing Cthulhu @@ -9048,13 +9048,13 @@ msgstr "" #~ "will automatically launch the preferences set up unless\n" #~ "the -n or --no-setup option is used.\n" #~ "\n" -#~ "Report bugs to cthulhu-list@gnome.org." +#~ "Report bugs to https://groups.io/g/stormux." #~ msgstr "" #~ "-n or --no-setup தேர்வு இல்லாவிடில்; பயனர் முன்னால் ஆர்காவை \n" #~ "அமைத்து இராவிடில், ஆர்கா தேர்வுகள் அமைப்பு நிரலை \n" #~ "தானியங்கியாக துவக்கிவிடும். \n" #~ "\n" -#~ "வழு அறிக்கைகளை அனுப்பcthulhu-list@gnome.org." +#~ "வழு அறிக்கைகளை அனுப்பhttps://groups.io/g/stormux." #~ msgid "Show this help message" #~ msgstr "இந்த உதவி செய்தியை காட்டவும்" diff --git a/po/te.po b/po/te.po index f84375d..ff3204c 100644 --- a/po/te.po +++ b/po/te.po @@ -3650,13 +3650,13 @@ msgid "" "will automatically launch the preferences set up unless\n" "the -n or --no-setup option is used.\n" "\n" -"Report bugs to cthulhu-list@gnome.org." +"Report bugs to https://groups.io/g/stormux." msgstr "" "వాడుకరిచేత ఓర్కా గతంలో అమర్చిఉండకపోయినప్పుడు, \n" "-n లేదా --no-setup ఐచ్చికాలు ఉపయోగించికపోతే ఓర్కా స్వయంచాలకంగా\n" "అభీష్టాల అమర్పును దించుతుంది.\n" "\n" -"దోషములను తెలియజేయు ఇ మెయిల్ అడ్రసు cthulhu-list@gnome.org" +"దోషములను తెలియజేయు ఇ మెయిల్ అడ్రసు https://groups.io/g/stormux" #. Translators: this is the description of the command line option #. '-?, --help' that is used to display usage information. @@ -9111,8 +9111,8 @@ msgstr "తగ్గించుటకు ఎడమ సూచికను, ప #~ msgstr "ఈమాక్స్‍‌స్పీక్ ఉపన్యాసం సేవలు" #~ msgid "Usage: cthulhu [OPTION...]" #~ msgstr "ఉపయోగం: ఓర్కా [ఐచ్చికం...]" -#~ msgid "Report bugs to cthulhu-list@gnome.org." -#~ msgstr "బగ్‌లను cthulhu-list@gnome.org. కు నివేదించండి." +#~ msgid "Report bugs to https://groups.io/g/stormux." +#~ msgstr "బగ్‌లను https://groups.io/g/stormux. కు నివేదించండి." #~ msgid "Invalid" #~ msgstr "Invalid" #~ msgid "invalid" diff --git a/po/tg.po b/po/tg.po index 090ab73..4fedb92 100644 --- a/po/tg.po +++ b/po/tg.po @@ -4730,7 +4730,7 @@ msgstr "" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: ../src/cthulhu/messages.py:277 -msgid "Report bugs to cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." msgstr "" #. Translators: In chat applications, it is often possible to see that a "buddy" diff --git a/po/th.po b/po/th.po index af82c37..ec42711 100644 --- a/po/th.po +++ b/po/th.po @@ -3975,8 +3975,8 @@ msgstr "" "Cthulhu จะถูกฆ่าได้" #: ../src/cthulhu/cthulhu.py:1450 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "รายงานข้อผิดพลาดไปยัง cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "รายงานข้อผิดพลาดไปยัง https://groups.io/g/stormux." #: ../src/cthulhu/cthulhu.py:1621 msgid "Welcome to Cthulhu." diff --git a/po/tr.po b/po/tr.po index 62de409..86603b1 100644 --- a/po/tr.po +++ b/po/tr.po @@ -15,7 +15,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2023-10-09 16:03+0000\n" "PO-Revision-Date: 2023-10-13 15:42+0300\n" "Last-Translator: Sabri Ünal \n" @@ -8497,8 +8497,8 @@ msgstr "Kullanıcı tercihlerini ayarla (GUI sürümü)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Hataları cthulhu-list@gnome.org adresine bildirin." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Hataları https://groups.io/g/stormux adresine bildirin." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/ug.po b/po/ug.po index 9e8ac71..ad83ab7 100644 --- a/po/ug.po +++ b/po/ug.po @@ -6201,8 +6201,8 @@ msgstr "ئوركا(cthulhu) - پروگرامما يازغىلى بولىدىغا #. Translators: this text is the description displayed when Cthulhu is #. launched from the command line and the help text is displayed. #: ../src/cthulhu/cthulhu_bin.py.in:93 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "كەمتۈكلەرنى cthulhu-list@gnome.org غا مەلۇم قىلىڭ." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "كەمتۈكلەرنى https://groups.io/g/stormux غا مەلۇم قىلىڭ." #. Translators: this is the description of the command line option #. '-r, --replace' which tells Cthulhu to replace any existing Cthulhu diff --git a/po/uk.po b/po/uk.po index f8b4093..45d8d9f 100644 --- a/po/uk.po +++ b/po/uk.po @@ -13,7 +13,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2023-08-11 17:30+0000\n" "PO-Revision-Date: 2023-08-11 21:31+0300\n" "Last-Translator: Yuri Chornoivan \n" @@ -8540,8 +8540,8 @@ msgstr "Вказати параметри користувача (графічн #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Повідомити про помилку у список розсилки cthulhu-list@gnome.org." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Повідомити про помилку у список розсилки https://groups.io/g/stormux." #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/vi.po b/po/vi.po index 4876504..2f17804 100644 --- a/po/vi.po +++ b/po/vi.po @@ -3588,8 +3588,8 @@ msgstr "" "không được dùng." #: ../src/cthulhu/cthulhu.py:517 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "Hãy thông báo lỗi nào cho ." +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "Hãy thông báo lỗi nào cho ." #. Translators: this is what Cthulhu speaks and brailles when it quits. #. diff --git a/po/zh_CN.po b/po/zh_CN.po index c440fd8..c150d5d 100644 --- a/po/zh_CN.po +++ b/po/zh_CN.po @@ -17,7 +17,7 @@ msgid "" msgstr "" "Project-Id-Version: cthulhu master\n" -"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/cthulhu/issues\n" +"Report-Msgid-Bugs-To: https://groups.io/g/stormux\n" "POT-Creation-Date: 2023-08-31 22:44+0000\n" "PO-Revision-Date: 2023-09-01 16:31+0800\n" "Last-Translator: Eni \n" @@ -8491,8 +8491,8 @@ msgstr "设定用户首选项(图形版本)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: src/cthulhu/messages.py:312 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "汇报错误至 cthulhu-list@gnome.org。" +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "汇报错误至 https://groups.io/g/stormux。" #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/po/zh_HK.po b/po/zh_HK.po index 1ead949..3551623 100644 --- a/po/zh_HK.po +++ b/po/zh_HK.po @@ -6014,8 +6014,8 @@ msgstr "設定使用者偏好設定(文字版本)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: ../src/cthulhu/messages.py:277 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "匯報錯誤至 cthulhu-list@gnome.org" +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "匯報錯誤至 https://groups.io/g/stormux" #. Translators: In chat applications, it is often possible to see that a "buddy" #. is typing currently (e.g. via a keyboard icon or status text). Some users like diff --git a/po/zh_TW.po b/po/zh_TW.po index f52256e..acea169 100644 --- a/po/zh_TW.po +++ b/po/zh_TW.po @@ -8536,8 +8536,8 @@ msgstr "設定使用者偏好設定(GUI 版本)" #. Translators: This text is the description displayed when Cthulhu is launched #. from the command line and the help text is displayed. #: ../src/cthulhu/messages.py:267 -msgid "Report bugs to cthulhu-list@gnome.org." -msgstr "匯報錯誤至 cthulhu-list@gnome.org" +msgid "Report bugs to https://groups.io/g/stormux." +msgstr "匯報錯誤至 https://groups.io/g/stormux" #. Translators: Cthulhu normal speaks the text which was just deleted from a #. document via command. Depending on the circumstances, that might be a diff --git a/pyproject.toml b/pyproject.toml index dd30b4c..c0911a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,13 +5,12 @@ build-backend = "hatchling.build" [project] name = "cthulhu" dynamic = ["version"] -description = "Fork of the Orca screen reader based on gnome-45" +description = "Fork of the Orca screen reader" readme = "README.md" requires-python = ">=3.10" license = { text = "LGPL-2.1-or-later" } dependencies = [ "pygobject>=3.18", - "python-atspi>=2.48", "brlapi; extra == 'braille'", "python-speechd; extra == 'speech'", "louis; extra == 'braille'" diff --git a/set-version.sh b/set-version.sh new file mode 100755 index 0000000..a3c9d26 --- /dev/null +++ b/set-version.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +set -euo pipefail + +scriptDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +dateInput="${1:-}" +branchInput="${2:-}" + +if [[ -z "$dateInput" ]]; then + dateInput="$(date +%Y.%m.%d)" +fi + +if [[ ! "$dateInput" =~ ^([0-9]{4}\.[0-9]{2}\.[0-9]{2})(-([A-Za-z0-9._-]+))?$ ]]; then + echo "Usage: $(basename "$0") [YYYY.MM.DD[-branch]] [branch]" >&2 + echo "Error: version must match YYYY.MM.DD or YYYY.MM.DD-branch" >&2 + exit 1 +fi + +datePart="${BASH_REMATCH[1]}" +suffixPart="${BASH_REMATCH[3]}" + +if [[ -n "$suffixPart" && -n "$branchInput" ]]; then + echo "Warning: branch argument ignored because suffix was provided in version." >&2 + branchInput="" +fi + +if [[ -z "$suffixPart" ]]; then + if [[ -z "$branchInput" ]]; then + branchInput="$(git -C "$scriptDir" branch --show-current 2>/dev/null || true)" + fi + + if [[ "$branchInput" == "HEAD" ]]; then + branchInput="" + fi + + if [[ -n "$branchInput" ]]; then + branchName="$(printf '%s' "$branchInput" | sed 's/[^A-Za-z0-9._-]/-/g')" + branchName="${branchName#-}" + branchName="${branchName%-}" + suffixPart="$branchName" + fi +fi + +pythonVersion="$datePart" +fullVersion="$datePart" +if [[ -n "$suffixPart" ]]; then + fullVersion="${fullVersion}-${suffixPart}" +fi + +if [[ ! "$fullVersion" =~ ^[0-9]{4}\.[0-9]{2}\.[0-9]{2}(-[A-Za-z0-9._-]+)?$ ]]; then + echo "Error: generated version '${fullVersion}' is invalid" >&2 + exit 1 +fi + +codeNameValue="" +if [[ -n "$suffixPart" ]]; then + codeNameValue="$suffixPart" +fi + +cthulhuVersionFile="${scriptDir}/src/cthulhu/cthulhuVersion.py" +mesonFile="${scriptDir}/meson.build" +pkgbuildFile="${scriptDir}/distro-packages/Arch-Linux/PKGBUILD" + +for path in "$cthulhuVersionFile" "$mesonFile" "$pkgbuildFile"; do + if [[ ! -f "$path" ]]; then + echo "Error: Missing file: $path" >&2 + exit 1 + fi +done + +sed -i "s/^version = \".*\"/version = \"${pythonVersion}\"/" "$cthulhuVersionFile" +if [[ -n "$codeNameValue" ]]; then + sed -i "s/^codeName = \".*\"/codeName = \"${codeNameValue}\"/" "$cthulhuVersionFile" +fi +sed -i "s/^ version: '.*',/ version: '${fullVersion}',/" "$mesonFile" +sed -i "s/^pkgver=.*/pkgver=${pythonVersion}/" "$pkgbuildFile" +sed -i "s/^pkgrel=.*/pkgrel=1/" "$pkgbuildFile" + +if ! rg -q "^version = \"${pythonVersion}\"" "$cthulhuVersionFile"; then + echo "Error: Failed to update ${cthulhuVersionFile}" >&2 + exit 1 +fi +if [[ -n "$codeNameValue" ]] && ! rg -q "^codeName = \"${codeNameValue}\"" "$cthulhuVersionFile"; then + echo "Error: Failed to update codeName in ${cthulhuVersionFile}" >&2 + exit 1 +fi +if ! rg -q "^ version: '${fullVersion}'," "$mesonFile"; then + echo "Error: Failed to update ${mesonFile}" >&2 + exit 1 +fi +if ! rg -q "^pkgver=${fullVersion}$" "$pkgbuildFile"; then + echo "Error: Failed to update ${pkgbuildFile}" >&2 + exit 1 +fi +if ! rg -q "^pkgrel=1$" "$pkgbuildFile"; then + echo "Error: Failed to reset pkgrel in ${pkgbuildFile}" >&2 + exit 1 +fi + +echo "Updated version to ${fullVersion} in:" \ + "${cthulhuVersionFile}" \ + "${mesonFile}" \ + "${pkgbuildFile}" diff --git a/sounds/default/browse_mode.wav b/sounds/default/browse_mode.wav new file mode 100644 index 0000000..c26e051 Binary files /dev/null and b/sounds/default/browse_mode.wav differ diff --git a/sounds/default/editbox.wav b/sounds/default/editbox.wav new file mode 100644 index 0000000..7e8268d Binary files /dev/null and b/sounds/default/editbox.wav differ diff --git a/sounds/meson.build b/sounds/meson.build new file mode 100644 index 0000000..8b8419a --- /dev/null +++ b/sounds/meson.build @@ -0,0 +1,7 @@ +# Install sound theme files +# Themes are installed to: {datadir}/cthulhu/sounds/{theme_name}/ + +install_subdir( + 'default', + install_dir: get_option('datadir') / 'cthulhu' / 'sounds' +) diff --git a/src/Makefile b/src/Makefile deleted file mode 100644 index f8d775a..0000000 --- a/src/Makefile +++ /dev/null @@ -1,652 +0,0 @@ -# Makefile.in generated by automake 1.18.1 from Makefile.am. -# src/Makefile. Generated from Makefile.in by configure. - -# Copyright (C) 1994-2025 Free Software Foundation, Inc. - -# This Makefile.in is free software; the Free Software Foundation -# gives unlimited permission to copy and/or distribute it, -# with or without modifications, as long as this notice is preserved. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY, to the extent permitted by law; without -# even the implied warranty of MERCHANTABILITY or FITNESS FOR A -# PARTICULAR PURPOSE. - - - -am__is_gnu_make = { \ - if test -z '$(MAKELEVEL)'; then \ - false; \ - elif test -n '$(MAKE_HOST)'; then \ - true; \ - elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \ - true; \ - else \ - false; \ - fi; \ -} -am__make_running_with_option = \ - case $${target_option-} in \ - ?) ;; \ - *) echo "am__make_running_with_option: internal error: invalid" \ - "target option '$${target_option-}' specified" >&2; \ - exit 1;; \ - esac; \ - has_opt=no; \ - sane_makeflags=$$MAKEFLAGS; \ - if $(am__is_gnu_make); then \ - sane_makeflags=$$MFLAGS; \ - else \ - case $$MAKEFLAGS in \ - *\\[\ \ ]*) \ - bs=\\; \ - sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \ - | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \ - esac; \ - fi; \ - skip_next=no; \ - strip_trailopt () \ - { \ - flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \ - }; \ - for flg in $$sane_makeflags; do \ - test $$skip_next = yes && { skip_next=no; continue; }; \ - case $$flg in \ - *=*|--*) continue;; \ - -*I) strip_trailopt 'I'; skip_next=yes;; \ - -*I?*) strip_trailopt 'I';; \ - -*O) strip_trailopt 'O'; skip_next=yes;; \ - -*O?*) strip_trailopt 'O';; \ - -*l) strip_trailopt 'l'; skip_next=yes;; \ - -*l?*) strip_trailopt 'l';; \ - -[dEDm]) skip_next=yes;; \ - -[JT]) skip_next=yes;; \ - esac; \ - case $$flg in \ - *$$target_option*) has_opt=yes; break;; \ - esac; \ - done; \ - test $$has_opt = yes -am__make_dryrun = (target_option=n; $(am__make_running_with_option)) -am__make_keepgoing = (target_option=k; $(am__make_running_with_option)) -am__rm_f = rm -f $(am__rm_f_notfound) -am__rm_rf = rm -rf $(am__rm_f_notfound) -pkgdatadir = $(datadir)/cthulhu -pkgincludedir = $(includedir)/cthulhu -pkglibdir = $(libdir)/cthulhu -pkglibexecdir = $(libexecdir)/cthulhu -am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd -install_sh_DATA = $(install_sh) -c -m 644 -install_sh_PROGRAM = $(install_sh) -c -install_sh_SCRIPT = $(install_sh) -c -INSTALL_HEADER = $(INSTALL_DATA) -transform = $(program_transform_name) -NORMAL_INSTALL = : -PRE_INSTALL = : -POST_INSTALL = : -NORMAL_UNINSTALL = : -PRE_UNINSTALL = : -POST_UNINSTALL = : -build_triplet = x86_64-pc-linux-gnu -host_triplet = x86_64-pc-linux-gnu -subdir = src -ACLOCAL_M4 = $(top_srcdir)/aclocal.m4 -am__aclocal_m4_deps = $(top_srcdir)/m4/build-to-host.m4 \ - $(top_srcdir)/m4/gettext.m4 $(top_srcdir)/m4/host-cpu-c-abi.m4 \ - $(top_srcdir)/m4/iconv.m4 $(top_srcdir)/m4/intlmacosx.m4 \ - $(top_srcdir)/m4/lib-ld.m4 $(top_srcdir)/m4/lib-link.m4 \ - $(top_srcdir)/m4/lib-prefix.m4 $(top_srcdir)/m4/nls.m4 \ - $(top_srcdir)/m4/po.m4 $(top_srcdir)/m4/progtest.m4 \ - $(top_srcdir)/acinclude.m4 $(top_srcdir)/configure.ac -am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ - $(ACLOCAL_M4) -DIST_COMMON = $(srcdir)/Makefile.am $(am__DIST_COMMON) -mkinstalldirs = $(install_sh) -d -CONFIG_CLEAN_FILES = -CONFIG_CLEAN_VPATH_FILES = -AM_V_P = $(am__v_P_$(V)) -am__v_P_ = $(am__v_P_$(AM_DEFAULT_VERBOSITY)) -am__v_P_0 = false -am__v_P_1 = : -AM_V_GEN = $(am__v_GEN_$(V)) -am__v_GEN_ = $(am__v_GEN_$(AM_DEFAULT_VERBOSITY)) -am__v_GEN_0 = @echo " GEN " $@; -am__v_GEN_1 = -AM_V_at = $(am__v_at_$(V)) -am__v_at_ = $(am__v_at_$(AM_DEFAULT_VERBOSITY)) -am__v_at_0 = @ -am__v_at_1 = -SOURCES = -DIST_SOURCES = -RECURSIVE_TARGETS = all-recursive check-recursive cscopelist-recursive \ - ctags-recursive dvi-recursive html-recursive info-recursive \ - install-data-recursive install-dvi-recursive \ - install-exec-recursive install-html-recursive \ - install-info-recursive install-pdf-recursive \ - install-ps-recursive install-recursive installcheck-recursive \ - installdirs-recursive pdf-recursive ps-recursive \ - tags-recursive uninstall-recursive -am__can_run_installinfo = \ - case $$AM_UPDATE_INFO_DIR in \ - n|no|NO) false;; \ - *) (install-info --version) >/dev/null 2>&1;; \ - esac -RECURSIVE_CLEAN_TARGETS = mostlyclean-recursive clean-recursive \ - distclean-recursive maintainer-clean-recursive -am__recursive_targets = \ - $(RECURSIVE_TARGETS) \ - $(RECURSIVE_CLEAN_TARGETS) \ - $(am__extra_recursive_targets) -AM_RECURSIVE_TARGETS = $(am__recursive_targets:-recursive=) TAGS CTAGS \ - distdir distdir-am -am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP) -# Read a list of newline-separated strings from the standard input, -# and print each of them once, without duplicates. Input order is -# *not* preserved. -am__uniquify_input = $(AWK) '\ - BEGIN { nonempty = 0; } \ - { items[$$0] = 1; nonempty = 1; } \ - END { if (nonempty) { for (i in items) print i; }; } \ -' -# Make sure the list of sources is unique. This is necessary because, -# e.g., the same source file might be shared among _SOURCES variables -# for different programs/libraries. -am__define_uniq_tagged_files = \ - list='$(am__tagged_files)'; \ - unique=`for i in $$list; do \ - if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \ - done | $(am__uniquify_input)` -DIST_SUBDIRS = $(SUBDIRS) -am__DIST_COMMON = $(srcdir)/Makefile.in -DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) -am__relativize = \ - dir0=`pwd`; \ - sed_first='s,^\([^/]*\)/.*$$,\1,'; \ - sed_rest='s,^[^/]*/*,,'; \ - sed_last='s,^.*/\([^/]*\)$$,\1,'; \ - sed_butlast='s,/*[^/]*$$,,'; \ - while test -n "$$dir1"; do \ - first=`echo "$$dir1" | sed -e "$$sed_first"`; \ - if test "$$first" != "."; then \ - if test "$$first" = ".."; then \ - dir2=`echo "$$dir0" | sed -e "$$sed_last"`/"$$dir2"; \ - dir0=`echo "$$dir0" | sed -e "$$sed_butlast"`; \ - else \ - first2=`echo "$$dir2" | sed -e "$$sed_first"`; \ - if test "$$first2" = "$$first"; then \ - dir2=`echo "$$dir2" | sed -e "$$sed_rest"`; \ - else \ - dir2="../$$dir2"; \ - fi; \ - dir0="$$dir0"/"$$first"; \ - fi; \ - fi; \ - dir1=`echo "$$dir1" | sed -e "$$sed_rest"`; \ - done; \ - reldir="$$dir2" -ACLOCAL = ${SHELL} '/home/storm/devel/cthulhu/missing' aclocal-1.18 -AMTAR = $${TAR-tar} -AM_DEFAULT_VERBOSITY = 1 -ATKBRIDGE_CFLAGS = -I/usr/include/at-spi2-atk/2.0 -I/usr/include/at-spi-2.0 -I/usr/include/libmount -I/usr/include/blkid -I/usr/include/atk-1.0 -I/usr/include/dbus-1.0 -I/usr/lib/dbus-1.0/include -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -I/usr/include/sysprof-6 -pthread -ATKBRIDGE_LIBS = -latk-bridge-2.0 -ATSPI2_CFLAGS = -I/usr/include/at-spi-2.0 -I/usr/include/dbus-1.0 -I/usr/lib/dbus-1.0/include -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -I/usr/include/libmount -I/usr/include/blkid -I/usr/include/sysprof-6 -pthread -ATSPI2_LIBS = -latspi -ldbus-1 -lglib-2.0 -AUTOCONF = ${SHELL} '/home/storm/devel/cthulhu/missing' autoconf -AUTOHEADER = ${SHELL} '/home/storm/devel/cthulhu/missing' autoheader -AUTOMAKE = ${SHELL} '/home/storm/devel/cthulhu/missing' automake-1.18 -AWK = gawk -CC = gcc -CCDEPMODE = depmode=none -CFLAGS = -g -O2 -CPP = gcc -E -CPPFLAGS = -CSCOPE = cscope -CTAGS = ctags -CYGPATH_W = echo -DEFS = -DPACKAGE_NAME=\"cthulhu\" -DPACKAGE_TARNAME=\"cthulhu\" -DPACKAGE_VERSION=\"2025.08.06\" -DPACKAGE_STRING=\"cthulhu\ 2025.08.06\" -DPACKAGE_BUGREPORT=\"https://gitlab.gnome.org/GNOME/cthulhu/-/issues/\" -DPACKAGE_URL=\"\" -DPACKAGE=\"cthulhu\" -DVERSION=\"2025.08.06\" -DENABLE_NLS=1 -DHAVE_GETTEXT=1 -DHAVE_DCGETTEXT=1 -DGETTEXT_PACKAGE=\"cthulhu\" -DEPDIR = .deps -DESIRED_LINGUAS = $(ALL_LINGUAS) -ECHO_C = -ECHO_N = -n -ECHO_T = -ETAGS = etags -EXEEXT = -GETTEXT_MACRO_VERSION = 0.24 -GETTEXT_PACKAGE = cthulhu -GMSGFMT = /usr/bin/msgfmt -GMSGFMT_015 = /usr/bin/msgfmt -GSTREAMER_CFLAGS = -I/usr/include/gstreamer-1.0 -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -I/usr/include/sysprof-6 -pthread -GSTREAMER_LIBS = -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -INSTALL = /usr/bin/install -c -INSTALL_DATA = ${INSTALL} -m 644 -INSTALL_PROGRAM = ${INSTALL} -INSTALL_SCRIPT = ${INSTALL} -INSTALL_STRIP_PROGRAM = $(install_sh) -c -s -INTLLIBS = -INTL_MACOSX_LIBS = -LDFLAGS = -LIBICONV = -liconv -LIBINTL = -LIBOBJS = -LIBPEAS_CFLAGS = -I/usr/include/libpeas-1.0 -I/usr/include/libmount -I/usr/include/blkid -I/usr/include/gobject-introspection-1.0 -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -I/usr/include/sysprof-6 -pthread -LIBPEAS_LIBS = -lpeas-1.0 -Wl,--export-dynamic -lgio-2.0 -lgmodule-2.0 -pthread -lgirepository-1.0 -lgobject-2.0 -lglib-2.0 -LIBS = -LOUIS_TABLE_DIR = /usr/share/liblouis/tables -LTLIBICONV = -liconv -LTLIBINTL = -LTLIBOBJS = -MAINT = -MAKEINFO = ${SHELL} '/home/storm/devel/cthulhu/missing' makeinfo -MKDIR_P = /usr/bin/mkdir -p -MSGFMT = /usr/bin/msgfmt -MSGMERGE = /usr/bin/msgmerge -MSGMERGE_FOR_MSGFMT_OPTION = --for-msgfmt -OBJEXT = o -PACKAGE = cthulhu -PACKAGE_BUGREPORT = https://gitlab.gnome.org/GNOME/cthulhu/-/issues/ -PACKAGE_NAME = cthulhu -PACKAGE_STRING = cthulhu 2025.08.06 -PACKAGE_TARNAME = cthulhu -PACKAGE_URL = -PACKAGE_VERSION = 2025.08.06 -PATH_SEPARATOR = : -PKG_CONFIG = /usr/bin/pkg-config -PKG_CONFIG_LIBDIR = -PKG_CONFIG_PATH = -PLATFORM_PATH = :/usr/bin:/usr/sbin:/bin -POSUB = po -PYGOBJECT_CFLAGS = -I/usr/include/pygobject-3.0 -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -I/usr/include/sysprof-6 -pthread -PYGOBJECT_LIBS = -lgobject-2.0 -lglib-2.0 -PYTHON = /home/storm/.pyenv/shims/python -PYTHON_EXEC_PREFIX = ${exec_prefix} -PYTHON_PLATFORM = linux -PYTHON_PREFIX = ${prefix} -PYTHON_VERSION = 3.13 -REVISION = 89df899 -SED = /usr/bin/sed -SET_MAKE = -SHELL = /bin/sh -STRIP = -USE_NLS = yes -VERSION = 2025.08.06 -XGETTEXT = /usr/bin/xgettext -XGETTEXT_015 = /usr/bin/xgettext -XGETTEXT_EXTRA_OPTIONS = -abs_builddir = /home/storm/devel/cthulhu/src -abs_srcdir = /home/storm/devel/cthulhu/src -abs_top_builddir = /home/storm/devel/cthulhu -abs_top_srcdir = /home/storm/devel/cthulhu -ac_ct_CC = gcc -am__include = include -am__leading_dot = . -am__quote = -am__rm_f_notfound = -am__tar = tar --format=ustar -chf - "$$tardir" -am__untar = tar -xf - -am__xargs_n = xargs -n -bindir = ${exec_prefix}/bin -build = x86_64-pc-linux-gnu -build_alias = -build_cpu = x86_64 -build_os = linux-gnu -build_vendor = pc -builddir = . -datadir = ${datarootdir} -datarootdir = ${prefix}/share -docdir = ${datarootdir}/doc/${PACKAGE_TARNAME} -dvidir = ${docdir} -exec_prefix = ${prefix} -host = x86_64-pc-linux-gnu -host_alias = -host_cpu = x86_64 -host_os = linux-gnu -host_vendor = pc -htmldir = ${docdir} -includedir = ${prefix}/include -infodir = ${datarootdir}/info -install_sh = ${SHELL} /home/storm/devel/cthulhu/install-sh -libdir = ${exec_prefix}/lib -libexecdir = ${exec_prefix}/libexec -localedir = ${datarootdir}/locale -localedir_c = "/home/storm/.local/share/locale" -localedir_c_make = \"$(localedir)\" -localstatedir = /home/storm/.local/var -mandir = ${datarootdir}/man -mkdir_p = $(MKDIR_P) -oldincludedir = /usr/include -pdfdir = ${docdir} -pkgpyexecdir = ${pyexecdir}/cthulhu -pkgpythondir = ${pythondir}/cthulhu -prefix = /home/storm/.local -program_transform_name = s,x,x, -psdir = ${docdir} -pyexecdir = ${PYTHON_EXEC_PREFIX}/lib/python3.13/site-packages -pythondir = ${PYTHON_PREFIX}/lib/python3.13/site-packages -runstatedir = ${localstatedir}/run -sbindir = ${exec_prefix}/sbin -sharedstatedir = ${prefix}/com -srcdir = . -sysconfdir = /home/storm/.local/etc -target_alias = -top_build_prefix = ../ -top_builddir = .. -top_srcdir = .. -SUBDIRS = cthulhu -all: all-recursive - -.SUFFIXES: -$(srcdir)/Makefile.in: $(srcdir)/Makefile.am $(am__configure_deps) - @for dep in $?; do \ - case '$(am__configure_deps)' in \ - *$$dep*) \ - ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \ - && { if test -f $@; then exit 0; else break; fi; }; \ - exit 1;; \ - esac; \ - done; \ - echo ' cd $(top_srcdir) && $(AUTOMAKE) --gnu src/Makefile'; \ - $(am__cd) $(top_srcdir) && \ - $(AUTOMAKE) --gnu src/Makefile -Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status - @case '$?' in \ - *config.status*) \ - cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \ - *) \ - echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \ - cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \ - esac; - -$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES) - cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh - -$(top_srcdir)/configure: $(am__configure_deps) - cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh -$(ACLOCAL_M4): $(am__aclocal_m4_deps) - cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh -$(am__aclocal_m4_deps): - -# This directory's subdirectories are mostly independent; you can cd -# into them and run 'make' without going through this Makefile. -# To change the values of 'make' variables: instead of editing Makefiles, -# (1) if the variable is set in 'config.status', edit 'config.status' -# (which will cause the Makefiles to be regenerated when you run 'make'); -# (2) otherwise, pass the desired values on the 'make' command line. -$(am__recursive_targets): - @fail=; \ - if $(am__make_keepgoing); then \ - failcom='fail=yes'; \ - else \ - failcom='exit 1'; \ - fi; \ - dot_seen=no; \ - target=`echo $@ | sed s/-recursive//`; \ - case "$@" in \ - distclean-* | maintainer-clean-*) list='$(DIST_SUBDIRS)' ;; \ - *) list='$(SUBDIRS)' ;; \ - esac; \ - for subdir in $$list; do \ - echo "Making $$target in $$subdir"; \ - if test "$$subdir" = "."; then \ - dot_seen=yes; \ - local_target="$$target-am"; \ - else \ - local_target="$$target"; \ - fi; \ - ($(am__cd) $$subdir && $(MAKE) $(AM_MAKEFLAGS) $$local_target) \ - || eval $$failcom; \ - done; \ - if test "$$dot_seen" = "no"; then \ - $(MAKE) $(AM_MAKEFLAGS) "$$target-am" || exit 1; \ - fi; test -z "$$fail" - -ID: $(am__tagged_files) - $(am__define_uniq_tagged_files); mkid -fID $$unique -tags: tags-recursive -TAGS: tags - -tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files) - set x; \ - here=`pwd`; \ - if ($(ETAGS) --etags-include --version) >/dev/null 2>&1; then \ - include_option=--etags-include; \ - empty_fix=.; \ - else \ - include_option=--include; \ - empty_fix=; \ - fi; \ - list='$(SUBDIRS)'; for subdir in $$list; do \ - if test "$$subdir" = .; then :; else \ - test ! -f $$subdir/TAGS || \ - set "$$@" "$$include_option=$$here/$$subdir/TAGS"; \ - fi; \ - done; \ - $(am__define_uniq_tagged_files); \ - shift; \ - if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \ - test -n "$$unique" || unique=$$empty_fix; \ - if test $$# -gt 0; then \ - $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \ - "$$@" $$unique; \ - else \ - $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \ - $$unique; \ - fi; \ - fi -ctags: ctags-recursive - -CTAGS: ctags -ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files) - $(am__define_uniq_tagged_files); \ - test -z "$(CTAGS_ARGS)$$unique" \ - || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \ - $$unique - -GTAGS: - here=`$(am__cd) $(top_builddir) && pwd` \ - && $(am__cd) $(top_srcdir) \ - && gtags -i $(GTAGS_ARGS) "$$here" -cscopelist: cscopelist-recursive - -cscopelist-am: $(am__tagged_files) - list='$(am__tagged_files)'; \ - case "$(srcdir)" in \ - [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \ - *) sdir=$(subdir)/$(srcdir) ;; \ - esac; \ - for i in $$list; do \ - if test -f "$$i"; then \ - echo "$(subdir)/$$i"; \ - else \ - echo "$$sdir/$$i"; \ - fi; \ - done >> $(top_builddir)/cscope.files - -distclean-tags: - -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags - -distdir: $(BUILT_SOURCES) - $(MAKE) $(AM_MAKEFLAGS) distdir-am - -distdir-am: $(DISTFILES) - @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ - topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ - list='$(DISTFILES)'; \ - dist_files=`for file in $$list; do echo $$file; done | \ - sed -e "s|^$$srcdirstrip/||;t" \ - -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \ - case $$dist_files in \ - */*) $(MKDIR_P) `echo "$$dist_files" | \ - sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \ - sort -u` ;; \ - esac; \ - for file in $$dist_files; do \ - if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \ - if test -d $$d/$$file; then \ - dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \ - if test -d "$(distdir)/$$file"; then \ - find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ - fi; \ - if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \ - cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \ - find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ - fi; \ - cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \ - else \ - test -f "$(distdir)/$$file" \ - || cp -p $$d/$$file "$(distdir)/$$file" \ - || exit 1; \ - fi; \ - done - @list='$(DIST_SUBDIRS)'; for subdir in $$list; do \ - if test "$$subdir" = .; then :; else \ - $(am__make_dryrun) \ - || test -d "$(distdir)/$$subdir" \ - || $(MKDIR_P) "$(distdir)/$$subdir" \ - || exit 1; \ - dir1=$$subdir; dir2="$(distdir)/$$subdir"; \ - $(am__relativize); \ - new_distdir=$$reldir; \ - dir1=$$subdir; dir2="$(top_distdir)"; \ - $(am__relativize); \ - new_top_distdir=$$reldir; \ - echo " (cd $$subdir && $(MAKE) $(AM_MAKEFLAGS) top_distdir="$$new_top_distdir" distdir="$$new_distdir" \\"; \ - echo " am__remove_distdir=: am__skip_length_check=: am__skip_mode_fix=: distdir)"; \ - ($(am__cd) $$subdir && \ - $(MAKE) $(AM_MAKEFLAGS) \ - top_distdir="$$new_top_distdir" \ - distdir="$$new_distdir" \ - am__remove_distdir=: \ - am__skip_length_check=: \ - am__skip_mode_fix=: \ - distdir) \ - || exit 1; \ - fi; \ - done -check-am: all-am -check: check-recursive -all-am: Makefile -installdirs: installdirs-recursive -installdirs-am: -install: install-recursive -install-exec: install-exec-recursive -install-data: install-data-recursive -uninstall: uninstall-recursive - -install-am: all-am - @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am - -installcheck: installcheck-recursive -install-strip: - if test -z '$(STRIP)'; then \ - $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ - install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ - install; \ - else \ - $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ - install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ - "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \ - fi -mostlyclean-generic: - -clean-generic: - -distclean-generic: - -$(am__rm_f) $(CONFIG_CLEAN_FILES) - -test . = "$(srcdir)" || $(am__rm_f) $(CONFIG_CLEAN_VPATH_FILES) - -maintainer-clean-generic: - @echo "This command is intended for maintainers to use" - @echo "it deletes files that may require special tools to rebuild." -clean: clean-recursive - -clean-am: clean-generic mostlyclean-am - -distclean: distclean-recursive - -rm -f Makefile -distclean-am: clean-am distclean-generic distclean-tags - -dvi: dvi-recursive - -dvi-am: - -html: html-recursive - -html-am: - -info: info-recursive - -info-am: - -install-data-am: - -install-dvi: install-dvi-recursive - -install-dvi-am: - -install-exec-am: - -install-html: install-html-recursive - -install-html-am: - -install-info: install-info-recursive - -install-info-am: - -install-man: - -install-pdf: install-pdf-recursive - -install-pdf-am: - -install-ps: install-ps-recursive - -install-ps-am: - -installcheck-am: - -maintainer-clean: maintainer-clean-recursive - -rm -f Makefile -maintainer-clean-am: distclean-am maintainer-clean-generic - -mostlyclean: mostlyclean-recursive - -mostlyclean-am: mostlyclean-generic - -pdf: pdf-recursive - -pdf-am: - -ps: ps-recursive - -ps-am: - -uninstall-am: - -.MAKE: $(am__recursive_targets) install-am install-strip - -.PHONY: $(am__recursive_targets) CTAGS GTAGS TAGS all all-am check \ - check-am clean clean-generic cscopelist-am ctags ctags-am \ - distclean distclean-generic distclean-tags distdir dvi dvi-am \ - html html-am info info-am install install-am install-data \ - install-data-am install-dvi install-dvi-am install-exec \ - install-exec-am install-html install-html-am install-info \ - install-info-am install-man install-pdf install-pdf-am \ - install-ps install-ps-am install-strip installcheck \ - installcheck-am installdirs installdirs-am maintainer-clean \ - maintainer-clean-generic mostlyclean mostlyclean-generic pdf \ - pdf-am ps ps-am tags tags-am uninstall uninstall-am - -.PRECIOUS: Makefile - - -# Tell versions [3.59,3.63) of GNU make to not export all variables. -# Otherwise a system limit (for SysV at least) may be exceeded. -.NOEXPORT: - -# Tell GNU make to disable its built-in pattern rules. -%:: %,v -%:: RCS/%,v -%:: RCS/% -%:: s.% -%:: SCCS/s.% diff --git a/src/cthulhu.py b/src/cthulhu.py index 9896840..c7fc998 100644 --- a/src/cthulhu.py +++ b/src/cthulhu.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu import argparse import gi diff --git a/src/cthulhu/__init__.py b/src/cthulhu/__init__.py index adc42ce..d893644 100644 --- a/src/cthulhu/__init__.py +++ b/src/cthulhu/__init__.py @@ -20,12 +20,10 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Cthulhu Screen Reader""" __copyright__ = "Copyright (c) 2005-2006 Sun Microsystems Inc." __license__ = "LGPL" - - diff --git a/src/cthulhu/acss.py b/src/cthulhu/acss.py index 6e762f1..1fb62b3 100644 --- a/src/cthulhu/acss.py +++ b/src/cthulhu/acss.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """ACSS --- Aural CSS. diff --git a/src/cthulhu/action_presenter.py b/src/cthulhu/action_presenter.py index c61b523..d3314df 100644 --- a/src/cthulhu/action_presenter.py +++ b/src/cthulhu/action_presenter.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Module for performing accessible actions via a list""" @@ -43,12 +43,13 @@ gi.require_version("Gtk", "3.0") from gi.repository import Gdk, GLib, Gtk from . import cmdnames +from . import dbus_service from . import debug +from . import focus_manager from . import guilabels from . import input_event from . import keybindings from . import messages -from . import cthulhu from . import cthulhu_state from . import script_manager from .ax_object import AXObject @@ -62,41 +63,26 @@ class ActionList(Gtk.Window): """Window containing a list of accessible actions.""" def __init__(self, presenter: ActionPresenter): - super().__init__() + super().__init__(window_position=Gtk.WindowPosition.MOUSE, transient_for=None) self._presenter = presenter - self._actions = [] self._setup_gui() def _setup_gui(self) -> None: """Sets up the GUI for the actions list.""" - + self.set_title(guilabels.KB_GROUP_ACTIONS) - self.set_modal(True) self.set_decorated(False) - self.set_skip_taskbar_hint(True) - self.set_skip_pager_hint(True) - self.set_type_hint(Gdk.WindowTypeHint.DIALOG) - # Note: set_window_position is deprecated, using move() instead - self.move(100, 100) # Position window at reasonable location - self.set_default_size(400, 300) - # Create scrolled window - scrolled = Gtk.ScrolledWindow() - scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) - self.add(scrolled) - - # Create list box self._listbox = Gtk.ListBox() self._listbox.set_selection_mode(Gtk.SelectionMode.SINGLE) self._listbox.connect("row-activated", self._on_row_activated) - scrolled.add(self._listbox) + self._listbox.set_margin_top(5) + self._listbox.set_margin_bottom(5) + self.add(self._listbox) - # Connect key events self.connect("key-press-event", self._on_key_press) self.connect("destroy", self._on_destroy) - self.show_all() - def _on_key_press(self, widget, event) -> bool: """Handles key press events.""" @@ -107,37 +93,37 @@ class ActionList(Gtk.Window): def _on_row_activated(self, listbox, row) -> None: """Handles row activation (Enter or double-click).""" - - if row is not None: - action_index = row.get_index() - if 0 <= action_index < len(self._actions): - action_name = self._actions[action_index] - self._presenter._perform_action(action_name) + + action_name = getattr(row, "_action_name", None) + if action_name: + self._presenter._perform_action(action_name) def _on_destroy(self, widget) -> None: """Handles window destruction.""" - + GLib.idle_add(self._presenter._clear_gui_and_restore_focus) - def populate_actions(self, actions: list[str]) -> None: + def populate_actions(self, actions: dict[str, str] | list[str]) -> None: """Populates the list with accessible actions.""" - - self._actions = actions - + + if isinstance(actions, dict): + items = list(actions.items()) + else: + items = [(action, action) for action in actions] + # Clear existing items for child in self._listbox.get_children(): self._listbox.remove(child) # Add actions to list - for action in actions: - label = Gtk.Label(label=action) - label.set_xalign(0.0) # Left align - label.set_margin_left(10) - label.set_margin_right(10) - label.set_margin_top(5) - label.set_margin_bottom(5) - - self._listbox.add(label) + for action_name, label_text in items: + row = Gtk.ListBoxRow() + label = Gtk.Label(label=label_text, xalign=0) + label.set_margin_start(10) + label.set_margin_end(10) + row.add(label) + setattr(row, "_action_name", action_name) + self._listbox.add(row) # Select first item if actions: @@ -146,7 +132,12 @@ class ActionList(Gtk.Window): self._listbox.select_row(first_row) first_row.grab_focus() + def show_gui(self) -> None: + """Shows the window.""" + self.show_all() + self.present_with_time(time.time()) + self._listbox.grab_focus() class ActionPresenter: @@ -163,6 +154,11 @@ class ActionPresenter: self._handlers = self.get_handlers(True) # _bindings will be initialized lazily in get_bindings() + msg = "ACTION PRESENTER: Registering D-Bus commands." + debug.printMessage(debug.LEVEL_INFO, msg, True) + controller = dbus_service.get_remote_controller() + controller.register_decorated_module("ActionPresenter", self) + def get_handlers(self, refresh: bool = False) -> dict: """Returns a dictionary of input event handlers.""" @@ -223,18 +219,56 @@ class ActionPresenter: reason = "Action Presenter list is being destroyed" app = AXObject.get_application(self._obj) - script = script_manager.getManager().getScript(app, self._obj) - script_manager.getManager().setActiveScript(script, reason) + script = script_manager.get_manager().get_script(app, self._obj) + script_manager.get_manager().set_active_script(script, reason) - # Update Cthulhu state - cthulhu_state.activeWindow = self._window - cthulhu_state.locusOfFocus = self._obj + manager = focus_manager.get_manager() + manager.clear_state(reason) + manager.set_active_window(self._window) + manager.set_locus_of_focus(None, self._obj) + + def _present_message(self, script, full_message, brief_message=None, notify_user=True) -> None: + """Presents a message using the provided script or the active script.""" + + if not notify_user: + return + + if script is not None: + script.presentMessage(full_message, brief_message) + return + + active_script = cthulhu_state.activeScript + if active_script is not None: + active_script.presentMessage(full_message, brief_message) + return + + msg = "ACTION PRESENTER: Unable to present message (no script)." + debug.printMessage(debug.LEVEL_INFO, msg, True) def _clear_gui_and_restore_focus(self) -> None: """Clears the GUI reference and then restores focus.""" self._gui = None + GLib.timeout_add(150, self._maybe_restore_focus) + + def _maybe_restore_focus(self) -> bool: + """Restores focus unless it already moved to another object in the target app.""" + + if not AXObject.is_valid(self._obj): + return False + + manager = focus_manager.get_manager() + current_focus = manager.get_locus_of_focus() + if current_focus and AXObject.is_valid(current_focus): + target_app = AXObject.get_application(self._obj) + focus_app = AXObject.get_application(current_focus) + if target_app and focus_app == target_app and current_focus != self._obj: + tokens = ["ACTION PRESENTER: Skipping focus restore; focus now on", current_focus] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + return False + self._restore_focus() + return False def _perform_action(self, action: str) -> None: """Attempts to perform the named action.""" @@ -252,7 +286,7 @@ class ActionPresenter: # Use idle_add for asynchronous destruction to allow action to complete GLib.idle_add(self._gui.destroy) - def present_with_time(self, obj, start_time: float) -> bool: + def present_with_time(self, obj, start_time: float, script=None, notify_user=True) -> bool: """Presents accessible actions for the given object with timing.""" try: @@ -264,31 +298,40 @@ class ActionPresenter: return False if obj is None: - debug.printMessage(debug.LEVEL_INFO, "ACTION PRESENTER: obj is None, using locusOfFocus", True) - obj = cthulhu_state.locusOfFocus - - if obj is None: - debug.printMessage(debug.LEVEL_INFO, "ACTION PRESENTER: No object found, presenting NO_ACCESSIBLE_ACTIONS", True) - full_message = messages.NO_ACCESSIBLE_ACTIONS - cthulhu.presentMessage(full_message) - return False + msg = "ACTION PRESENTER: No object found, presenting LOCATION_NOT_FOUND" + debug.printMessage(debug.LEVEL_INFO, msg, True) + full_message = messages.LOCATION_NOT_FOUND_FULL + brief_message = messages.LOCATION_NOT_FOUND_BRIEF + self._present_message(script, full_message, brief_message, notify_user) + return True debug.printMessage(debug.LEVEL_INFO, f"ACTION PRESENTER: Getting actions for object: {obj}", True) - actions = AXObject.get_action_names(obj) - debug.printMessage(debug.LEVEL_INFO, f"ACTION PRESENTER: Found {len(actions) if actions else 0} actions: {actions}", True) + actions = {} + for i in range(AXObject.get_n_actions(obj)): + name = AXObject.get_action_name(obj, i) + if not name: + continue + localized_name = AXObject.get_action_localized_name(obj, i) + description = AXObject.get_action_description(obj, i) + actions[name] = localized_name or description or name + + debug.printMessage(debug.LEVEL_INFO, f"ACTION PRESENTER: Found {len(actions)} actions: {actions}", True) if not actions: - debug.printMessage(debug.LEVEL_INFO, "ACTION PRESENTER: No actions found, presenting NO_ACCESSIBLE_ACTIONS", True) - full_message = messages.NO_ACCESSIBLE_ACTIONS - cthulhu.presentMessage(full_message) - return False + msg = "ACTION PRESENTER: No actions found, presenting NO_ACTIONS_FOUND_ON" + debug.printMessage(debug.LEVEL_INFO, msg, True) + name = AXObject.get_name(obj) or AXUtilities.get_localized_role_name(obj) + full_message = messages.NO_ACTIONS_FOUND_ON % name + self._present_message(script, full_message, notify_user=notify_user) + return True debug.printMessage(debug.LEVEL_INFO, "ACTION PRESENTER: Creating GUI", True) self._obj = obj - self._window = cthulhu_state.activeWindow + self._window = focus_manager.get_manager().get_active_window() self._gui = ActionList(self) self._gui.populate_actions(actions) + self._gui.show_gui() debug.printMessage(debug.LEVEL_INFO, "ACTION PRESENTER: GUI created successfully", True) return True @@ -298,7 +341,13 @@ class ActionPresenter: debug.printMessage(debug.LEVEL_INFO, f"ACTION PRESENTER: Traceback: {traceback.format_exc()}", True) return False - def show_actions_list(self, script: script.Script = None, input_event: input_event.InputEvent = None) -> bool: + @dbus_service.command + def show_actions_list( + self, + script: script.Script = None, + input_event: input_event.InputEvent = None, + notify_user: bool = True + ) -> bool: """Shows the accessible actions list.""" try: @@ -306,12 +355,19 @@ class ActionPresenter: debug.printMessage(debug.LEVEL_INFO, msg, True) start_time = time.time() - - # Get object from input event if available, otherwise let present_with_time handle it - obj = input_event.getObject() if input_event else None + + obj = None + if input_event is not None: + obj = input_event.get_object() debug.printMessage(debug.LEVEL_INFO, f"ACTION PRESENTER: Object from input_event: {obj}", True) - - result = self.present_with_time(obj, start_time) + + if obj is None: + manager = focus_manager.get_manager() + _mode, obj = manager.get_active_mode_and_object_of_interest() + if obj is None: + obj = manager.get_locus_of_focus() + + result = self.present_with_time(obj, start_time, script, notify_user) debug.printMessage(debug.LEVEL_INFO, f"ACTION PRESENTER: show_actions_list returning: {result}", True) return result except Exception as e: @@ -325,4 +381,4 @@ _presenter = ActionPresenter() def getPresenter() -> ActionPresenter: """Returns the Action Presenter singleton.""" - return _presenter \ No newline at end of file + return _presenter diff --git a/src/cthulhu/ax_collection.py b/src/cthulhu/ax_collection.py index f3251a2..e20aa50 100644 --- a/src/cthulhu/ax_collection.py +++ b/src/cthulhu/ax_collection.py @@ -1,9 +1,7 @@ -#!/usr/bin/env python3 +# Utilities for obtaining objects via the collection interface. # -# Copyright (c) 2024 Stormux -# Copyright (c) 2010-2012 The Orca Team -# Copyright (c) 2012 Igalia, S.L. -# Copyright (c) 2005-2010 Sun Microsystems Inc. +# Copyright 2023 Igalia, S.L. +# Author: Joanmarie Diggs # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -19,19 +17,11 @@ # License along with this library; if not, write to the # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. -# -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca -""" -Utilities for obtaining objects via the collection interface. -These utilities are app-type- and toolkit-agnostic. Utilities that might have -different implementations or results depending on the type of app (e.g. terminal, -chat, web) or toolkit (e.g. Qt, Gtk) should be in script_utilities.py file(s). +# pylint: disable=wrong-import-position +# pylint: disable=too-many-positional-arguments -N.B. There are currently utilities that should never have custom implementations -that live in script_utilities.py files. These will be moved over time. -""" +"""Utilities for obtaining objects via the collection interface.""" __id__ = "$Id$" __version__ = "$Revision$" @@ -44,6 +34,7 @@ import time import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi +from gi.repository import GLib from . import debug from .ax_object import AXObject @@ -56,23 +47,33 @@ class AXCollection: # This function wraps Atspi.MatchRule.new which has all the arguments. # pylint: disable=R0913,R0914 @staticmethod - def create_match_rule(states=[], - state_match_type=Atspi.CollectionMatchType.ALL, - attributes=[], - attribute_match_type=Atspi.CollectionMatchType.ANY, - roles=[], - role_match_type=Atspi.CollectionMatchType.ANY, - interfaces=[], - interface_match_type=Atspi.CollectionMatchType.ALL, - invert=False): + def create_match_rule( + states: list[str] | None = None, + state_match_type: Atspi.CollectionMatchType = Atspi.CollectionMatchType.ALL, + attributes: list[str] | None = None, + attribute_match_type: Atspi.CollectionMatchType = Atspi.CollectionMatchType.ALL, + roles: list[str] | None = None, + role_match_type: Atspi.CollectionMatchType = Atspi.CollectionMatchType.ALL, + interfaces: list[str] | None = None, + interface_match_type: Atspi.CollectionMatchType = Atspi.CollectionMatchType.ALL, + invert: bool = False) -> Atspi.MatchRule | None: """Creates a match rule based on the supplied criteria.""" + if states is None: + states = [] + if attributes is None: + attributes = [] + if roles is None: + roles = [] + if interfaces is None: + interfaces = [] + state_set = Atspi.StateSet() if states: for state in states: state_set.add(state) - attributes_dict = {} + attributes_dict: dict[str, str] = {} if attributes: for attr in attributes: key, value = attr.split(":", 1) @@ -92,19 +93,25 @@ class AXCollection: interfaces, interface_match_type, invert) - except Exception as error: + except GLib.GError as error: tokens = ["AXCollection: Exception in create_match_rule:", error] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return None return rule # pylint: enable=R0913,R0914 @staticmethod - def get_all_matches(obj, rule, order=Atspi.CollectionSortOrder.CANONICAL): + def get_all_matches( + obj: Atspi.Accessible, + rule: Atspi.MatchRule, + order: Atspi.CollectionSortOrder = Atspi.CollectionSortOrder.CANONICAL + ) -> list[Atspi.Accessible]: """Returns a list of objects matching the specified rule.""" if not AXObject.supports_collection(obj): + tokens = ["AXCollection:", obj, "does not implement this interface."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return [] if rule is None: @@ -115,20 +122,26 @@ class AXCollection: # 0 means no limit on the number of results # The final argument, traverse, is not supported but is expected. matches = Atspi.Collection.get_matches(obj, rule, order, 0, True) - except Exception as error: + except GLib.GError as error: tokens = ["AXCollection: Exception in get_all_matches:", error] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return [] msg = f"AXCollection: {len(matches)} match(es) found in {time.time() - start:.4f}s" - debug.printMessage(debug.LEVEL_INFO, msg, True) + debug.print_message(debug.LEVEL_INFO, msg, True) return matches @staticmethod - def get_first_match(obj, rule, order=Atspi.CollectionSortOrder.CANONICAL): + def get_first_match( + obj: Atspi.Accessible, + rule: Atspi.MatchRule, + order: Atspi.CollectionSortOrder = Atspi.CollectionSortOrder.CANONICAL + ) -> Atspi.Accessible | None: """Returns the first object matching the specified rule.""" if not AXObject.supports_collection(obj): + tokens = ["AXCollection:", obj, "does not implement this interface."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return None if rule is None: @@ -139,15 +152,15 @@ class AXCollection: # 1 means limit the number of results to 1 # The final argument, traverse, is not supported but is expected. matches = Atspi.Collection.get_matches(obj, rule, order, 1, True) - except Exception as error: + except GLib.GError as error: tokens = ["AXCollection: Exception in get_first_match:", error] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return None match = None if matches: match = matches[0] - msg = f"AXCollection: found {match} in {time.time() - start:.4f}s" - debug.printMessage(debug.LEVEL_INFO, msg, True) + tokens = ["AXCollection: found", match, f"in {time.time() - start:.4f}s"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return match diff --git a/src/cthulhu/ax_component.py b/src/cthulhu/ax_component.py new file mode 100644 index 0000000..ae08da1 --- /dev/null +++ b/src/cthulhu/ax_component.py @@ -0,0 +1,414 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 Stormux +# Copyright 2024 Igalia, S.L. +# Copyright 2024 GNOME Foundation Inc. +# Author: Joanmarie Diggs +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., Franklin Street, Fifth Floor, +# Boston MA 02110-1301 USA. +# +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu + +# pylint: disable=wrong-import-position + +"""Utilities for obtaining position-related information about accessible objects.""" + +__id__ = "$Id$" +__version__ = "$Revision$" +__date__ = "$Date$" +__copyright__ = "Copyright (c) 2024 Igalia, S.L." \ + "Copyright (c) 2024 GNOME Foundation Inc." +__license__ = "LGPL" + +import functools + +import gi +gi.require_version("Atspi", "2.0") +from gi.repository import Atspi +from gi.repository import GLib + +from . import debug +from .ax_object import AXObject +from .ax_utilities_role import AXUtilitiesRole + + +class AXComponent: + """Utilities for obtaining position-related information about accessible objects.""" + + @staticmethod + def get_center_point(obj: Atspi.Accessible) -> tuple[float, float]: + """Returns the center point of obj with respect to its window.""" + + rect = AXComponent.get_rect(obj) + return rect.x + rect.width / 2, rect.y + rect.height / 2 + + @staticmethod + def get_position(obj: Atspi.Accessible) -> tuple[int, int]: + """Returns the x, y position tuple of obj with respect to its window.""" + + if not AXObject.supports_component(obj): + return -1, -1 + + try: + point = Atspi.Component.get_position(obj, Atspi.CoordType.WINDOW) + except GLib.GError as error: + msg = f"AXComponent: Exception in get_position: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return -1, -1 + + if point is None: + tokens = ["AXComponent: get_position failed for", obj] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return -1, -1 + + return point.x, point.y + + @staticmethod + def get_rect(obj: Atspi.Accessible) -> Atspi.Rect: + """Returns the Atspi rect of obj with respect to its window.""" + + if not AXObject.supports_component(obj): + return Atspi.Rect() + + try: + rect = Atspi.Component.get_extents(obj, Atspi.CoordType.WINDOW) + except GLib.GError as error: + msg = f"AXComponent: Exception in get_rect: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return Atspi.Rect() + + return rect + + @staticmethod + def get_rect_intersection(rect1: Atspi.Rect, rect2: Atspi.Rect) -> Atspi.Rect: + """Returns a rect representing the intersection of rect1 and rect2.""" + + result = Atspi.Rect() + + dest_x = max(rect1.x, rect2.x) + dest_y = max(rect1.y, rect2.y) + dest_x2 = min(rect1.x + rect1.width, rect2.x + rect2.width) + dest_y2 = min(rect1.y + rect1.height, rect2.y + rect2.height) + + if dest_x2 >= dest_x and dest_y2 >= dest_y: + result.x = dest_x + result.y = dest_y + result.width = dest_x2 - dest_x + result.height = dest_y2 - dest_y + + tokens = ["AXComponent: The intersection of", rect1, "and", rect2, "is:", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def get_size(obj: Atspi.Accessible) -> tuple[int, int]: + """Returns the width, height tuple of obj with respect to its window.""" + + if not AXObject.supports_component(obj): + return -1, -1 + + try: + point = Atspi.Component.get_size(obj, Atspi.CoordType.WINDOW) + except GLib.GError as error: + msg = f"AXComponent: Exception in get_position: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return -1, -1 + + if point is None: + tokens = ["AXComponent: get_size failed for", obj] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return -1, -1 + + # An Atspi.Point object stores width in x and height in y. + return point.x, point.y + + @staticmethod + def has_no_size(obj: Atspi.Accessible) -> bool: + """Returns True if obj has a width and height of 0.""" + + rect = AXComponent.get_rect(obj) + return not(rect.width or rect.height) + + @staticmethod + def has_no_size_or_invalid_rect(obj: Atspi.Accessible) -> bool: + """Returns True if the rect associated with obj is sizeless or invalid.""" + + rect = AXComponent.get_rect(obj) + if not (rect.width or rect.height): + return True + + if rect.x == rect.y == rect.width == rect.height == -1: + return True + + if (rect.width < -1 or rect.height < -1): + tokens = ["WARNING: ", obj, "has a broken rect:", rect] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + AXObject.clear_cache(obj) + rect = AXComponent.get_rect(obj) + if (rect.width < -1 or rect.height < -1): + msg = "AXComponent: Clearing cache did not fix the rect" + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + + return False + + @staticmethod + def is_empty_rect(rect: Atspi.Rect) -> bool: + """Returns True if rect's x, y, width, and height are all 0.""" + + return rect.x == 0 and rect.y == 0 and rect.width == 0 and rect.height == 0 + + @staticmethod + def is_same_rect(rect1: Atspi.Rect, rect2: Atspi.Rect) -> bool: + """Returns True if rect1 and rect2 represent the same bounding box.""" + + return rect1.x == rect2.x \ + and rect1.y == rect2.y \ + and rect1.width == rect2.width \ + and rect1.height == rect2.height + + @staticmethod + def object_contains_point(obj: Atspi.Accessible, x: int, y: int) -> bool: + """Returns True if obj's rect contains the specified point.""" + + if not AXObject.supports_component(obj): + return False + + if AXObject.is_bogus(obj): + return False + + try: + result = Atspi.Component.contains(obj, x, y, Atspi.CoordType.WINDOW) + except GLib.GError as error: + msg = f"AXComponent: Exception in object_contains_point: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + tokens = ["AXComponent: ", obj, f"contains point {x}, {y}: {result}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def object_intersects_rect(obj: Atspi.Accessible, rect: Atspi.Rect) -> bool: + """Returns True if the Atspi.Rect associated with obj intersects rect.""" + + intersection = AXComponent.get_rect_intersection(AXComponent.get_rect(obj), rect) + return not AXComponent.is_empty_rect(intersection) + + @staticmethod + def object_is_off_screen(obj: Atspi.Accessible) -> bool: + """Returns True if the rect associated with obj is off-screen""" + + rect = AXComponent.get_rect(obj) + if abs(rect.x) > 10000 or abs(rect.y) > 10000: + tokens = ["AXComponent: Treating", obj, "as offscreen due to position"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return True + + if rect.width == 0 or rect.height == 0: + if not AXObject.get_child_count(obj): + tokens = ["AXComponent: Treating", obj, "as offscreen due to size and no children"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return True + if AXUtilitiesRole.is_menu(obj): + tokens = ["AXComponent: Treating", obj, "as offscreen due to size and role"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return True + tokens = ["AXComponent: Treating sizeless", obj, "as onscreen"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return False + + result = rect.x + rect.width < 0 and rect.y + rect.height < 0 + tokens = ["AXComponent:", obj, f"is off-screen: {result}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def objects_have_same_rect(obj1: Atspi.Accessible, obj2: Atspi.Accessible) -> bool: + """Returns True if obj1 and obj2 have the same rect.""" + + return AXComponent.is_same_rect(AXComponent.get_rect(obj1),AXComponent.get_rect(obj2)) + + @staticmethod + def objects_overlap(obj1: Atspi.Accessible, obj2: Atspi.Accessible) -> bool: + """Returns True if the rects associated with obj1 and obj2 overlap.""" + + intersection = AXComponent.get_rect_intersection( + AXComponent.get_rect(obj1), AXComponent.get_rect(obj2)) + return not AXComponent.is_empty_rect(intersection) + + @staticmethod + def on_same_line(obj1: Atspi.Accessible, obj2: Atspi.Accessible, delta: int = 0) -> bool: + """Returns True if obj1 and obj2 are on the same line.""" + + rect1 = AXComponent.get_rect(obj1) + rect2 = AXComponent.get_rect(obj2) + y1_center = rect1.y + rect1.height / 2 + y2_center = rect2.y + rect2.height / 2 + + # If the center points differ by more than delta, they are not on the same line. + if abs(y1_center - y2_center) > delta: + return False + + # If there's a significant difference in height, they are not on the same line. + min_height = min(rect1.height, rect2.height) + max_height = max(rect1.height, rect2.height) + if min_height > 0 and max_height / min_height > 2.0: + return False + + return True + + @staticmethod + def _object_bounds_includes_children(obj: Atspi.Accessible) -> bool: + """Returns True if obj's rect is expected to include the rects of its children.""" + + if AXUtilitiesRole.is_menu(obj) or AXUtilitiesRole.is_page_tab(obj): + return False + + rect = AXComponent.get_rect(obj) + return rect.width > 0 and rect.height > 0 + + @staticmethod + def _find_descendant_at_point( + obj: Atspi.Accessible, x: int, y: int + ) -> Atspi.Accessible | None: + """Checks each child to see if it has a descendant at the specified point.""" + + for child in AXObject.iter_children(obj): + if AXComponent._object_bounds_includes_children(child): + continue + for descendant in AXObject.iter_children(child): + if AXComponent.object_contains_point(descendant, x, y): + return descendant + return None + + @staticmethod + def _get_object_at_point(obj: Atspi.Accessible, x: int, y: int) -> Atspi.Accessible | None: + """Returns the child (or descendant?) of obj at the specified point.""" + + if not AXObject.supports_component(obj): + return None + + try: + result = Atspi.Component.get_accessible_at_point(obj, x, y, Atspi.CoordType.WINDOW) + except GLib.GError as error: + msg = f"AXComponent: Exception in get_child_at_point: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return None + + tokens = ["AXComponent: Child of", obj, f"at {x}, {y} is", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def _get_descendant_at_point( + obj: Atspi.Accessible, x: int, y: int + ) -> Atspi.Accessible | None: + """Returns the deepest descendant of obj at the specified point.""" + + child = AXComponent._get_object_at_point(obj, x, y) + if child is None and AXComponent.object_contains_point(obj, x, y): + descendant = AXComponent._find_descendant_at_point(obj, x, y) + if descendant is None: + return obj + child = descendant + + if child == obj or not AXObject.get_child_count(child): + return child + + result = AXComponent._get_descendant_at_point(child, x, y) + if result and not AXObject.is_dead(result): + return result + return child + + @staticmethod + def get_descendant_at_point( + obj: Atspi.Accessible, x: int, y: int + ) -> Atspi.Accessible | None: + """Returns the deepest descendant of obj at the specified point.""" + + result = AXComponent._get_descendant_at_point(obj, x, y) + tokens = ["AXComponent: Descendant of", obj, f"at {x}, {y} is", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def scroll_object_to_point(obj: Atspi.Accessible, x: int, y: int) -> bool: + """Attempts to scroll obj to the specified point.""" + + if not AXObject.supports_component(obj): + return False + + try: + result = Atspi.Component.scroll_to_point(obj, Atspi.CoordType.WINDOW, x, y) + except GLib.GError as error: + msg = f"AXComponent: Exception in scroll_object_to_point: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + tokens = ["AXComponent: Scrolled", obj, f"to {x}, {y}:", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def scroll_object_to_location(obj: Atspi.Accessible, location: Atspi.ScrollType) -> bool: + """Attempts to scroll obj to the specified Atspi.ScrollType location.""" + + if not AXObject.supports_component(obj): + return False + + try: + result = Atspi.Component.scroll_to(obj, location) + except GLib.GError as error: + msg = f"AXComponent: Exception in scroll_object_to_location: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + tokens = ["AXComponent: Scrolled", obj, "to", location, f": {result}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def sort_objects_by_size(objects: list[Atspi.Accessible]) -> list[Atspi.Accessible]: + """Returns objects sorted from smallest to largest.""" + + def _size_comparison(obj1: Atspi.Accessible, obj2: Atspi.Accessible) -> int: + rect1 = AXComponent.get_rect(obj1) + rect2 = AXComponent.get_rect(obj2) + return (rect1.width * rect1.height) - (rect2.width * rect2.height) + + return sorted(objects, key=functools.cmp_to_key(_size_comparison)) + + @staticmethod + def sort_objects_by_position(objects: list[Atspi.Accessible]) -> list[Atspi.Accessible]: + """Returns objects sorted from top-left to bottom-right.""" + + def _spatial_comparison(obj1: Atspi.Accessible, obj2: Atspi.Accessible) -> int: + rect1 = AXComponent.get_rect(obj1) + rect2 = AXComponent.get_rect(obj2) + rv = rect1.y - rect2.y or rect1.x - rect2.x + + # If the objects claim to have the same coordinates and the same parent, + # we probably have bogus coordinates from the implementation. + if not rv and AXObject.get_parent(obj1) == AXObject.get_parent(obj2): + rv = AXObject.get_index_in_parent(obj1) - AXObject.get_index_in_parent(obj2) + + rv = max(rv, -1) + rv = min(rv, 1) + return rv + + return sorted(objects, key=functools.cmp_to_key(_spatial_comparison)) diff --git a/src/cthulhu/ax_document.py b/src/cthulhu/ax_document.py new file mode 100644 index 0000000..f261c18 --- /dev/null +++ b/src/cthulhu/ax_document.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 Stormux +# Copyright 2024 Igalia, S.L. +# Copyright 2024 GNOME Foundation Inc. +# Author: Joanmarie Diggs +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., Franklin Street, Fifth Floor, +# Boston MA 02110-1301 USA. +# +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu + +# pylint: disable=wrong-import-position + +"""Utilities for obtaining document-related information about accessible objects.""" + +__id__ = "$Id$" +__version__ = "$Revision$" +__date__ = "$Date$" +__copyright__ = "Copyright (c) 2024 Igalia, S.L." \ + "Copyright (c) 2024 GNOME Foundation Inc." +__license__ = "LGPL" + +import threading +import time +import urllib.parse + +import gi +gi.require_version("Atspi", "2.0") +from gi.repository import Atspi +from gi.repository import GLib + +from . import debug +from . import messages +from .ax_collection import AXCollection +from .ax_object import AXObject +from .ax_table import AXTable +from .ax_utilities_role import AXUtilitiesRole +from .ax_utilities_state import AXUtilitiesState + +class AXDocument: + """Utilities for obtaining document-related information about accessible objects.""" + + LAST_KNOWN_PAGE: dict[int, int] = {} + _lock = threading.Lock() + + @staticmethod + def _clear_stored_data() -> None: + """Clears any data we have cached for objects""" + + while True: + time.sleep(60) + msg = "AXDocument: Clearing local cache." + debug.print_message(debug.LEVEL_INFO, msg, True) + AXDocument.LAST_KNOWN_PAGE.clear() + + @staticmethod + def start_cache_clearing_thread() -> None: + """Starts thread to periodically clear cached details.""" + + thread = threading.Thread(target=AXDocument._clear_stored_data) + thread.daemon = True + thread.start() + + @staticmethod + def did_page_change(document: Atspi.Accessible) -> bool: + """Returns True if the current page changed.""" + + if not AXObject.supports_document(document): + return False + + old_page = AXDocument.LAST_KNOWN_PAGE.get(hash(document)) + result = old_page != AXDocument._get_current_page(document) + if result: + tokens = ["AXDocument: Previous page of", document, f"was {old_page}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + return result + + @staticmethod + def _get_current_page(document: Atspi.Accessible) -> int: + """Returns the current page of document.""" + + if not AXObject.supports_document(document): + return 0 + + try: + page = Atspi.Document.get_current_page_number(document) + except GLib.GError as error: + msg = f"AXDocument: Exception in _get_current_page: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return 0 + + tokens = ["AXDocument: Current page of", document, f"is {page}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return page + + @staticmethod + def get_current_page(document: Atspi.Accessible) -> int: + """Returns the current page of document.""" + + if not AXObject.supports_document(document): + return 0 + + page = AXDocument._get_current_page(document) + AXDocument.LAST_KNOWN_PAGE[hash(document)] = page + return page + + @staticmethod + def get_page_count(document: Atspi.Accessible) -> int: + """Returns the page count of document.""" + + if not AXObject.supports_document(document): + return 0 + + try: + count = Atspi.Document.get_page_count(document) + except GLib.GError as error: + msg = f"AXDocument: Exception in get_page_count: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return 0 + + tokens = ["AXDocument: Page count of", document, f"is {count}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return count + + @staticmethod + def get_locale(document: Atspi.Accessible) -> str: + """Returns the locale of document.""" + + if not AXObject.supports_document(document): + return "" + + try: + result = Atspi.Document.get_locale(document) + except GLib.GError as error: + msg = f"AXDocument: Exception in get_locale: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return "" + + if result is None: + tokens = ["AXDocument: get_locale failed for", document] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return "" + + tokens = ["AXDocument: Locale of", document, f"is '{result}'"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def _get_attributes_dict(document: Atspi.Accessible) -> dict[str, str]: + """Returns a dict with the document-attributes of document.""" + + if not AXObject.supports_document(document): + return {} + + try: + result = Atspi.Document.get_document_attributes(document) + except GLib.GError as error: + msg = f"AXDocument: Exception in _get_attributes_dict: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return {} + + tokens = ["AXDocument: Attributes of", document, "are:", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result or {} + + @staticmethod + def get_uri(document: Atspi.Accessible) -> str: + """Returns the uri of document.""" + + if not AXObject.supports_document(document): + return "" + + attributes = AXDocument._get_attributes_dict(document) + return attributes.get("DocURL", attributes.get("URI", "")) + + @staticmethod + def get_mime_type(document: Atspi.Accessible) -> str: + """Returns the uri of document.""" + + if not AXObject.supports_document(document): + return "" + + attributes = AXDocument._get_attributes_dict(document) + return attributes.get("MimeType", "") + + @staticmethod + def is_plain_text(document: Atspi.Accessible) -> bool: + """Returns True if document is a plain-text document.""" + + return AXDocument.get_mime_type(document) == "text/plain" + + @staticmethod + def is_pdf(document: Atspi.Accessible) -> bool: + """Returns True if document is a PDF document.""" + + mime_type = AXDocument.get_mime_type(document) + if mime_type == "application/pdf": + return True + if mime_type == "text/html": + return AXDocument.get_uri(document).endswith(".pdf") + return False + + @staticmethod + def get_document_uri_fragment(document: Atspi.Accessible) -> str: + """Returns the fragment portion of document's uri.""" + + result = urllib.parse.urlparse(AXDocument.get_uri(document)) + return result.fragment + + @staticmethod + def _get_object_counts(document: Atspi.Accessible) -> dict[str, int]: + """Returns a dictionary of object counts used in a document summary.""" + + result = {"forms": 0, + "landmarks": 0, + "headings": 0, + "tables": 0, + "unvisited_links": 0, + "visited_links": 0} + + roles = [Atspi.Role.HEADING, + Atspi.Role.LINK, + Atspi.Role.TABLE, + Atspi.Role.FORM, + Atspi.Role.LANDMARK] + + rule = AXCollection.create_match_rule( + roles=roles, role_match_type=Atspi.CollectionMatchType.ANY) + matches = AXCollection.get_all_matches(document, rule) + + for obj in matches: + if AXUtilitiesRole.is_heading(obj): + result["headings"] += 1 + elif AXUtilitiesRole.is_form(obj): + result["forms"] += 1 + elif AXUtilitiesRole.is_table(obj) and not AXTable.is_layout_table(obj): + result["tables"] += 1 + elif AXUtilitiesRole.is_link(obj): + if AXUtilitiesState.is_visited(obj): + result["visited_links"] += 1 + else: + result["unvisited_links"] += 1 + elif AXUtilitiesRole.is_landmark(obj): + result["landmarks"] += 1 + + return result + + @staticmethod + def get_document_summary(document: Atspi.Accessible, only_if_found: bool = True) -> str: + """Returns a string summarizing the document's structure and objects of interest.""" + + result = [] + counts = AXDocument._get_object_counts(document) + result.append(messages.landmark_count(counts.get("landmarks", 0), only_if_found)) + result.append(messages.heading_count(counts.get("headings", 0), only_if_found)) + result.append(messages.form_count(counts.get("forms", 0), only_if_found)) + result.append(messages.table_count(counts.get("tables", 0), only_if_found)) + result.append(messages.visited_link_count(counts.get("visited_links", 0), only_if_found)) + result.append(messages.unvisited_link_count( + counts.get("unvisited_links", 0), only_if_found)) + result = list(filter(lambda x: x, result)) + if not result: + return "" + + return messages.PAGE_SUMMARY_PREFIX % ", ".join(result) diff --git a/src/cthulhu/ax_event_synthesizer.py b/src/cthulhu/ax_event_synthesizer.py index ccb1c32..9b55505 100644 --- a/src/cthulhu/ax_event_synthesizer.py +++ b/src/cthulhu/ax_event_synthesizer.py @@ -1,9 +1,8 @@ #!/usr/bin/env python3 # # Copyright (c) 2024 Stormux -# Copyright (c) 2010-2012 The Orca Team -# Copyright (c) 2012 Igalia, S.L. -# Copyright (c) 2005-2010 Sun Microsystems Inc. +# Copyright 2005-2008 Sun Microsystems Inc. +# Copyright 2018-2023 Igalia, S.L. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -20,8 +19,11 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu + +# pylint: disable=wrong-import-position +# pylint: disable=too-many-public-methods """Provides support for synthesizing accessible input events.""" @@ -32,555 +34,328 @@ __copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." \ "Copyright (c) 2018-2023 Igalia, S.L." __license__ = "LGPL" -import time - import gi - gi.require_version("Atspi", "2.0") -from gi.repository import Atspi - gi.require_version("Gtk", "3.0") -from gi.repository import Gtk +from gi.repository import Atspi +from gi.repository import GLib from . import debug +from .ax_component import AXComponent from .ax_object import AXObject -from .ax_utilities import AXUtilities +from .ax_text import AXText +from .ax_utilities_debugging import AXUtilitiesDebugging +from .ax_utilities_role import AXUtilitiesRole class AXEventSynthesizer: """Provides support for synthesizing accessible input events.""" - _banner = None + @staticmethod + def _highest_ancestor(obj: Atspi.Accessible) -> bool: + """Returns True if the parent of obj is the application or None.""" + + parent = AXObject.get_parent(obj) + return parent is None or AXUtilitiesRole.is_application(parent) @staticmethod - def _get_mouse_coordinates(): - """Returns the current mouse coordinates.""" + def _is_scrolled_off_screen( + obj: Atspi.Accessible, + offset: int | None = None, + ancestor: Atspi.Accessible | None = None + ) -> bool: + """Returns true if obj, or the caret offset therein, is scrolled off-screen.""" - root_window = Gtk.Window().get_screen().get_root_window() - window, x_coord, y_coord, modifiers = root_window.get_pointer() - tokens = ["AXEventSynthesizer: Mouse coordinates:", x_coord, ",", y_coord] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return x_coord, y_coord + tokens = ["AXEventSynthesizer: Checking if", obj, "is scrolled offscreen"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) - @staticmethod - def _generate_mouse_event(x_coord, y_coord, event): - """Synthesize a mouse event at a specific screen coordinate.""" - - old_x, old_y = AXEventSynthesizer._get_mouse_coordinates() - tokens = ["AXEventSynthesizer: Generating", event, "mouse event at", x_coord, ",", y_coord] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - - try: - success = Atspi.generate_mouse_event(x_coord, y_coord, event) - except Exception as error: - tokens = ["AXEventSynthesizer: Exception in _generate_mouse_event:", error] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - success = False - else: - tokens = ["AXEventSynthesizer: Atspi.generate_mouse_event returned", success] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - - # There seems to be a timeout / lack of reply from this blocking call. - # But often the mouse event is successful. Pause briefly before checking. - time.sleep(1) - - new_x, new_y = AXEventSynthesizer._get_mouse_coordinates() - if old_x == new_x and old_y == new_y and (old_x, old_y) != (x_coord, y_coord): - msg = "AXEventSynthesizer: Mouse event possible failure. Pointer didn't move" - debug.println(debug.LEVEL_INFO, msg, True) + rect = AXComponent.get_rect(obj) + ancestor = ancestor or AXObject.find_ancestor(obj, AXEventSynthesizer._highest_ancestor) + if ancestor is None: + tokens = ["AXEventSynthesizer: Could not get ancestor of", obj] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return False + ancestor_rect = AXComponent.get_rect(ancestor) + intersection = AXComponent.get_rect_intersection(ancestor_rect, rect) + if AXComponent.is_empty_rect(intersection): + tokens = ["AXEventSynthesizer:", obj, "is outside of", ancestor, ancestor_rect] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return True + + if offset is None: + tokens = ["AXEventSynthesizer:", obj, "is not scrolled offscreen"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return False + + extents = AXText.get_character_rect(obj, offset) + if AXComponent.is_empty_rect(extents): + tokens = ["AXEventSynthesizer: Could not get character rect of", obj] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return False + + intersection = AXComponent.get_rect_intersection(extents, rect) + if AXComponent.is_empty_rect(intersection): + tokens = ["AXEventSynthesizer:", obj, "'s caret", extents, "not in obj", rect] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return True + + return False + + @staticmethod + def _generate_mouse_event( + obj: Atspi.Accessible, relative_x: int, relative_y: int, event: str + ) -> bool: + tokens = ["AXEventSynthesizer: Attempting to generate mouse event on", obj, + f"at relative coordinates {relative_x},{relative_y}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + try: + device = Atspi.Device.new() + Atspi.Device.generate_mouse_event(device, obj, relative_x, relative_y, event) + except GLib.GError as error: + message = f"AXEventSynthesizer: Exception in _generate_mouse_event_new: {error}" + debug.print_message(debug.LEVEL_INFO, message, True) + return False return True @staticmethod - def _intersection(extents1, extents2): - """Returns the bounding box containing the intersection of the two boxes.""" - - x_coord1, y_coord1, width1, height1 = extents1 - x_coord2, y_coord2, width2, height2 = extents2 - - x_points1 = range(x_coord1, x_coord1 + width1 + 1) - x_points2 = range(x_coord2, x_coord2 + width2 + 1) - x_intersection = sorted(set(x_points1).intersection(set(x_points2))) - - y_points1 = range(y_coord1, y_coord1 + height1 + 1) - y_points2 = range(y_coord2, y_coord2 + height2 + 1) - y_intersection = sorted(set(y_points1).intersection(set(y_points2))) - - if not (x_intersection and y_intersection): - return 0, 0, 0, 0 - - x_coord = x_intersection[0] - y_coord = y_intersection[0] - width = x_intersection[-1] - x_coord - height = y_intersection[-1] - y_coord - return x_coord, y_coord, width, height - - @staticmethod - def _extents_at_caret(obj): - """Returns the character extents of obj at the current caret offset.""" - - try: - text = obj.queryText() - extents = text.getCharacterExtents(text.caretOffset, Atspi.CoordType.SCREEN) - except Exception: - tokens = ["ERROR: Exception getting character extents for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return 0, 0, 0, 0 - - return extents - - @staticmethod - def _object_extents(obj): - """Returns the bounding box associated with obj.""" - - try: - extents = obj.queryComponent().getExtents(Atspi.CoordType.SCREEN) - except Exception: - tokens = ["ERROR: Exception getting extents for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return 0, 0, 0, 0 - - return extents - - @staticmethod - def _mouse_event_on_character(obj, event): + def _mouse_event_on_character(obj: Atspi.Accessible, offset: int | None, event: str) -> bool: """Performs the specified mouse event on the current character in obj.""" - extents = AXEventSynthesizer._extents_at_caret(obj) - if extents == (0, 0, 0, 0): + if offset is None: + offset = max(AXText.get_caret_offset(obj), 0) + + if AXEventSynthesizer._is_scrolled_off_screen(obj, offset): + AXEventSynthesizer.scroll_into_view(obj, offset) + if AXEventSynthesizer._is_scrolled_off_screen(obj, offset): + tokens = ["AXEventSynthesizer:", obj, "is still offscreen. Setting caret."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + AXText.set_caret_offset(obj, offset) + + extents = AXText.get_character_rect(obj, offset) + if AXComponent.is_empty_rect(extents): return False - obj_extents = AXEventSynthesizer._object_extents(obj) - intersection = AXEventSynthesizer._intersection(extents, obj_extents) - if intersection == (0, 0, 0, 0): - tokens = ["AXEventSynthesizer:", obj, "'s caret", extents, "not in obj", obj_extents] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + rect = AXComponent.get_rect(obj) + intersection = AXComponent.get_rect_intersection(extents, rect) + if AXComponent.is_empty_rect(intersection): + tokens = ["AXEventSynthesizer:", obj, "'s caret", extents, "not in obj", rect] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return False - x_coord = max(extents[0], extents[0] + (extents[2] / 2) - 1) - y_coord = extents[1] + extents[3] / 2 - return AXEventSynthesizer._generate_mouse_event(x_coord, y_coord, event) + relative_x = (extents.x - rect.x) + extents.width / 2 + relative_y = (extents.y - rect.y) + extents.height / 2 + return AXEventSynthesizer._generate_mouse_event(obj, relative_x, relative_y, event) @staticmethod - def _mouse_event_on_object(obj, event): + def _mouse_event_on_object(obj: Atspi.Accessible, event: str) -> bool: """Performs the specified mouse event on obj.""" - extents = AXEventSynthesizer._object_extents(obj) - if extents == (0, 0, 0, 0): - return False + if AXEventSynthesizer._is_scrolled_off_screen(obj): + AXEventSynthesizer.scroll_into_view(obj) + if AXEventSynthesizer._is_scrolled_off_screen(obj): + tokens = ["AXEventSynthesizer:", obj, "is still offscreen. Grabbing focus."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + AXObject.grab_focus(obj) - x_coord = extents.x + extents.width/2 - y_coord = extents.y + extents.height/2 - return AXEventSynthesizer._generate_mouse_event(x_coord, y_coord, event) + rect = AXComponent.get_rect(obj) + relative_x = rect.width / 2 + relative_y = rect.height / 2 + return AXEventSynthesizer._generate_mouse_event(obj, relative_x, relative_y, event) @staticmethod - def route_to_character(obj): + def route_to_character(obj: Atspi.Accessible, offset: int | None = None) -> bool: """Routes the pointer to the current character in obj.""" - tokens = ["AXEventSynthesizer: Attempting to route to character in", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return AXEventSynthesizer._mouse_event_on_character(obj, "abs") + tokens = [f"AXEventSynthesizer: Attempting to route to offset {offset} in", obj] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return AXEventSynthesizer._mouse_event_on_character(obj, offset, "abs") @staticmethod - def route_to_object(obj): + def route_to_object(obj: Atspi.Accessible) -> bool: """Moves the mouse pointer to the center of obj.""" tokens = ["AXEventSynthesizer: Attempting to route to", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return AXEventSynthesizer._mouse_event_on_object(obj, "abs") @staticmethod - def route_to_point(x_coord, y_coord): - """Routes the pointer to the specified coordinates.""" - - return AXEventSynthesizer._generate_mouse_event(x_coord, y_coord, "abs") - - @staticmethod - def click_character(obj, button=1): + def click_character( + obj: Atspi.Accessible, offset: int | None = None, button: int = 1 + ) -> bool: """Single click on the current character in obj using the specified button.""" - return AXEventSynthesizer._mouse_event_on_character(obj, f"b{button}c") + tokens = [f"AXEventSynthesizer: Attempting to click at offset {offset} in", obj] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return AXEventSynthesizer._mouse_event_on_character(obj, offset, f"b{button}c") @staticmethod - def click_object(obj, button=1): + def click_object(obj: Atspi.Accessible, button: int = 1) -> bool: """Single click on obj using the specified button.""" return AXEventSynthesizer._mouse_event_on_object(obj, f"b{button}c") @staticmethod - def click_point(x_coord, y_coord, button=1): - """Single click on the given point using the specified button.""" - - return AXEventSynthesizer._generate_mouse_event(x_coord, y_coord, f"b{button}c") - - @staticmethod - def double_click_character(obj, button=1): - """Double click on the current character in obj using the specified button.""" - - return AXEventSynthesizer._mouse_event_on_character(obj, f"b{button}d") - - @staticmethod - def double_click_object(obj, button=1): - """Double click on obj using the specified button.""" - - return AXEventSynthesizer._mouse_event_on_object(obj, f"b{button}d") - - @staticmethod - def double_click_point(x_coord, y_coord, button=1): - """Double click on the given point using the specified button.""" - - return AXEventSynthesizer._generate_mouse_event(x_coord, y_coord, f"b{button}d") - - @staticmethod - def press_at_character(obj, button=1): - """Performs a press on the current character in obj using the specified button.""" - - return AXEventSynthesizer._mouse_event_on_character(obj, f"b{button}p") - - @staticmethod - def press_at_object(obj, button=1): - """Performs a press on obj using the specified button.""" - - return AXEventSynthesizer._mouse_event_on_object(obj, f"b{button}p") - - @staticmethod - def press_at_point(x_coord, y_coord, button=1): - """Performs a press on the given point using the specified button.""" - - return AXEventSynthesizer._generate_mouse_event(x_coord, y_coord, f"b{button}p") - - @staticmethod - def release_at_character(obj, button=1): - """Performs a release on the current character in obj using the specified button.""" - - return AXEventSynthesizer._mouse_event_on_character(obj, f"b{button}r") - - @staticmethod - def release_at_object(obj, button=1): - """Performs a release on obj using the specified button.""" - - return AXEventSynthesizer._mouse_event_on_object(obj, f"b{button}r") - - @staticmethod - def release_at_point(x_coord, y_coord, button=1): - """Performs a release on the given point using the specified button.""" - - return AXEventSynthesizer._generate_mouse_event(x_coord, y_coord, f"b{button}r") - - @staticmethod - def _scroll_substring_to_location(obj, location, start_offset, end_offset): - """Attempts to scroll the given substring to the specified location.""" - - try: - text = obj.queryText() - if not text.characterCount: - return False - if start_offset is None: - start_offset = 0 - if end_offset is None: - end_offset = text.characterCount - 1 - result = text.scrollSubstringTo(start_offset, end_offset, location) - except NotImplementedError: - tokens = ["AXEventSynthesizer: Text interface not implemented for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return False - except Exception: - msg = ( - f"AXEventSynthesizer: Exception scrolling {obj} ({start_offset}, {end_offset}) " - f"to {location.value_name}." - ) - debug.println(debug.LEVEL_INFO, msg, True) - return False - - msg = ( - f"AXEventSynthesizer: scrolled {obj} substring ({start_offset}, {end_offset}) " - f"to {location.value_name}: {result}" - ) - debug.println(debug.LEVEL_INFO, msg, True) - return result - - @staticmethod - def _scroll_object_to_location(obj, location): - """Attempts to scroll obj to the specified location.""" - - try: - result = obj.queryComponent().scrollTo(location) - except NotImplementedError: - tokens = ["AXEventSynthesizer: Component interface not implemented for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return False - except Exception: - tokens = ["AXEventSynthesizer: Exception scrolling", - obj, "to", location.value_name, "."] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return False - - tokens = ["AXEventSynthesizer: scrolled", obj, "to", location.value_name, ":", result] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return result - - @staticmethod - def _scroll_to_location(obj, location, start_offset=None, end_offset=None): + def _scroll_to_location( + obj: Atspi.Accessible, location: Atspi.ScrollType, + start_offset: int | None = None, end_offset: int | None = None + ) -> None: """Attempts to scroll to the specified location.""" - try: - component = obj.queryComponent() - except Exception: - tokens = ["AXEventSynthesizer: Exception querying component of", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + before = AXComponent.get_position(obj) + AXText.scroll_substring_to_location(obj, location, start_offset, end_offset) + AXObject.clear_cache(obj, False, "To obtain updated location after scroll.") + after = AXComponent.get_position(obj) + tokens = ["AXEventSynthesizer: Text scroll, before:", before, "after:", after] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + if before != after: return - before = component.getExtents(Atspi.CoordType.SCREEN) - - if not AXEventSynthesizer._scroll_substring_to_location( - obj, location, start_offset, end_offset): - AXEventSynthesizer._scroll_object_to_location(obj, location) - - after = component.getExtents(Atspi.CoordType.SCREEN) - msg = ( - f"AXEventSynthesizer: Before scroll: {before[0]}, {before[1]}. " - f"After scroll: {after[0]}, {after[1]}." - ) - debug.println(debug.LEVEL_INFO, msg, True) + AXComponent.scroll_object_to_location(obj, location) + AXObject.clear_cache(obj, False, "To obtain updated location after scroll.") + after = AXComponent.get_position(obj) + tokens = ["AXEventSynthesizer: Object scroll, before:", before, "after:", after] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) @staticmethod - def _scroll_substring_to_point(obj, x_coord, y_coord, start_offset, end_offset): - """Attempts to scroll the given substring to the specified location.""" - - try: - text = obj.queryText() - if not text.characterCount: - return False - if start_offset is None: - start_offset = 0 - if end_offset is None: - end_offset = text.characterCount - 1 - result = text.scrollSubstringToPoint( - start_offset, end_offset, Atspi.CoordType.SCREEN, x_coord, y_coord) - except NotImplementedError: - tokens = ["AXEventSynthesizer: Text interface not implemented for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return False - except Exception: - msg = ( - f"AXEventSynthesizer: Exception scrolling {obj} ({start_offset}, {end_offset}) " - f"to {x_coord}, {y_coord}" - ) - debug.println(debug.LEVEL_INFO, msg, True) - return False - - msg = "AXEventSynthesizer: scrolled %s (%i, %i) to %i, %i: %s" % \ - (obj, start_offset, end_offset, x_coord, y_coord, result) - debug.println(debug.LEVEL_INFO, msg, True) - return result - - @staticmethod - def _scroll_object_to_point(obj, x_coord, y_coord): + def _scroll_to_point( + obj: Atspi.Accessible, x_coord: int, y_coord: int, + start_offset: int | None = None, end_offset: int | None = None + ) -> None: """Attempts to scroll obj to the specified point.""" - try: - result = obj.queryComponent().scrollToPoint(Atspi.CoordType.SCREEN, x_coord, y_coord) - except NotImplementedError: - tokens = ["AXEventSynthesizer: Component interface not implemented for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return False - except Exception: - tokens = ["AXEventSynthesizer: Exception scrolling", obj, "to", x_coord, ",", y_coord] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return False - - tokens = ["AXEventSynthesizer: scrolled", obj, "to", x_coord, ",", y_coord, ":", result] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return result - - @staticmethod - def _scroll_to_point(obj, x_coord, y_coord, start_offset=None, end_offset=None): - """Attempts to scroll obj to the specified point.""" - - try: - component = obj.queryComponent() - except Exception: - tokens = ["AXEventSynthesizer: Exception querying component of", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + before = AXComponent.get_position(obj) + AXText.scroll_substring_to_point(obj, x_coord, y_coord, start_offset, end_offset) + AXObject.clear_cache(obj, False, "To obtain updated location after scroll.") + after = AXComponent.get_position(obj) + tokens = ["AXEventSynthesizer: Text scroll, before:", before, "after:", after] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + if before != after: return - before = component.getExtents(Atspi.CoordType.SCREEN) - - if not AXEventSynthesizer._scroll_substring_to_point( - obj, x_coord, y_coord, start_offset, end_offset): - AXEventSynthesizer._scroll_object_to_point(obj, x_coord, y_coord) - - after = component.getExtents(Atspi.CoordType.SCREEN) - msg = ( - f"AXEventSynthesizer: Before scroll: {before[0]}, {before[1]}. " - f"After scroll: {after[0]}, {after[1]}." - ) - debug.println(debug.LEVEL_INFO, msg, True) + AXComponent.scroll_object_to_point(obj, x_coord, y_coord) + AXObject.clear_cache(obj, False, "To obtain updated location after scroll.") + after = AXComponent.get_position(obj) + tokens = ["AXEventSynthesizer: Object scroll, before:", before, "after:", after] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) @staticmethod - def scroll_into_view(obj, start_offset=None, end_offset=None): + def scroll_into_view( + obj: Atspi.Accessible, + start_offset: int | None = None, + end_offset: int | None = None + ) -> None: """Attempts to scroll obj into view.""" AXEventSynthesizer._scroll_to_location( obj, Atspi.ScrollType.ANYWHERE, start_offset, end_offset) @staticmethod - def _containing_document(obj): - """Returns the document containing obj""" + def scroll_to_center( + obj: Atspi.Accessible, + start_offset: int | None = None, + end_offset: int | None = None + ) -> None: + """Attempts to scroll obj to the center of its window.""" - document = AXObject.find_ancestor(obj, AXUtilities.is_document) - while document: - ancestor = AXObject.find_ancestor(document, AXUtilities.is_document) - if ancestor is None or ancestor == document: - break - document = ancestor - - return document - - @staticmethod - def _get_accessible_at_point(root, x_coord, y_coord): - """"Returns the accessible in root at the specified point.""" - - try: - result = root.queryComponent().getAccessibleAtPoint( - x_coord, y_coord, Atspi.CoordType.SCREEN) - except NotImplementedError: - tokens = ["AXEventSynthesizer: Component interface not implemented for", root] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return None - except Exception: - msg = ( - f"AXEventSynthesizer: Exception getting accessible at " - f"{x_coord}, {y_coord} for {root}" - ) - debug.println(debug.LEVEL_INFO, msg, True) - return None - - tokens = ["AXEventSynthesizer: Accessible at", - x_coord, ",", y_coord, "in", root, ":", result] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return result - - @staticmethod - def _get_obscuring_banner(obj): - """"Returns the banner obscuring obj from view.""" - - document = AXEventSynthesizer._containing_document(obj) - if not document: - tokens = ["AXEventSynthesizer: No obscuring banner found for", obj, ". No document."] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return None - - if not AXObject.supports_component(document): - tokens = ["AXEventSynthesizer: No obscuring banner found for", obj, ". No doc iface."] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return None - - obj_x, obj_y, obj_width, obj_height = AXEventSynthesizer._object_extents(obj) - doc_x, doc_y, doc_width, doc_height = AXEventSynthesizer._object_extents(document) - - left = AXEventSynthesizer._get_accessible_at_point(document, doc_x, obj_y) - right = AXEventSynthesizer._get_accessible_at_point(document, doc_x + doc_width, obj_y) - if not (left and right and left == right != document): - tokens = ["AXEventSynthesizer: No obscuring banner found for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return None - - tokens = ["AXEventSynthesizer:", obj, "believed to be obscured by banner", left] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return left - - @staticmethod - def _scroll_below_banner(obj, banner, start_offset, end_offset, margin=25): - """Attempts to scroll obj below banner.""" - - obj_x, obj_y, obj_width, obj_height = AXEventSynthesizer._object_extents(obj) - banner_x, banner_y, banner_width, banner_height = AXEventSynthesizer._object_extents(banner) - msg = ( - f"AXEventSynthesizer: Extents of banner: " - f"({banner_x}, {banner_y}, {banner_width}, {banner_height})" - ) - debug.println(debug.LEVEL_INFO, msg, True) - AXEventSynthesizer._scroll_to_point( - obj, obj_x, banner_y + banner_height + margin, start_offset, end_offset) - - @staticmethod - def scroll_to_top_edge(obj, start_offset=None, end_offset=None): - """Attempts to scroll obj to the top edge.""" - - if AXEventSynthesizer._banner and not AXObject.is_dead(AXEventSynthesizer._banner): - msg = ( - f"AXEventSynthesizer: Suspected existing banner found: " - f"{AXEventSynthesizer._banner}" - ) - debug.println(debug.LEVEL_INFO, msg, True) - AXEventSynthesizer._scroll_below_banner( - obj, AXEventSynthesizer._banner, start_offset, end_offset) + ancestor = AXObject.find_ancestor(obj, AXEventSynthesizer._highest_ancestor) + if ancestor is None: + tokens = ["AXEventSynthesizer: Could not get ancestor of", obj] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return + ancestor_rect = AXComponent.get_rect(ancestor) + x_coord = ancestor_rect.x + ancestor_rect.width / 2 + y_coord = ancestor_rect.y + ancestor_rect.height / 2 + AXEventSynthesizer._scroll_to_point(obj, x_coord, y_coord, start_offset, end_offset) + + @staticmethod + def scroll_to_top_edge( + obj: Atspi.Accessible, + start_offset: int | None = None, + end_offset: int | None = None + ) -> None: + """Attempts to scroll obj to the top edge.""" + AXEventSynthesizer._scroll_to_location( obj, Atspi.ScrollType.TOP_EDGE, start_offset, end_offset) - AXEventSynthesizer._banner = AXEventSynthesizer._get_obscuring_banner(obj) - if AXEventSynthesizer._banner: - msg = f"AXEventSynthesizer: Re-scrolling {obj} due to banner" - AXEventSynthesizer._scroll_below_banner( - obj, AXEventSynthesizer._banner, start_offset, end_offset) - debug.println(debug.LEVEL_INFO, msg, True) - @staticmethod - def scroll_to_top_left(obj, start_offset=None, end_offset=None): + def scroll_to_top_left( + obj: Atspi.Accessible, + start_offset: int | None = None, + end_offset: int | None = None + ) -> None: """Attempts to scroll obj to the top left.""" AXEventSynthesizer._scroll_to_location( obj, Atspi.ScrollType.TOP_LEFT, start_offset, end_offset) @staticmethod - def scroll_to_left_edge(obj, start_offset=None, end_offset=None): + def scroll_to_left_edge( + obj: Atspi.Accessible, + start_offset: int | None = None, + end_offset: int | None = None + ) -> None: """Attempts to scroll obj to the left edge.""" AXEventSynthesizer._scroll_to_location( obj, Atspi.ScrollType.LEFT_EDGE, start_offset, end_offset) @staticmethod - def scroll_to_bottom_edge(obj, start_offset=None, end_offset=None): + def scroll_to_bottom_edge( + obj: Atspi.Accessible, + start_offset: int | None = None, + end_offset: int | None = None + ) -> None: """Attempts to scroll obj to the bottom edge.""" AXEventSynthesizer._scroll_to_location( obj, Atspi.ScrollType.BOTTOM_EDGE, start_offset, end_offset) @staticmethod - def scroll_to_bottom_right(obj, start_offset=None, end_offset=None): + def scroll_to_bottom_right( + obj: Atspi.Accessible, + start_offset: int | None = None, + end_offset: int | None = None + ) -> None: """Attempts to scroll obj to the bottom right.""" AXEventSynthesizer._scroll_to_location( obj, Atspi.ScrollType.BOTTOM_RIGHT, start_offset, end_offset) @staticmethod - def scroll_to_right_edge(obj, start_offset=None, end_offset=None): + def scroll_to_right_edge( + obj: Atspi.Accessible, + start_offset: int | None = None, + end_offset: int | None = None + ) -> None: """Attempts to scroll obj to the right edge.""" AXEventSynthesizer._scroll_to_location( obj, Atspi.ScrollType.RIGHT_EDGE, start_offset, end_offset) @staticmethod - def try_all_clickable_actions(obj): + def try_all_clickable_actions(obj: Atspi.Accessible) -> bool: """Attempts to perform a click-like action if one is available.""" - actions = ["click", "press", "jump", "open"] + actions = ["click", "press", "jump", "open", "activate"] for action in actions: if AXObject.do_named_action(obj, action): tokens = ["AXEventSynthesizer: '", action, "' on", obj, "performed successfully"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return True if debug.LEVEL_INFO < debug.debugLevel: return False - tokens = ["AXEventSynthesizer: Actions on", obj, ":", AXObject.actions_as_string(obj)] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + tokens = ["AXEventSynthesizer: Actions on", obj, ":", + AXUtilitiesDebugging.actions_as_string(obj)] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return False _synthesizer = AXEventSynthesizer() -def getSynthesizer(): +def get_synthesizer() -> AXEventSynthesizer: + """Returns the Event Synthesizer.""" + return _synthesizer diff --git a/src/cthulhu/ax_hypertext.py b/src/cthulhu/ax_hypertext.py new file mode 100644 index 0000000..4da6dc6 --- /dev/null +++ b/src/cthulhu/ax_hypertext.py @@ -0,0 +1,268 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 Stormux +# Copyright 2024 Igalia, S.L. +# Author: Joanmarie Diggs +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., Franklin Street, Fifth Floor, +# Boston MA 02110-1301 USA. +# +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu + +# pylint: disable=wrong-import-position + +"""Utilities for obtaining information about accessible hypertext and hyperlinks.""" + +__id__ = "$Id$" +__version__ = "$Revision$" +__date__ = "$Date$" +__copyright__ = "Copyright (c) 2024 Igalia, S.L." +__license__ = "LGPL" + +import os +import re +from urllib.parse import urlparse + +import gi +gi.require_version("Atspi", "2.0") +from gi.repository import Atspi +from gi.repository import GLib + +from . import debug +from .ax_object import AXObject + +class AXHypertext: + """Utilities for obtaining information about accessible hypertext and hyperlinks.""" + + @staticmethod + def _get_link_count(obj: Atspi.Accessible) -> int: + """Returns the number of hyperlinks in obj.""" + + if not AXObject.supports_hypertext(obj): + return 0 + + try: + count = Atspi.Hypertext.get_n_links(obj) + except GLib.GError as error: + msg = f"AXHypertext: Exception in _get_link_count: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return 0 + + tokens = ["AXHypertext:", obj, f"reports {count} hyperlinks"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return count + + @staticmethod + def _get_link_at_index(obj: Atspi.Accessible, index: int) -> Atspi.Hyperlink | None: + """Returns the hyperlink object at the specified index.""" + + if not AXObject.supports_hypertext(obj): + return None + + try: + link = Atspi.Hypertext.get_link(obj, index) + except GLib.GError as error: + msg = f"AXHypertext: Exception in _get_link_at_index: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return None + + return link + + @staticmethod + def get_all_links_in_range( + obj: Atspi.Accessible, start_offset: int, end_offset: int + ) -> list[Atspi.Hyperlink]: + """Returns all the hyperlinks in obj who started within the specified range.""" + + links = [] + for i in range(AXHypertext._get_link_count(obj)): + link = AXHypertext._get_link_at_index(obj, i) + if start_offset <= AXHypertext.get_link_start_offset(link) < end_offset \ + or start_offset < AXHypertext.get_link_end_offset(link) <= end_offset: + links.append(link) + + tokens = [f"AXHypertext: {len(links)} hyperlinks found in", obj, + f"between start: {start_offset} and end: {end_offset}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return links + + @staticmethod + def get_all_links(obj: Atspi.Accessible) -> list[Atspi.Hyperlink]: + """Returns a list of all the hyperlinks in obj.""" + + links = [] + for i in range(AXHypertext._get_link_count(obj)): + link = AXHypertext._get_link_at_index(obj, i) + if link is not None: + links.append(link) + + tokens = [f"AXHypertext: {len(links)} hyperlinks found in", obj] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return links + + @staticmethod + def get_link_uri(obj: Atspi.Accessible, index: int = 0) -> str: + """Returns the URI associated with obj at the specified index.""" + + try: + link = Atspi.Accessible.get_hyperlink(obj) + uri = Atspi.Hyperlink.get_uri(link, index) + except GLib.GError as error: + msg = f"AXHypertext: Exception in get_link_uri: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return "" + + tokens = ["AXHypertext: URI of", obj, f"at index {index} is {uri}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return uri + + @staticmethod + def get_link_start_offset(obj: Atspi.Accessible) -> int: + """Returns the start offset of obj in the associated text.""" + + if isinstance(obj, Atspi.Hyperlink): + link = obj + obj = Atspi.Hyperlink.get_object(link, 0) + else: + link = Atspi.Accessible.get_hyperlink(obj) + + if link is None: + tokens = ["AXHypertext: Couldn't get hyperlink for", obj] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return -1 + + try: + offset = Atspi.Hyperlink.get_start_index(link) + except GLib.GError as error: + msg = f"AXHypertext: Exception in get_link_start_offset: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return -1 + + tokens = ["AXHypertext: Start offset of", obj, f"is {offset}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return offset + + @staticmethod + def get_link_end_offset(obj: Atspi.Accessible) -> int: + """Returns the end offset of obj in the associated text.""" + + if isinstance(obj, Atspi.Hyperlink): + link = obj + obj = Atspi.Hyperlink.get_object(link, 0) + else: + link = Atspi.Accessible.get_hyperlink(obj) + + if link is None: + tokens = ["AXHypertext: Couldn't get hyperlink for", obj] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return -1 + + try: + offset = Atspi.Hyperlink.get_end_index(link) + except GLib.GError as error: + msg = f"AXHypertext: Exception in get_link_end_offset: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return -1 + + tokens = ["AXHypertext: End offset of", obj, f"is {offset}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return offset + + @staticmethod + def get_link_basename( + obj: Atspi.Accessible, index: int = 0, remove_extension: bool = False + ) -> str: + """Strip directory and suffix off of the URL associated with obj.""" + + uri = AXHypertext.get_link_uri(obj, index) + if not uri: + return "" + + parsed_uri = urlparse(uri) + basename = os.path.basename(parsed_uri.path) + if remove_extension: + basename = os.path.splitext(basename)[0] + basename = re.sub(r"[-_]", " ", basename) + + tokens = ["AXHypertext: Basename for link", obj, f"is '{basename}'"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return basename + + @staticmethod + def find_child_at_offset(obj: Atspi.Accessible, offset: int) -> Atspi.Accessible | None: + """Attempts to correct for off-by-one brokenness in implementations""" + + if child := AXHypertext.get_child_at_offset(obj, offset): + return child + + if child_before := AXHypertext.get_child_at_offset(obj, offset - 1): + offset_in_parent = AXHypertext.get_character_offset_in_parent(child_before) + if offset_in_parent == offset: + tokens = [f"AXHypertext: Corrected child at offset {offset} in", obj, "is", + child_before, f"at offset {offset - 1}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return child_before + + if child_after := AXHypertext.get_child_at_offset(obj, offset + 1): + offset_in_parent = AXHypertext.get_character_offset_in_parent(child_after) + if offset_in_parent == offset: + tokens = [f"AXHypertext: Corrected child at offset {offset} in", obj, "is", + child_after, f"at offset {offset + 1}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return child_after + + return None + + @staticmethod + def get_child_at_offset(obj: Atspi.Accessible, offset: int) -> Atspi.Accessible | None: + """Returns the embedded-object child of obj at the specified offset.""" + + if not AXObject.supports_hypertext(obj): + return None + + try: + index = Atspi.Hypertext.get_link_index(obj, offset) + except GLib.GError as error: + msg = f"AXHypertext: Exception in get_child_at_offset: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return None + + if index < 0: + return None + + link = AXHypertext._get_link_at_index(obj, index) + if link is None: + return None + + try: + child = Atspi.Hyperlink.get_object(link, 0) + except GLib.GError as error: + msg = f"AXHypertext: Exception in get_child_at_offset: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return None + + tokens = [f"AXHypertext: Child at offset {offset} in", obj, "is", child] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return child + + @staticmethod + def get_character_offset_in_parent(obj: Atspi.Accessible) -> int: + """Returns the offset of the embedded-object obj in the text of its parent.""" + + if not AXObject.supports_text(AXObject.get_parent(obj)): + return -1 + + return AXHypertext.get_link_start_offset(obj) diff --git a/src/cthulhu/ax_object.py b/src/cthulhu/ax_object.py index d28aa17..362ffca 100644 --- a/src/cthulhu/ax_object.py +++ b/src/cthulhu/ax_object.py @@ -1,9 +1,7 @@ -#!/usr/bin/env python3 +# Utilities for obtaining information about accessible objects. # -# Copyright (c) 2024 Stormux -# Copyright (c) 2010-2012 The Orca Team -# Copyright (c) 2012 Igalia, S.L. -# Copyright (c) 2005-2010 Sun Microsystems Inc. +# Copyright 2023 Igalia, S.L. +# Author: Joanmarie Diggs # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -19,19 +17,13 @@ # License along with this library; if not, write to the # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. -# -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca -""" -Utilities for obtaining information about accessible objects. -These utilities are app-type- and toolkit-agnostic. Utilities that might have -different implementations or results depending on the type of app (e.g. terminal, -chat, web) or toolkit (e.g. Qt, Gtk) should be in script_utilities.py file(s). +# pylint: disable=wrong-import-position +# pylint: disable=too-many-lines +# pylint: disable=too-many-return-statements +# pylint: disable=too-many-public-methods -N.B. There are currently utilities that should never have custom implementations -that live in script_utilities.py files. These will be moved over time. -""" +"""Utilities for obtaining information about accessible objects.""" __id__ = "$Id$" __version__ = "$Revision$" @@ -42,25 +34,31 @@ __license__ = "LGPL" import re import threading import time +from typing import Callable, Generator import gi gi.require_version("Atspi", "2.0") +gi.require_version("Gtk", "3.0") from gi.repository import Atspi +from gi.repository import GLib +from gi.repository import Gtk from . import debug +from . import keynames class AXObject: """Utilities for obtaining information about accessible objects.""" - KNOWN_DEAD = {} - REAL_APP_FOR_MUTTER_FRAME = {} - REAL_FRAME_FOR_MUTTER_FRAME = {} + KNOWN_DEAD: dict[int, bool] = {} + OBJECT_ATTRIBUTES: dict[int, dict[str, str]] = {} + REAL_APP_FOR_MUTTER_FRAME: dict[int, Atspi.Accessible] = {} + REAL_FRAME_FOR_MUTTER_FRAME: dict[int, Atspi.Accessible] = {} _lock = threading.Lock() @staticmethod - def _clear_stored_data(): + def _clear_stored_data() -> None: """Clears any data we have cached for objects""" while True: @@ -68,36 +66,24 @@ class AXObject: AXObject._clear_all_dictionaries() @staticmethod - def _clear_all_dictionaries(reason=""): - msg = "AXObject: Clearing cache." + def _clear_all_dictionaries(reason: str = "") -> None: + msg = "AXObject: Clearing local cache." if reason: msg += f" Reason: {reason}" - debug.printMessage(debug.LEVEL_INFO, msg, True) + debug.print_message(debug.LEVEL_INFO, msg, True) with AXObject._lock: - tokens = ["AXObject: Clearing known dead-or-alive state for", - len(AXObject.KNOWN_DEAD), "objects"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) AXObject.KNOWN_DEAD.clear() - - tokens = ["AXObject: Clearing", len(AXObject.REAL_APP_FOR_MUTTER_FRAME), - "real apps for mutter frames"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - AXObject.REAL_APP_FOR_MUTTER_FRAME.clear() - - tokens = ["AXObject: Clearing", len(AXObject.REAL_FRAME_FOR_MUTTER_FRAME), - "real frames for mutter frames"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - AXObject.REAL_FRAME_FOR_MUTTER_FRAME.clear() + AXObject.OBJECT_ATTRIBUTES.clear() @staticmethod - def clear_cache_now(reason=""): + def clear_cache_now(reason: str = "") -> None: """Clears all cached information immediately.""" AXObject._clear_all_dictionaries(reason) @staticmethod - def start_cache_clearing_thread(): + def start_cache_clearing_thread() -> None: """Starts thread to periodically clear cached details.""" thread = threading.Thread(target=AXObject._clear_stored_data) @@ -105,19 +91,90 @@ class AXObject: thread.start() @staticmethod - def is_valid(obj): + def get_toolkit_name(obj: Atspi.Accessible) -> str: + """Returns the toolkit name of obj as a lowercase string""" + + try: + app = Atspi.Accessible.get_application(obj) + name = Atspi.Accessible.get_toolkit_name(app) or "" + except GLib.GError as error: + tokens = ["AXObject: Exception calling _get_toolkit_name_on", app, f": {error}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return "" + + return name.lower() + + @staticmethod + def get_application(obj: Atspi.Accessible) -> Atspi.Accessible | None: + """Returns the accessible application associated with obj.""" + + if obj is None: + return None + + try: + app = Atspi.Accessible.get_application(obj) + except GLib.GError as error: + msg = f"AXObject: Exception in get_application: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return None + return app + + @staticmethod + def is_bogus(obj: Atspi.Accessible) -> bool: + """Hack to ignore certain objects. All entries must have a bug.""" + + # TODO - JD: Periodically check for fixes and remove hacks which are no + # longer needed. + + # https://bugzilla.mozilla.org/show_bug.cgi?id=1879750 + if AXObject.get_role(obj) == Atspi.Role.SECTION \ + and AXObject.get_role(AXObject.get_parent(obj)) == Atspi.Role.FRAME \ + and AXObject.get_toolkit_name(obj) == "gecko": + tokens = ["AXObject:", obj, "is bogus. See mozilla bug 1879750."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True, True) + return True + + return False + + @staticmethod + def has_broken_ancestry(obj: Atspi.Accessible) -> bool: + """Returns True if obj's ancestry is broken.""" + + if obj is None: + return False + + # https://bugreports.qt.io/browse/QTBUG-130116 + toolkit_name = AXObject.get_toolkit_name(obj) + if not toolkit_name.startswith("qt"): + return False + + reached_app = False + parent = AXObject.get_parent(obj) + while parent and not reached_app: + reached_app = AXObject.get_role(parent) == Atspi.Role.APPLICATION + parent = AXObject.get_parent(parent) + + if not reached_app: + tokens = ["AXObject:", obj, "has broken ancestry. See qt bug 130116."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return True + + return False + + @staticmethod + def is_valid(obj: Atspi.Accessible) -> bool: """Returns False if we know for certain this object is invalid""" return not (obj is None or AXObject.object_is_known_dead(obj)) @staticmethod - def object_is_known_dead(obj): + def object_is_known_dead(obj: Atspi.Accessible) -> bool: """Returns True if we know for certain this object no longer exists""" - return obj and AXObject.KNOWN_DEAD.get(hash(obj)) is True + return bool(obj and AXObject.KNOWN_DEAD.get(hash(obj))) is True @staticmethod - def _set_known_dead_status(obj, is_dead): + def _set_known_dead_status(obj: Atspi.Accessible, is_dead: bool) -> None: """Updates the known-dead status of obj""" if obj is None: @@ -130,33 +187,33 @@ class AXObject: AXObject.KNOWN_DEAD[hash(obj)] = is_dead if is_dead: msg = "AXObject: Adding to known dead objects" - debug.printMessage(debug.LEVEL_INFO, msg, True, True) + debug.print_message(debug.LEVEL_INFO, msg, True, True) return if current_status: tokens = ["AXObject: Removing", obj, "from known-dead objects"] - debug.printTokens(debug.LEVEL_INFO, msg, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) @staticmethod - def handle_error(obj, error, msg): + def handle_error(obj: Atspi.Accessible, error: Exception, msg: str) -> None: """Parses the exception and potentially updates our status for obj""" - error = str(error) - if re.search(r"accessible/\d+ does not exist", error): - msg = msg.replace(error, "object no longer exists") - debug.printMessage(debug.LEVEL_INFO, msg, True) - elif re.search(r"The application no longer exists", error): - msg = msg.replace(error, "app no longer exists") - debug.printMessage(debug.LEVEL_INFO, msg, True) + error_string = str(error) + if re.search(r"accessible/\d+ does not exist", error_string): + msg = msg.replace(error_string, "object no longer exists") + debug.print_message(debug.LEVEL_INFO, msg, True) + elif re.search(r"The application no longer exists", error_string): + msg = msg.replace(error_string, "app no longer exists") + debug.print_message(debug.LEVEL_INFO, msg, True) else: - debug.printMessage(debug.LEVEL_INFO, msg, True) + debug.print_message(debug.LEVEL_INFO, msg, True) return if AXObject.KNOWN_DEAD.get(hash(obj)) is False: AXObject._set_known_dead_status(obj, True) @staticmethod - def supports_action(obj): + def supports_action(obj: Atspi.Accessible) -> bool: """Returns True if the action interface is supported on obj""" if not AXObject.is_valid(obj): @@ -164,7 +221,7 @@ class AXObject: try: iface = Atspi.Accessible.get_action_iface(obj) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception calling get_action_iface on {obj}: {error}" AXObject.handle_error(obj, error, msg) return False @@ -172,29 +229,58 @@ class AXObject: return iface is not None @staticmethod - def supports_collection(obj): + def _has_document_spreadsheet(obj: Atspi.Accessible) -> bool: + # To avoid circular import. pylint: disable=import-outside-toplevel + from .ax_collection import AXCollection + rule = AXCollection.create_match_rule(roles=[Atspi.Role.DOCUMENT_SPREADSHEET]) + if rule is None: + return False + + frame = AXObject.find_ancestor_inclusive( + obj, lambda x: AXObject.get_role(x) == Atspi.Role.FRAME) + if frame is None: + return False + return bool(Atspi.Collection.get_matches( + frame, rule, Atspi.CollectionSortOrder.CANONICAL, 1, True)) + + @staticmethod + def supports_collection(obj: Atspi.Accessible) -> bool: """Returns True if the collection interface is supported on obj""" if not AXObject.is_valid(obj): return False - app_name = AXObject.get_name(AXObject.get_application(obj)) - if app_name in ["soffice"]: - tokens = ["AXObject: Treating", app_name, "as not supporting collection."] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + try: + app = Atspi.Accessible.get_application(obj) + except GLib.GError as error: + msg = f"AXObject: Exception in supports_collection: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) return False try: iface = Atspi.Accessible.get_collection_iface(obj) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception calling get_collection_iface on {obj}: {error}" AXObject.handle_error(obj, error, msg) return False - return iface is not None + app_name = AXObject.get_name(app) + if app_name != "soffice": + return iface is not None + + if AXObject.find_ancestor_inclusive( + obj, lambda x: AXObject.get_role(x) == Atspi.Role.DOCUMENT_TEXT): + return True + + if AXObject._has_document_spreadsheet(obj): + msg = "AXObject: Treating soffice as not supporting collection due to spreadsheet." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + return True @staticmethod - def supports_component(obj): + def supports_component(obj: Atspi.Accessible) -> bool: """Returns True if the component interface is supported on obj""" if not AXObject.is_valid(obj): @@ -202,7 +288,7 @@ class AXObject: try: iface = Atspi.Accessible.get_component_iface(obj) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception calling get_component_iface on {obj}: {error}" AXObject.handle_error(obj, error, msg) return False @@ -211,7 +297,7 @@ class AXObject: @staticmethod - def supports_document(obj): + def supports_document(obj: Atspi.Accessible) -> bool: """Returns True if the document interface is supported on obj""" if not AXObject.is_valid(obj): @@ -219,7 +305,7 @@ class AXObject: try: iface = Atspi.Accessible.get_document_iface(obj) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception calling get_document_iface on {obj}: {error}" AXObject.handle_error(obj, error, msg) return False @@ -227,7 +313,59 @@ class AXObject: return iface is not None @staticmethod - def supports_editable_text(obj): + def find_real_app_and_window_for(obj: Atspi.Accessible, app: Atspi.Accessible | None = None): + """Work around for window events coming from mutter-x11-frames.""" + + if app is None: + try: + app = Atspi.Accessible.get_application(obj) + except Exception as error: + msg = f"AXObject: Exception getting application of {obj}: {error}" + AXObject.handle_error(obj, error, msg) + return None, None + + if AXObject.get_name(app) != "mutter-x11-frames": + return app, obj + + real_app = AXObject.REAL_APP_FOR_MUTTER_FRAME.get(hash(obj)) + real_frame = AXObject.REAL_FRAME_FOR_MUTTER_FRAME.get(hash(obj)) + if real_app is not None and real_frame is not None: + return real_app, real_frame + + tokens = ["AXObject:", app, "is not valid app for", obj] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + try: + desktop = Atspi.get_desktop(0) + except Exception as error: + tokens = ["AXObject: Exception getting desktop from Atspi:", error] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return None, None + + name = AXObject.get_name(obj) + real_app = None + real_frame = None + for desktop_app in AXObject.iter_children(desktop): + if AXObject.get_name(desktop_app) == "mutter-x11-frames": + continue + for frame in AXObject.iter_children(desktop_app): + if name == AXObject.get_name(frame): + real_app = desktop_app + real_frame = frame + + tokens = ["AXObject:", real_app, "is real app for", obj] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + if real_frame != obj: + msg = "AXObject: Updated frame to frame from real app" + debug.print_message(debug.LEVEL_INFO, msg, True) + + AXObject.REAL_APP_FOR_MUTTER_FRAME[hash(obj)] = real_app + AXObject.REAL_FRAME_FOR_MUTTER_FRAME[hash(obj)] = real_frame + return real_app, real_frame + + @staticmethod + def supports_editable_text(obj: Atspi.Accessible) -> bool: """Returns True if the editable-text interface is supported on obj""" if not AXObject.is_valid(obj): @@ -235,7 +373,7 @@ class AXObject: try: iface = Atspi.Accessible.get_editable_text_iface(obj) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception calling get_editable_text_iface on {obj}: {error}" AXObject.handle_error(obj, error, msg) return False @@ -243,7 +381,7 @@ class AXObject: return iface is not None @staticmethod - def supports_hyperlink(obj): + def supports_hyperlink(obj: Atspi.Accessible) -> bool: """Returns True if the hyperlink interface is supported on obj""" if not AXObject.is_valid(obj): @@ -251,7 +389,7 @@ class AXObject: try: iface = Atspi.Accessible.get_hyperlink(obj) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception calling get_hyperlink on {obj}: {error}" AXObject.handle_error(obj, error, msg) return False @@ -259,7 +397,7 @@ class AXObject: return iface is not None @staticmethod - def supports_hypertext(obj): + def supports_hypertext(obj: Atspi.Accessible) -> bool: """Returns True if the hypertext interface is supported on obj""" if not AXObject.is_valid(obj): @@ -267,7 +405,7 @@ class AXObject: try: iface = Atspi.Accessible.get_hypertext_iface(obj) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception calling get_hypertext_iface on {obj}: {error}" AXObject.handle_error(obj, error, msg) return False @@ -275,7 +413,7 @@ class AXObject: return iface is not None @staticmethod - def supports_image(obj): + def supports_image(obj: Atspi.Accessible) -> bool: """Returns True if the image interface is supported on obj""" if not AXObject.is_valid(obj): @@ -283,7 +421,7 @@ class AXObject: try: iface = Atspi.Accessible.get_image_iface(obj) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception calling get_image_iface on {obj}: {error}" AXObject.handle_error(obj, error, msg) return False @@ -291,7 +429,7 @@ class AXObject: return iface is not None @staticmethod - def supports_selection(obj): + def supports_selection(obj: Atspi.Accessible) -> bool: """Returns True if the selection interface is supported on obj""" if not AXObject.is_valid(obj): @@ -299,7 +437,7 @@ class AXObject: try: iface = Atspi.Accessible.get_selection_iface(obj) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception calling get_selection_iface on {obj}: {error}" AXObject.handle_error(obj, error, msg) return False @@ -307,7 +445,7 @@ class AXObject: return iface is not None @staticmethod - def supports_table(obj): + def supports_table(obj: Atspi.Accessible) -> bool: """Returns True if the table interface is supported on obj""" if not AXObject.is_valid(obj): @@ -315,7 +453,7 @@ class AXObject: try: iface = Atspi.Accessible.get_table_iface(obj) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception calling get_table_iface on {obj}: {error}" AXObject.handle_error(obj, error, msg) return False @@ -323,7 +461,7 @@ class AXObject: return iface is not None @staticmethod - def supports_table_cell(obj): + def supports_table_cell(obj: Atspi.Accessible) -> bool: """Returns True if the table cell interface is supported on obj""" if not AXObject.is_valid(obj): @@ -331,7 +469,7 @@ class AXObject: try: iface = Atspi.Accessible.get_table_cell(obj) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception calling get_table_cell on {obj}: {error}" AXObject.handle_error(obj, error, msg) return False @@ -339,7 +477,7 @@ class AXObject: return iface is not None @staticmethod - def supports_text(obj): + def supports_text(obj: Atspi.Accessible) -> bool: """Returns True if the text interface is supported on obj""" if not AXObject.is_valid(obj): @@ -347,14 +485,14 @@ class AXObject: try: iface = Atspi.Accessible.get_text_iface(obj) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception calling get_text_iface on {obj}: {error}" AXObject.handle_error(obj, error, msg) return False return iface is not None @staticmethod - def supports_value(obj): + def supports_value(obj: Atspi.Accessible) -> bool: """Returns True if the value interface is supported on obj""" if not AXObject.is_valid(obj): @@ -362,7 +500,7 @@ class AXObject: try: iface = Atspi.Accessible.get_value_iface(obj) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception calling get_value_iface on {obj}: {error}" AXObject.handle_error(obj, error, msg) return False @@ -370,33 +508,7 @@ class AXObject: return iface is not None @staticmethod - def supported_interfaces_as_string(obj): - """Returns the supported interfaces of obj as a string""" - - if not AXObject.is_valid(obj): - return "" - - iface_checks = [ - (AXObject.supports_action, "Action"), - (AXObject.supports_collection, "Collection"), - (AXObject.supports_component, "Component"), - (AXObject.supports_document, "Document"), - (AXObject.supports_editable_text, "EditableText"), - (AXObject.supports_hyperlink, "Hyperlink"), - (AXObject.supports_hypertext, "Hypertext"), - (AXObject.supports_image, "Image"), - (AXObject.supports_selection, "Selection"), - (AXObject.supports_table, "Table"), - (AXObject.supports_table_cell, "TableCell"), - (AXObject.supports_text, "Text"), - (AXObject.supports_value, "Value"), - ] - - ifaces = [iface for check, iface in iface_checks if check(obj)] - return ", ".join(ifaces) - - @staticmethod - def get_path(obj): + def get_path(obj: Atspi.Accessible) -> list[int]: """Returns the path from application to obj as list of child indices""" if not AXObject.is_valid(obj): @@ -407,7 +519,7 @@ class AXObject: while acc: try: path.append(Atspi.Accessible.get_index_in_parent(acc)) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception getting index in parent for {acc}: {error}" AXObject.handle_error(acc, error, msg) return [] @@ -417,7 +529,7 @@ class AXObject: return path @staticmethod - def get_index_in_parent(obj): + def get_index_in_parent(obj: Atspi.Accessible) -> int: """Returns the child index of obj within its parent""" if not AXObject.is_valid(obj): @@ -425,7 +537,7 @@ class AXObject: try: index = Atspi.Accessible.get_index_in_parent(obj) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception in get_index_in_parent: {error}" AXObject.handle_error(obj, error, msg) return -1 @@ -433,7 +545,7 @@ class AXObject: return index @staticmethod - def get_parent(obj): + def get_parent(obj: Atspi.Accessible) -> Atspi.Accessible | None: """Returns the accessible parent of obj. See also get_parent_checked.""" if not AXObject.is_valid(obj): @@ -441,20 +553,25 @@ class AXObject: try: parent = Atspi.Accessible.get_parent(obj) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception in get_parent: {error}" AXObject.handle_error(obj, error, msg) return None if parent == obj: tokens = ["AXObject:", obj, "claims to be its own parent"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return None + if parent is None \ + and AXObject.get_role(obj) not in [Atspi.Role.INVALID, Atspi.Role.DESKTOP_FRAME]: + tokens = ["AXObject:", obj, "claims to have no parent"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return parent @staticmethod - def get_parent_checked(obj): + def get_parent_checked(obj: Atspi.Accessible) -> Atspi.Accessible | None: """Returns the parent of obj, doing checks for tree validity""" if not AXObject.is_valid(obj): @@ -479,7 +596,7 @@ class AXObject: if index < 0 or index >= n_children: tokens = ["AXObject:", obj, "has index", index, "; parent", parent, "has", n_children, "children"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return parent # This performs our check and includes any errors. We don't need the return value here. @@ -487,7 +604,62 @@ class AXObject: return parent @staticmethod - def find_ancestor(obj, pred): + def _get_ancestors(obj: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns a list of the ancestors of obj, starting with its parent.""" + + ancestors = [] + parent = AXObject.get_parent_checked(obj) + while parent: + ancestors.append(parent) + parent = AXObject.get_parent_checked(parent) + ancestors.reverse() + return ancestors + + @staticmethod + def get_common_ancestor( + obj1: Atspi.Accessible, + obj2: Atspi.Accessible + ) -> Atspi.Accessible | None: + """Returns the common ancestor of obj1 and obj2.""" + + tokens = ["AXObject: Looking for common ancestor of", obj1, "and", obj2] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + if not (obj1 and obj2): + return None + + if obj1 == obj2: + return obj1 + + obj1_ancestors = AXObject._get_ancestors(obj1) + [obj1] + obj2_ancestors = AXObject._get_ancestors(obj2) + [obj2] + result = None + for a1, a2 in zip(obj1_ancestors, obj2_ancestors): + if a1 == a2: + result = a1 + else: + break + + tokens = ["AXObject: Common ancestor of", obj1, "and", obj2, "is", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def find_ancestor_inclusive( + obj: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] + ) -> Atspi.Accessible | None: + """Returns obj, or the ancestor of obj, for which the function pred is true""" + + if pred(obj): + return obj + + return AXObject.find_ancestor(obj, pred) + + @staticmethod + def find_ancestor( + obj: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] + ) -> Atspi.Accessible | None: """Returns the ancestor of obj if the function pred is true""" if not AXObject.is_valid(obj): @@ -500,7 +672,7 @@ class AXObject: if parent in objects: tokens = ["AXObject: Circular tree suspected in find_ancestor. ", parent, "already in: ", objects] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return None if pred(parent): @@ -512,8 +684,12 @@ class AXObject: return None @staticmethod - def is_ancestor(obj, ancestor): - """Returns true if ancestor is an ancestor of obj""" + def is_ancestor( + obj: Atspi.Accessible, + ancestor: Atspi.Accessible, + inclusive: bool = False + ) -> bool: + """Returns true if ancestor is an ancestor of obj or, if inclusive, obj is ancestor.""" if not AXObject.is_valid(obj): return False @@ -521,10 +697,13 @@ class AXObject: if not AXObject.is_valid(ancestor): return False + if obj == ancestor and inclusive: + return True + return AXObject.find_ancestor(obj, lambda x: x == ancestor) is not None @staticmethod - def get_child(obj, index): + def get_child(obj: Atspi.Accessible, index: int) -> Atspi.Accessible | None: """Returns the nth child of obj. See also get_child_checked.""" if not AXObject.is_valid(obj): @@ -542,20 +721,22 @@ class AXObject: try: child = Atspi.Accessible.get_child_at_index(obj, index) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception in get_child: {error}" AXObject.handle_error(obj, error, msg) return None if child == obj: tokens = ["AXObject:", obj, "claims to be its own child"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return None return child @staticmethod - def get_child_checked(obj, index): + def get_child_checked( + obj: Atspi.Accessible, index: int + ) -> Atspi.Accessible | None: """Returns the nth child of obj, doing checks for tree validity""" if not AXObject.is_valid(obj): @@ -568,12 +749,15 @@ class AXObject: parent = AXObject.get_parent(child) if obj != parent: tokens = ["AXObject:", obj, "claims", child, "as child; child's parent is", parent] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return child @staticmethod - def get_active_descendant_checked(container, reported_child): + def get_active_descendant_checked( + container: Atspi.Accessible, + reported_child: Atspi.Accessible + ) -> Atspi.Accessible | None: """Checks the reported active descendant and return the real/valid one.""" if not AXObject.has_state(container, Atspi.StateType.MANAGES_DESCENDANTS): @@ -582,20 +766,25 @@ class AXObject: index = AXObject.get_index_in_parent(reported_child) try: real_child = Atspi.Accessible.get_child_at_index(container, index) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception in get_active_descendant_checked: {error}" AXObject.handle_error(container, error, msg) return reported_child if real_child != reported_child: - tokens = ["AXObject: ", container, f"'s child at {index} is ", real_child, - "; not reported child", reported_child] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + tokens = [ + "AXObject: ", container, f"'s child at {index} is ", real_child, + "; not reported child", reported_child + ] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return real_child @staticmethod - def _find_descendant(obj, pred): + def _find_descendant( + obj: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] + ) -> Atspi.Accessible | None: """Returns the descendant of obj if the function pred is true""" if not AXObject.is_valid(obj): @@ -603,27 +792,31 @@ class AXObject: for i in range(AXObject.get_child_count(obj)): child = AXObject.get_child_checked(obj, i) - if child and pred(child): + if child is None: + continue + if pred(child): return child - child = AXObject._find_descendant(child, pred) - if child and pred(child): + if child: return child return None @staticmethod - def find_descendant(obj, pred): + def find_descendant( + obj: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] + ) -> Atspi.Accessible | None: """Returns the descendant of obj if the function pred is true""" start = time.time() result = AXObject._find_descendant(obj, pred) tokens = ["AXObject: find_descendant: found", result, f"in {time.time() - start:.4f}s"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return result @staticmethod - def find_deepest_descendant(obj): + def find_deepest_descendant(obj: Atspi.Accessible) -> Atspi.Accessible | None: """Returns the deepest descendant of obj""" if not AXObject.is_valid(obj): @@ -636,7 +829,12 @@ class AXObject: return AXObject.find_deepest_descendant(last_child) @staticmethod - def _find_all_descendants(obj, include_if, exclude_if, matches): + def _find_all_descendants( + obj: Atspi.Accessible, + include_if: Callable[[Atspi.Accessible], bool] | None, + exclude_if: Callable[[Atspi.Accessible], bool] | None, + matches: list[Atspi.Accessible] + ) -> None: """Returns all descendants which match the specified inclusion and exclusion""" if not AXObject.is_valid(obj): @@ -652,21 +850,25 @@ class AXObject: AXObject._find_all_descendants(child, include_if, exclude_if, matches) @staticmethod - def find_all_descendants(root, include_if=None, exclude_if=None): + def find_all_descendants( + root: Atspi.Accessible, + include_if: Callable[[Atspi.Accessible], bool] | None = None, + exclude_if: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants which match the specified inclusion and exclusion""" start = time.time() - matches = [] + matches: list[Atspi.Accessible] = [] AXObject._find_all_descendants(root, include_if, exclude_if, matches) msg = ( f"AXObject: find_all_descendants: {len(matches)} " f"matches found in {time.time() - start:.4f}s" ) - debug.printMessage(debug.LEVEL_INFO, msg, True) + debug.print_message(debug.LEVEL_INFO, msg, True) return matches @staticmethod - def get_role(obj): + def get_role(obj: Atspi.Accessible) -> Atspi.Role: """Returns the accessible role of obj""" if not AXObject.is_valid(obj): @@ -674,7 +876,7 @@ class AXObject: try: role = Atspi.Accessible.get_role(obj) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception in get_role: {error}" AXObject.handle_error(obj, error, msg) return Atspi.Role.INVALID @@ -683,15 +885,18 @@ class AXObject: return role @staticmethod - def get_role_name(obj): + def get_role_name(obj: Atspi.Accessible, localized: bool = False) -> str: """Returns the accessible role name of obj""" if not AXObject.is_valid(obj): return "" try: - role_name = Atspi.Accessible.get_role_name(obj) - except Exception as error: + if not localized: + role_name = Atspi.Accessible.get_role_name(obj) + else: + role_name = Atspi.Accessible.get_localized_role_name(obj) + except GLib.GError as error: msg = f"AXObject: Exception in get_role_name: {error}" AXObject.handle_error(obj, error, msg) return "" @@ -699,7 +904,37 @@ class AXObject: return role_name @staticmethod - def get_name(obj): + def get_role_description(obj: Atspi.Accessible, is_braille: bool = False) -> str: + """Returns the accessible role description of obj""" + + if not AXObject.is_valid(obj): + return "" + + attrs = AXObject.get_attributes_dict(obj) + rv = attrs.get("roledescription", "") + if is_braille: + rv = attrs.get("brailleroledescription", rv) + return rv + + @staticmethod + def get_accessible_id(obj: Atspi.Accessible) -> str: + """Returns the accessible id of obj""" + + if not AXObject.is_valid(obj): + return "" + + try: + result = Atspi.Accessible.get_accessible_id(obj) + except GLib.GError as error: + msg = f"AXObject: Exception in get_accessible_id: {error}" + AXObject.handle_error(obj, error, msg) + return "" + + AXObject._set_known_dead_status(obj, False) + return result + + @staticmethod + def get_name(obj: Atspi.Accessible) -> str: """Returns the accessible name of obj""" if not AXObject.is_valid(obj): @@ -707,7 +942,7 @@ class AXObject: try: name = Atspi.Accessible.get_name(obj) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception in get_name: {error}" AXObject.handle_error(obj, error, msg) return "" @@ -716,7 +951,7 @@ class AXObject: return name @staticmethod - def has_same_non_empty_name(obj1, obj2): + def has_same_non_empty_name(obj1: Atspi.Accessible, obj2: Atspi.Accessible) -> bool: """Returns true if obj1 and obj2 share the same non-empty name""" name1 = AXObject.get_name(obj1) @@ -726,7 +961,7 @@ class AXObject: return name1 == AXObject.get_name(obj2) @staticmethod - def get_description(obj): + def get_description(obj: Atspi.Accessible) -> str: """Returns the accessible description of obj""" if not AXObject.is_valid(obj): @@ -734,7 +969,7 @@ class AXObject: try: description = Atspi.Accessible.get_description(obj) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception in get_description: {error}" AXObject.handle_error(obj, error, msg) return "" @@ -742,7 +977,56 @@ class AXObject: return description @staticmethod - def get_child_count(obj): + def get_image_description(obj: Atspi.Accessible) -> str: + """Returns the accessible image description of obj""" + + if not AXObject.supports_image(obj): + return "" + + try: + description = Atspi.Image.get_image_description(obj) + except GLib.GError as error: + msg = f"AXObject: Exception in get_image_description: {error}" + AXObject.handle_error(obj, error, msg) + return "" + + return description + + @staticmethod + def get_image_size(obj: Atspi.Accessible) -> tuple[int, int]: + """Returns a (width, height) tuple of the image in obj""" + + if not AXObject.supports_image(obj): + return 0, 0 + + try: + result = Atspi.Image.get_image_size(obj) + except GLib.GError as error: + msg = f"AXObject: Exception in get_image_size: {error}" + AXObject.handle_error(obj, error, msg) + return 0, 0 + + # The return value is an AtspiPoint, hence x and y. + return result.x, result.y + + @staticmethod + def get_help_text(obj: Atspi.Accessible) -> str: + """Returns the accessible help text of obj""" + + if not AXObject.is_valid(obj): + return "" + + try: + # Added in Atspi 2.52. + text = Atspi.Accessible.get_help_text(obj) or "" + except GLib.GError: + # This is for prototyping in the meantime. + text = AXObject.get_attribute(obj, "helptext") or "" + + return text + + @staticmethod + def get_child_count(obj: Atspi.Accessible) -> int: """Returns the child count of obj""" if not AXObject.is_valid(obj): @@ -750,7 +1034,7 @@ class AXObject: try: count = Atspi.Accessible.get_child_count(obj) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception in get_child_count: {error}" AXObject.handle_error(obj, error, msg) return 0 @@ -758,7 +1042,10 @@ class AXObject: return count @staticmethod - def iter_children(obj, pred=None): + def iter_children( + obj: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> Generator[Atspi.Accessible, None, None]: """Generator to iterate through obj's children. If the function pred is specified, children for which pred is False will be skipped.""" @@ -766,13 +1053,22 @@ class AXObject: return child_count = AXObject.get_child_count(obj) + if child_count > 500: + tokens = ["AXObject:", obj, "has more than 500 children"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True, True) + for index in range(child_count): child = AXObject.get_child(obj, index) + if child is None and not AXObject.is_valid(obj): + tokens = ["AXObject:", obj, "is no longer valid"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return + if child is not None and (pred is None or pred(child)): yield child @staticmethod - def get_previous_sibling(obj): + def get_previous_sibling(obj: Atspi.Accessible) -> Atspi.Accessible | None: """Returns the previous sibling of obj, based on child indices""" if not AXObject.is_valid(obj): @@ -789,13 +1085,13 @@ class AXObject: sibling = AXObject.get_child(parent, index - 1) if sibling == obj: tokens = ["AXObject:", obj, "claims to be its own sibling"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return None return sibling @staticmethod - def get_next_sibling(obj): + def get_next_sibling(obj: Atspi.Accessible) -> Atspi.Accessible | None: """Returns the next sibling of obj, based on child indices""" if not AXObject.is_valid(obj): @@ -812,63 +1108,29 @@ class AXObject: sibling = AXObject.get_child(parent, index + 1) if sibling == obj: tokens = ["AXObject:", obj, "claims to be its own sibling"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return None return sibling @staticmethod - def get_next_object(obj): - """Returns the next object (depth first) in the accessibility tree""" + def get_locale(obj: Atspi.Accessible) -> str: + """Returns the locale of obj""" if not AXObject.is_valid(obj): - return None + return "" - index = AXObject.get_index_in_parent(obj) + 1 - parent = AXObject.get_parent(obj) - while parent and not 0 < index < AXObject.get_child_count(parent): - obj = parent - index = AXObject.get_index_in_parent(obj) + 1 - parent = AXObject.get_parent(obj) + try: + locale = Atspi.Accessible.get_object_locale(obj) + except GLib.GError as error: + msg = f"AXObject: Exception in get_locale: {error}" + AXObject.handle_error(obj, error, msg) + return "" - if parent is None: - return None - - next_object = AXObject.get_child(parent, index) - if next_object == obj: - tokens = ["AXObject:", obj, "claims to be its own next object"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return None - - return next_object + return locale or "" @staticmethod - def get_previous_object(obj): - """Returns the previous object (depth first) in the accessibility tree""" - - if not AXObject.is_valid(obj): - return None - - index = AXObject.get_index_in_parent(obj) - 1 - parent = AXObject.get_parent(obj) - while parent and not 0 <= index < AXObject.get_child_count(parent) - 1: - obj = parent - index = AXObject.get_index_in_parent(obj) - 1 - parent = AXObject.get_parent(obj) - - if parent is None: - return None - - previous_object = AXObject.get_child(parent, index) - if previous_object == obj: - tokens = ["AXObject:", obj, "claims to be its own previous object"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return None - - return previous_object - - @staticmethod - def get_state_set(obj): + def get_state_set(obj: Atspi.Accessible) -> Atspi.StateSet: """Returns the state set associated with obj""" if not AXObject.is_valid(obj): @@ -876,16 +1138,21 @@ class AXObject: try: state_set = Atspi.Accessible.get_state_set(obj) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception in get_state_set: {error}" AXObject.handle_error(obj, error, msg) return Atspi.StateSet() + if state_set is None: + tokens = ["AXObject: get_state_set failed for", obj] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return Atspi.StateSet() + AXObject._set_known_dead_status(obj, False) return state_set @staticmethod - def has_state(obj, state): + def has_state(obj: Atspi.Accessible, state: Atspi.StateType) -> bool: """Returns true if obj has the specified state""" if not AXObject.is_valid(obj): @@ -894,264 +1161,37 @@ class AXObject: return AXObject.get_state_set(obj).contains(state) @staticmethod - def state_set_as_string(obj): - """Returns the state set associated with obj as a string""" - - if not AXObject.is_valid(obj): - return "" - - def as_string(state): - return state.value_name[12:].replace("_", "-").lower() - - return ", ".join(map(as_string, AXObject.get_state_set(obj).get_states())) - - @staticmethod - def get_relations(obj): - """Returns the list of Atspi.Relation objects associated with obj""" - - if not AXObject.is_valid(obj): - return [] - - try: - relations = Atspi.Accessible.get_relation_set(obj) - except Exception as error: - msg = f"AXObject: Exception in get_relations: {error}" - AXObject.handle_error(obj, error, msg) - return [] - - return relations - - @staticmethod - def get_relation(obj, relation_type): - """Returns the specified Atspi.Relation for obj""" - - if not AXObject.is_valid(obj): - return None - - for relation in AXObject.get_relations(obj): - if relation and relation.get_relation_type() == relation_type: - return relation - - return None - - @staticmethod - def has_relation(obj, relation_type): - """Returns true if obj has the specified relation type""" - - if not AXObject.is_valid(obj): - return False - - return AXObject.get_relation(obj, relation_type) is not None - - @staticmethod - def get_relation_targets(obj, relation_type, pred=None): - """Returns the list of targets with the specified relation type to obj. - If pred is provided, a target will only be included if pred is true.""" - - if not AXObject.is_valid(obj): - return [] - - relation = AXObject.get_relation(obj, relation_type) - if relation is None: - return [] - - targets = set() - for i in range(relation.get_n_targets()): - target = relation.get_target(i) - if pred is None or pred(target): - targets.add(target) - - # We want to avoid self-referential relationships. - type_includes_object = [Atspi.RelationType.MEMBER_OF] - if relation_type not in type_includes_object and obj in targets: - tokens = ["AXObject: ", obj, "is in its own", relation_type, "target list"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - targets.remove(obj) - - return list(targets) - - @staticmethod - def relations_as_string(obj): - """Returns the relations associated with obj as a string""" - - if not AXObject.is_valid(obj): - return "" - - def as_string(relations): - return relations.value_name[15:].replace("_", "-").lower() - - results = [] - for rel in AXObject.get_relations(obj): - type_string = as_string(rel.get_relation_type()) - targets = AXObject.get_relation_targets(obj, rel.get_relation_type()) - target_string = ",".join(map(str, targets)) - results.append(f"{type_string}: {target_string}") - - return "; ".join(results) - - - @staticmethod - def find_real_app_and_window_for(obj, app=None): - """Work around for window events coming from mutter-x11-frames.""" - - if app is None: - try: - app = Atspi.Accessible.get_application(obj) - except Exception as error: - msg = f"AXObject: Exception getting application of {obj}: {error}" - AXObject.handle_error(obj, error, msg) - return None, None - - if AXObject.get_name(app) != "mutter-x11-frames": - return app, obj - - real_app = AXObject.REAL_APP_FOR_MUTTER_FRAME.get(hash(obj)) - real_frame = AXObject.REAL_FRAME_FOR_MUTTER_FRAME.get(hash(obj)) - if real_app is not None and real_frame is not None: - return real_app, real_frame - - tokens = ["AXObject:", app, "is not valid app for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - - try: - desktop = Atspi.get_desktop(0) - except Exception as error: - tokens = ["AXObject: Exception getting desktop from Atspi:", error] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return None, None - - name = AXObject.get_name(obj) - for desktop_app in AXObject.iter_children(desktop): - if AXObject.get_name(desktop_app) == "mutter-x11-frames": - continue - for frame in AXObject.iter_children(desktop_app): - if name == AXObject.get_name(frame): - real_app = desktop_app - real_frame = frame - - tokens = ["AXObject:", real_app, "is real app for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - - if real_frame != obj: - msg = "AXObject: Updated frame to frame from real app" - debug.printMessage(debug.LEVEL_INFO, msg, True) - - AXObject.REAL_APP_FOR_MUTTER_FRAME[hash(obj)] = real_app - AXObject.REAL_FRAME_FOR_MUTTER_FRAME[hash(obj)] = real_frame - return real_app, real_frame - - @staticmethod - def get_application(obj): - """Returns the accessible application associated with obj""" - - if not AXObject.is_valid(obj): - return None - - app = AXObject.REAL_APP_FOR_MUTTER_FRAME.get(hash(obj)) - if app is not None: - return app - - try: - app = Atspi.Accessible.get_application(obj) - except Exception as error: - msg = f"AXObject: Exception in get_application: {error}" - AXObject.handle_error(obj, error, msg) - return None - - if AXObject.get_name(app) != "mutter-x11-frames": - return app - - real_app = AXObject.find_real_app_and_window_for(obj, app)[0] - if real_app is not None: - app = real_app - - return app - - @staticmethod - def get_application_toolkit_name(obj): - """Returns the toolkit name reported for obj's application.""" - - if not AXObject.is_valid(obj): - return "" - - app = AXObject.get_application(obj) - if app is None: - return "" - - try: - name = Atspi.Accessible.get_toolkit_name(app) - except Exception as error: - tokens = ["AXObject: Exception in get_application_toolkit_name:", error] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return "" - - return name - - @staticmethod - def get_application_toolkit_version(obj): - """Returns the toolkit version reported for obj's application.""" - - if not AXObject.is_valid(obj): - return "" - - app = AXObject.get_application(obj) - if app is None: - return "" - - try: - version = Atspi.Accessible.get_toolkit_version(app) - except Exception as error: - tokens = ["AXObject: Exception in get_application_toolkit_version:", error] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return "" - - return version - - @staticmethod - def application_as_string(obj): - """Returns the application details of obj as a string.""" - - if not AXObject.is_valid(obj): - return "" - - app = AXObject.get_application(obj) - if app is None: - return "" - - string = ( - f"{AXObject.get_name(app)} " - f"({AXObject.get_application_toolkit_name(obj)} " - f"{AXObject.get_application_toolkit_version(obj)})" - ) - return string - - @staticmethod - def clear_cache(obj, recursive=False): + def clear_cache( + obj: Atspi.Accessible, + recursive: bool = False, + reason: str = "" + ) -> None: """Clears the Atspi cached information associated with obj""" - if not AXObject.is_valid(obj): + if obj is None: return + tokens = ["AXObject: Clearing AT-SPI cache on", obj, f"Recursive: {recursive}."] + if reason: + tokens.append(f" Reason: {reason}") + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + if not recursive: try: Atspi.Accessible.clear_cache_single(obj) - except Exception: - # This is new API, added in 2.49.1. So log success rather than - # (likely) failure for now. - pass - else: - msg = "AXObject: clear_cache_single succeeded." - debug.printMessage(debug.LEVEL_INFO, msg, True) - return + except GLib.GError as error: + msg = f"AXObject: Exception in clear_cache_single: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return try: Atspi.Accessible.clear_cache(obj) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception in clear_cache: {error}" AXObject.handle_error(obj, error, msg) @staticmethod - def get_process_id(obj): + def get_process_id(obj: Atspi.Accessible) -> int: """Returns the process id associated with obj""" if not AXObject.is_valid(obj): @@ -1159,7 +1199,7 @@ class AXObject: try: pid = Atspi.Accessible.get_process_id(obj) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception in get_process_id: {error}" AXObject.handle_error(obj, error, msg) return -1 @@ -1167,7 +1207,7 @@ class AXObject: return pid @staticmethod - def is_dead(obj): + def is_dead(obj: Atspi.Accessible) -> bool: """Returns true of obj exists but is believed to be dead.""" if obj is None: @@ -1180,7 +1220,7 @@ class AXObject: # We use the Atspi function rather than the AXObject function because the # latter intentionally handles exceptions. Atspi.Accessible.get_name(obj) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Accessible is dead: {error}" AXObject.handle_error(obj, error, msg) return True @@ -1189,15 +1229,23 @@ class AXObject: return False @staticmethod - def get_attributes_dict(obj): + def get_attributes_dict( + obj: Atspi.Accessible, + use_cache: bool = True + ) -> dict[str, str]: """Returns the object attributes of obj as a dictionary.""" if not AXObject.is_valid(obj): return {} + if use_cache: + attributes = AXObject.OBJECT_ATTRIBUTES.get(hash(obj)) + if attributes: + return attributes + try: attributes = Atspi.Accessible.get_attributes(obj) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception in get_attributes_dict: {error}" AXObject.handle_error(obj, error, msg) return {} @@ -1205,32 +1253,25 @@ class AXObject: if attributes is None: return {} + AXObject.OBJECT_ATTRIBUTES[hash(obj)] = attributes return attributes @staticmethod - def get_attribute(obj, attribute_name): + def get_attribute( + obj: Atspi.Accessible, + attribute_name: str, + use_cache: bool = True + ) -> str: """Returns the value of the specified attribute as a string.""" if not AXObject.is_valid(obj): return "" - attributes = AXObject.get_attributes_dict(obj) + attributes = AXObject.get_attributes_dict(obj, use_cache) return attributes.get(attribute_name, "") @staticmethod - def attributes_as_string(obj): - """Returns the object attributes of obj as a string.""" - - if not AXObject.is_valid(obj): - return "" - - def as_string(attribute): - return f"{attribute[0]}:{attribute[1]}" - - return ", ".join(map(as_string, AXObject.get_attributes_dict(obj).items())) - - @staticmethod - def get_n_actions(obj): + def get_n_actions(obj: Atspi.Accessible) -> int: """Returns the number of actions supported on obj.""" if not AXObject.supports_action(obj): @@ -1238,7 +1279,7 @@ class AXObject: try: count = Atspi.Action.get_n_actions(obj) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception in get_n_actions: {error}" AXObject.handle_error(obj, error, msg) return 0 @@ -1246,7 +1287,7 @@ class AXObject: return count @staticmethod - def _normalize_action_name(action_name): + def _normalize_action_name(action_name: str) -> str: """Adjusts the name to account for differences in implementations.""" if not action_name: @@ -1257,7 +1298,7 @@ class AXObject: return name @staticmethod - def get_action_name(obj, i): + def get_action_name(obj: Atspi.Accessible, i: int) -> str: """Returns the name of obj's action at index i.""" if not 0 <= i < AXObject.get_n_actions(obj): @@ -1265,7 +1306,7 @@ class AXObject: try: name = Atspi.Action.get_action_name(obj, i) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception in get_action_name: {error}" AXObject.handle_error(obj, error, msg) return "" @@ -1273,7 +1314,7 @@ class AXObject: return AXObject._normalize_action_name(name) @staticmethod - def get_action_names(obj): + def get_action_names(obj: Atspi.Accessible) -> list[str]: """Returns the list of actions supported on obj.""" results = [] @@ -1284,7 +1325,7 @@ class AXObject: return results @staticmethod - def get_action_description(obj, i): + def get_action_description(obj: Atspi.Accessible, i: int) -> str: """Returns the description of obj's action at index i.""" if not 0 <= i < AXObject.get_n_actions(obj): @@ -1292,7 +1333,7 @@ class AXObject: try: description = Atspi.Action.get_action_description(obj, i) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception in get_action_description: {error}" AXObject.handle_error(obj, error, msg) return "" @@ -1300,7 +1341,23 @@ class AXObject: return description @staticmethod - def get_action_key_binding(obj, i): + def get_action_localized_name(obj: Atspi.Accessible, i: int) -> str: + """Returns the localized name of obj's action at index i.""" + + if not 0 <= i < AXObject.get_n_actions(obj): + return "" + + try: + name = Atspi.Action.get_localized_name(obj, i) + except GLib.GError as error: + msg = f"AXObject: Exception in get_action_localized_name: {error}" + AXObject.handle_error(obj, error, msg) + return "" + + return name + + @staticmethod + def get_action_key_binding(obj: Atspi.Accessible, i: int) -> str: """Returns the key binding string of obj's action at index i.""" if not 0 <= i < AXObject.get_n_actions(obj): @@ -1308,21 +1365,127 @@ class AXObject: try: keybinding = Atspi.Action.get_key_binding(obj, i) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception in get_action_key_binding: {error}" AXObject.handle_error(obj, error, msg) return "" + # GTK4 does this. + if keybinding == "": + return "" return keybinding @staticmethod - def has_action(obj, action_name): + def _get_label_for_key_sequence(sequence: str) -> str: + """Returns the human consumable label for the key sequence.""" + + if not sequence: + return "" + + # We get all sorts of variations in the keybinding string. Try to normalize it. + if len(sequence) > 1 and not sequence.startswith("<") and "," not in sequence: + tokens = sequence.split("+") + sequence = "".join(f"<{part}>" for part in tokens[:-1]) + tokens[-1] + + # We use Gtk for conversion to handle things like . + try: + key, mods = Gtk.accelerator_parse(sequence) + result = Gtk.accelerator_get_label(key, mods) + except GLib.GError as error: + msg = f"AXObject: Exception in _get_label_for_key_sequence: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + sequence = sequence.replace("<", "").replace(">", " ").strip() + else: + if result and not result.endswith("+"): + sequence = result + + return keynames.localize_key_sequence(sequence) + + @staticmethod + def get_accelerator(obj: Atspi.Accessible) -> str: + """Returns the accelerator/shortcut associated with obj.""" + + attrs = AXObject.get_attributes_dict(obj) + # The ARIA spec suggests a given shortcut's components should be separated by a "+". + # Multiple shortcuts are apparently allowed and separated by a space. + shortcuts = attrs.get("keyshortcuts", "").split(" ") + if shortcuts and shortcuts[0]: + result = " ".join(map(AXObject._get_label_for_key_sequence, shortcuts)).strip() + # Accelerators are typically modified and thus more than one character. + if len(result) > 1: + return result + + index = AXObject._find_first_action_with_keybinding(obj) + if index == -1: + return "" + + # This should be a string separated by semicolons and in the form: + # ;; (optional) + # In practice we get all sorts of variations. + + # If there's a third item, it's probably the accelerator. + strings = AXObject.get_action_key_binding(obj, index).split(";") + if len(strings) == 3: + return AXObject._get_label_for_key_sequence(strings[2]) + + # If the last thing has Ctrl in it, it's probably the accelerator. + result = AXObject._get_label_for_key_sequence(strings[-1]) + if "Ctrl" in result: + return result + + return "" + + @staticmethod + def get_mnemonic(obj: Atspi.Accessible) -> str: + """Returns the mnemonic associated with obj.""" + + attrs = AXObject.get_attributes_dict(obj) + # The ARIA spec suggests a given shortcut's components should be separated by a "+". + # Multiple shortcuts are apparently allowed and separated by a space. + shortcuts = attrs.get("keyshortcuts", "").split(" ") + if shortcuts and shortcuts[0]: + result = " ".join(map(AXObject._get_label_for_key_sequence, shortcuts)).strip() + # If it's not a single letter it's probably not the mnemonic. + if len(result) == 1: + return result + + index = AXObject._find_first_action_with_keybinding(obj) + if index == -1: + return "" + + # This should be a string separated by semicolons and in the form: + # ;; (optional) + # In practice we get all sorts of variations. + + strings = AXObject.get_action_key_binding(obj, index).split(";") + result = AXObject._get_label_for_key_sequence(strings[0]) + # If Ctrl is in the result, it's probably the accelerator rather than the mnemonic. + if "Ctrl" in result or "Control" in result: + return "" + + # Don't treat space as a mnemonic. + if result.lower() in [" ", "space", ""]: + return "" + + return result + + @staticmethod + def _find_first_action_with_keybinding(obj: Atspi.Accessible) -> int: + """Returns the index of the first action with a keybinding on obj.""" + + for i in range(AXObject.get_n_actions(obj)): + if AXObject.get_action_key_binding(obj, i): + return i + return -1 + + @staticmethod + def has_action(obj: Atspi.Accessible, action_name: str) -> bool: """Returns true if the named action is supported on obj.""" return AXObject.get_action_index(obj, action_name) >= 0 @staticmethod - def get_action_index(obj, action_name): + def get_action_index(obj: Atspi.Accessible, action_name: str) -> int: """Returns the index of the named action or -1 if unsupported.""" action_name = AXObject._normalize_action_name(action_name) @@ -1333,7 +1496,7 @@ class AXObject: return -1 @staticmethod - def do_action(obj, i): + def do_action(obj: Atspi.Accessible, i: int) -> bool: """Invokes obj's action at index i. The return value, if true, may be meaningless because most implementors return true without knowing if the action was successfully performed.""" @@ -1343,7 +1506,7 @@ class AXObject: try: result = Atspi.Action.do_action(obj, i) - except Exception as error: + except GLib.GError as error: msg = f"AXObject: Exception in do_action: {error}" AXObject.handle_error(obj, error, msg) return False @@ -1351,7 +1514,7 @@ class AXObject: return result @staticmethod - def do_named_action(obj, action_name): + def do_named_action(obj: Atspi.Accessible, action_name: str) -> bool: """Invokes the named action on obj. The return value, if true, may be meaningless because most implementors return true without knowing if the action was successfully performed.""" @@ -1359,23 +1522,32 @@ class AXObject: index = AXObject.get_action_index(obj, action_name) if index == -1: tokens = ["INFO:", action_name, "not an available action for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return False return AXObject.do_action(obj, index) @staticmethod - def actions_as_string(obj): - """Returns information about the actions as a string.""" + def grab_focus(obj: Atspi.Accessible) -> bool: + """Attempts to grab focus on obj. Returns true if successful.""" - results = [] - for i in range(AXObject.get_n_actions(obj)): - result = AXObject.get_action_name(obj, i) - keybinding = AXObject.get_action_key_binding(obj, i) - if keybinding: - result += f" ({keybinding})" - results.append(result) + if not AXObject.supports_component(obj): + return False - return "; ".join(results) + try: + result = Atspi.Component.grab_focus(obj) + except GLib.GError as error: + msg = f"AXObject: Exception in grab_focus: {error}" + AXObject.handle_error(obj, error, msg) + return False + + if debug.LEVEL_INFO < debug.debugLevel: + return result + + if result and not AXObject.has_state(obj, Atspi.StateType.FOCUSED): + tokens = ["AXObject:", obj, "lacks focused state after focus grab"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + return result AXObject.start_cache_clearing_thread() diff --git a/src/cthulhu/ax_selection.py b/src/cthulhu/ax_selection.py index 6926699..903dd5e 100644 --- a/src/cthulhu/ax_selection.py +++ b/src/cthulhu/ax_selection.py @@ -1,9 +1,7 @@ -#!/usr/bin/env python3 +# Utilities for obtaining information about containers supporting selection # -# Copyright (c) 2024 Stormux -# Copyright (c) 2010-2012 The Orca Team -# Copyright (c) 2012 Igalia, S.L. -# Copyright (c) 2005-2010 Sun Microsystems Inc. +# Copyright 2023 Igalia, S.L. +# Author: Joanmarie Diggs # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -19,19 +17,10 @@ # License along with this library; if not, write to the # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. -# -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca -""" -Utilities for obtaining information about containers supporting selection. -These utilities are app-type- and toolkit-agnostic. Utilities that might have -different implementations or results depending on the type of app (e.g. terminal, -chat, web) or toolkit (e.g. Qt, Gtk) should be in script_utilities.py file(s). +# pylint: disable=wrong-import-position -N.B. There are currently utilities that should never have custom implementations -that live in script_utilities.py files. These will be moved over time. -""" +"""Utilities for obtaining information about containers supporting selection.""" __id__ = "$Id$" __version__ = "$Revision$" @@ -40,19 +29,20 @@ __copyright__ = "Copyright (c) 2023 Igalia, S.L." __license__ = "LGPL" import gi - gi.require_version("Atspi", "2.0") from gi.repository import Atspi +from gi.repository import GLib from . import debug from .ax_object import AXObject +from .ax_utilities_role import AXUtilitiesRole class AXSelection: """Utilities for obtaining information about containers supporting selection.""" @staticmethod - def get_selected_child_count(obj): + def get_selected_child_count(obj: Atspi.Accessible) -> int: """Returns the selected child count of obj""" if not AXObject.supports_selection(obj): @@ -60,17 +50,17 @@ class AXSelection: try: count = Atspi.Selection.get_n_selected_children(obj) - except Exception as error: + except GLib.GError as error: tokens = ["AXSelection: Exception in get_selected_child_count:", error] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return 0 tokens = ["AXSelection:", obj, "reports", count, "selected children"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return count @staticmethod - def get_selected_child(obj, index): + def get_selected_child(obj: Atspi.Accessible, index: int) -> Atspi.Accessible | None: """Returns the nth selected child of obj.""" n_children = AXSelection.get_selected_child_count(obj) @@ -85,32 +75,40 @@ class AXSelection: try: child = Atspi.Selection.get_selected_child(obj, index) - except Exception as error: + except GLib.GError as error: tokens = ["AXSelection: Exception in get_selected_child:", error] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return None if child == obj: tokens = ["AXSelection:", obj, "claims to be its own selected child"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return None tokens = ["AXSelection:", child, "is selected child #", index, "of", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return child @staticmethod - def get_selected_children(obj): + def get_selected_children(obj: Atspi.Accessible) -> list[Atspi.Accessible]: """Returns a list of all the selected children of obj.""" + if obj is None: + return [] + count = AXSelection.get_selected_child_count(obj) + if not count and AXUtilitiesRole.is_combo_box(obj): + container = AXObject.find_descendant( + obj, lambda x: AXUtilitiesRole.is_menu(x) or AXUtilitiesRole.is_list_box(x)) + return AXSelection.get_selected_children(container) + children = set() for i in range(count): try: child = Atspi.Selection.get_selected_child(obj, i) - except Exception as error: + except GLib.GError as error: tokens = ["AXSelection: Exception in get_selected_children:", error] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return [] if child is not None: @@ -118,12 +116,12 @@ class AXSelection: if obj in children: tokens = ["AXSelection:", obj, "claims to be its own selected child"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) children.remove(obj) result = list(children) if len(result) != count: tokens = ["AXSelection: Selected child count of", obj, f"is {count}"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return result diff --git a/src/cthulhu/ax_table.py b/src/cthulhu/ax_table.py new file mode 100644 index 0000000..7dc4414 --- /dev/null +++ b/src/cthulhu/ax_table.py @@ -0,0 +1,1373 @@ +# Utilities for obtaining information about accessible tables. +# +# Copyright 2023 Igalia, S.L. +# Copyright 2023 GNOME Foundation Inc. +# Author: Joanmarie Diggs +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., Franklin Street, Fifth Floor, +# Boston MA 02110-1301 USA. + +# pylint: disable=wrong-import-position +# pylint: disable=too-many-lines +# pylint: disable=too-many-public-methods +# pylint: disable=too-many-return-statements + +"""Utilities for obtaining information about accessible tables.""" + +__id__ = "$Id$" +__version__ = "$Revision$" +__date__ = "$Date$" +__copyright__ = "Copyright (c) 2023 Igalia, S.L." \ + "Copyright (c) 2023 GNOME Foundation Inc." +__license__ = "LGPL" + +import threading +import time +from typing import Generator + +import gi +gi.require_version("Atspi", "2.0") +from gi.repository import Atspi +from gi.repository import GLib + +from . import debug +from . import messages +from . import object_properties +from .ax_object import AXObject +from .ax_component import AXComponent +from .ax_utilities_role import AXUtilitiesRole +from .ax_utilities_state import AXUtilitiesState + + +class AXTable: + """Utilities for obtaining information about accessible tables.""" + + # Things we cache. + CAPTIONS: dict[int, Atspi.Accessible] = {} + PHYSICAL_COORDINATES_FROM_CELL: dict[int, tuple[int, int]] = {} + PHYSICAL_COORDINATES_FROM_TABLE: dict[int, tuple[int, int]] = {} + PHYSICAL_SPANS_FROM_CELL: dict[int, tuple[int, int]] = {} + PHYSICAL_SPANS_FROM_TABLE: dict[int, tuple[int, int]] = {} + PHYSICAL_COLUMN_COUNT: dict[int, int] = {} + PHYSICAL_ROW_COUNT: dict[int, int] = {} + PRESENTABLE_COORDINATES: dict[int, tuple[str | None, str | None]] = {} + PRESENTABLE_COORDINATES_LABELS: dict[int, str] = {} + PRESENTABLE_SPANS: dict[int, tuple[str | None, str | None]] = {} + PRESENTABLE_COLUMN_COUNT: dict[int, int | None] = {} + PRESENTABLE_ROW_COUNT: dict[int, int | None] = {} + COLUMN_HEADERS_FOR_CELL: dict[int, list[Atspi.Accessible]] = {} + ROW_HEADERS_FOR_CELL: dict[int, list[Atspi.Accessible]] = {} + + # Things which have to be explicitly cleared. + DYNAMIC_COLUMN_HEADERS_ROW: dict[int, int] = {} + DYNAMIC_ROW_HEADERS_COLUMN: dict[int, int] = {} + + _lock = threading.Lock() + + @staticmethod + def start_cache_clearing_thread() -> None: + """Starts thread to periodically clear cached details.""" + + thread = threading.Thread(target=AXTable._clear_stored_data) + thread.daemon = True + thread.start() + + @staticmethod + def _clear_stored_data() -> None: + """Clears any data we have cached for objects""" + + while True: + time.sleep(60) + AXTable._clear_all_dictionaries() + + @staticmethod + def _clear_all_dictionaries(reason: str = "") -> None: + msg = "AXTable: Clearing cache." + if reason: + msg += f" Reason: {reason}" + debug.print_message(debug.LEVEL_INFO, msg, True) + + with AXTable._lock: + AXTable.CAPTIONS.clear() + AXTable.PHYSICAL_COORDINATES_FROM_CELL.clear() + AXTable.PHYSICAL_COORDINATES_FROM_TABLE.clear() + AXTable.PHYSICAL_SPANS_FROM_CELL.clear() + AXTable.PHYSICAL_SPANS_FROM_TABLE.clear() + AXTable.PHYSICAL_COLUMN_COUNT.clear() + AXTable.PHYSICAL_ROW_COUNT.clear() + AXTable.PRESENTABLE_COORDINATES.clear() + AXTable.PRESENTABLE_COORDINATES_LABELS.clear() + AXTable.PRESENTABLE_COLUMN_COUNT.clear() + AXTable.PRESENTABLE_ROW_COUNT.clear() + AXTable.COLUMN_HEADERS_FOR_CELL.clear() + AXTable.ROW_HEADERS_FOR_CELL.clear() + + @staticmethod + def clear_cache_now(reason: str = "") -> None: + """Clears all cached information immediately.""" + + AXTable._clear_all_dictionaries(reason) + + @staticmethod + def get_caption(table: Atspi.Accessible) -> Atspi.Accessible | None: + """Returns the accessible object containing the caption of table.""" + + if not AXObject.supports_table(table): + return None + + if hash(table) in AXTable.CAPTIONS: + return AXTable.CAPTIONS.get(hash(table)) + + try: + caption = Atspi.Table.get_caption(table) + except GLib.GError as error: + msg = f"AXTable: Exception in get_caption: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return None + + tokens = ["AXTable: Caption for", table, "is", caption] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + AXTable.CAPTIONS[hash(table)] = caption + return caption + + @staticmethod + def get_column_count(table: Atspi.Accessible, prefer_attribute: bool = True) -> int: + """Returns the column count of table.""" + + if not AXObject.supports_table(table): + return -1 + + if prefer_attribute: + count = AXTable._get_column_count_from_attribute(table) + if count is not None: + return count + + count = AXTable.PHYSICAL_COLUMN_COUNT.get(hash(table)) + if count is not None: + return count + + try: + count = Atspi.Table.get_n_columns(table) + except GLib.GError as error: + msg = f"AXTable: Exception in get_column_count: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return -1 + + tokens = ["AXTable: Column count for", table, "is", count] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + AXTable.PHYSICAL_COLUMN_COUNT[hash(table)] = count + return count + + @staticmethod + def _get_column_count_from_attribute(table: Atspi.Accessible) -> int | None: + """Returns the value of the 'colcount' object attribute or None if not found.""" + + if hash(table) in AXTable.PRESENTABLE_COLUMN_COUNT: + return AXTable.PRESENTABLE_COLUMN_COUNT.get(hash(table)) + + attrs = AXObject.get_attributes_dict(table) + attr = attrs.get("colcount") + count = None + if attr is not None: + count = int(attr) + + tokens = ["AXTable: Column count attribute for", table, "is", count] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + AXTable.PRESENTABLE_COLUMN_COUNT[hash(table)] = count + return count + + @staticmethod + def get_row_count(table: Atspi.Accessible, prefer_attribute: bool = True) -> int: + """Returns the row count of table.""" + + if not AXObject.supports_table(table): + return -1 + + if prefer_attribute: + count = AXTable._get_row_count_from_attribute(table) + if count is not None: + return count + + count = AXTable.PHYSICAL_ROW_COUNT.get(hash(table)) + if count is not None: + return count + + try: + count = Atspi.Table.get_n_rows(table) + except GLib.GError as error: + msg = f"AXTable: Exception in get_row_count: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return -1 + + tokens = ["AXTable: Row count for", table, "is", count] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + AXTable.PHYSICAL_ROW_COUNT[hash(table)] = count + return count + + @staticmethod + def _get_row_count_from_attribute(table: Atspi.Accessible) -> int | None: + """Returns the value of the 'rowcount' object attribute or None if not found.""" + + if hash(table) in AXTable.PRESENTABLE_ROW_COUNT: + return AXTable.PRESENTABLE_ROW_COUNT.get(hash(table)) + + attrs = AXObject.get_attributes_dict(table) + attr = attrs.get("rowcount") + count = None + if attr is not None: + count = int(attr) + + tokens = ["AXTable: Row count attribute for", table, "is", count] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + AXTable.PRESENTABLE_ROW_COUNT[hash(table)] = count + return count + + @staticmethod + def is_non_uniform_table( + table: Atspi.Accessible, max_rows: int = 25, max_cols: int = 25 + ) -> bool: + """Returns True if table has at least one cell with a span > 1.""" + + for row in range(min(max_rows, AXTable.get_row_count(table, False))): + for col in range(min(max_cols, AXTable.get_column_count(table, False))): + try: + if Atspi.Table.get_row_extent_at(table, row, col) > 1: + return True + if Atspi.Table.get_column_extent_at(table, row, col) > 1: + return True + except GLib.GError as error: + msg = f"AXTable: Exception in is_non_uniform_table: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + return False + + @staticmethod + def get_selected_column_count(table: Atspi.Accessible) -> int: + """Returns the number of selected columns in table.""" + + if not AXObject.supports_table(table): + return 0 + + try: + count = Atspi.Table.get_n_selected_columns(table) + except GLib.GError as error: + msg = f"AXTable: Exception in get_selected_column_count {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return 0 + + tokens = ["AXTable: Selected column count for", table, "is", count] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return count + + @staticmethod + def get_selected_columns(table: Atspi.Accessible) -> list[int]: + """Returns a list of column indices for the selected columns in table.""" + + if not AXObject.supports_table(table): + return [] + + try: + columns = Atspi.Table.get_selected_columns(table) + except GLib.GError as error: + msg = f"AXTable: Exception in get_selected_columns: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return [] + + tokens = ["AXTable: Selected columns for", table, "are", columns] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return columns + + @staticmethod + def get_selected_row_count(table: Atspi.Accessible) -> int: + """Returns the number of selected rows in table.""" + + if not AXObject.supports_table(table): + return 0 + + try: + count = Atspi.Table.get_n_selected_rows(table) + except GLib.GError as error: + msg = f"AXTable: Exception in get_selected_row_count {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return 0 + + tokens = ["AXTable: Selected row count for", table, "is", count] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return count + + @staticmethod + def get_selected_rows(table: Atspi.Accessible) -> list[int]: + """Returns a list of row indices for the selected rows in table.""" + + if not AXObject.supports_table(table): + return [] + + try: + rows = Atspi.Table.get_selected_rows(table) + except GLib.GError as error: + msg = f"AXTable: Exception in get_selected_rows: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return [] + + tokens = ["AXTable: Selected rows for", table, "are", rows] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return rows + + @staticmethod + def all_cells_are_selected(table: Atspi.Accessible) -> bool: + """Returns True if all cells in table are selected.""" + + if not AXObject.supports_table(table): + return False + + rows = AXTable.get_row_count(table, prefer_attribute=False) + if rows <= 0: + return False + + if AXTable.get_selected_row_count(table) == rows: + return True + + cols = AXTable.get_column_count(table, prefer_attribute=False) + return AXTable.get_selected_column_count(table) == cols + + @staticmethod + def get_cell_at(table: Atspi.Accessible, row: int, column: int) -> Atspi.Accessible | None: + """Returns the cell at the 0-indexed row and column.""" + + if not AXObject.supports_table(table): + return None + + try: + cell = Atspi.Table.get_accessible_at(table, row, column) + except GLib.GError as error: + tokens = [f"AXTable: Exception getting cell at row: {row} col: {column} in", table, + ":", error] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return None + + tokens = [f"AXTable: Cell at row: {row} col: {column} in", table, "is", cell] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return cell + + @staticmethod + def _get_cell_index(cell: Atspi.Accessible) -> int: + """Returns the index of cell to be used with the table interface.""" + + index = AXObject.get_attribute(cell, "table-cell-index") + if index is not None and index != "": + return int(index) + + # We might have nested cells. So far this has only been seen in Gtk, + # where the parent of a table cell is also a table cell. We need the + # index of the parent for use with the table interface. + parent = AXObject.get_parent(cell) + if AXObject.get_role(parent) == Atspi.Role.TABLE_CELL: + cell = parent + + return AXObject.get_index_in_parent(cell) + + @staticmethod + def get_cell_spans(cell: Atspi.Accessible, prefer_attribute: bool = True) -> tuple[int, int]: + """Returns the row and column spans.""" + + if not AXUtilitiesRole.is_table_cell_or_header(cell): + return -1, -1 + + if AXObject.supports_table_cell(cell): + row_span, col_span = AXTable._get_cell_spans_from_table_cell(cell) + else: + row_span, col_span = AXTable._get_cell_spans_from_table(cell) + + if not prefer_attribute: + return row_span, col_span + + rowspan_attr, colspan_attr = AXTable._get_cell_spans_from_attribute(cell) + if rowspan_attr is not None: + row_span = int(rowspan_attr) + if colspan_attr is not None: + col_span = int(colspan_attr) + + return row_span, col_span + + @staticmethod + def _get_cell_spans_from_attribute( + cell: Atspi.Accessible + ) -> tuple[str | None, str | None]: + """Returns the row and column spans exposed via object attribute, or None, None.""" + + if hash(cell) in AXTable.PRESENTABLE_SPANS: + return AXTable.PRESENTABLE_SPANS.get(hash(cell), (None, None)) + + attrs = AXObject.get_attributes_dict(cell) + row_span = attrs.get("rowspan", None) + col_span = attrs.get("colspan", None) + + tokens = ["AXTable: Row and col span attributes for", cell, ":", row_span, ",", col_span] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + AXTable.PRESENTABLE_SPANS[hash(cell)] = row_span, col_span + return row_span, col_span + + @staticmethod + def _get_cell_spans_from_table(cell: Atspi.Accessible) -> tuple[int, int]: + """Returns the row and column spans of cell via the table interface.""" + + if hash(cell) in AXTable.PHYSICAL_SPANS_FROM_TABLE: + return AXTable.PHYSICAL_SPANS_FROM_TABLE.get(hash(cell), (-1, -1)) + + index = AXTable._get_cell_index(cell) + if index < 0: + return -1, -1 + + table = AXTable.get_table(cell) + if table is None: + return -1, -1 + + if not AXObject.supports_table(table): + return -1, -1 + + # Cells in a tree are expected to not span multiple rows or columns. + # Also this: https://bugreports.qt.io/browse/QTBUG-119167 + if AXUtilitiesRole.is_tree(table): + return 1, 1 + + try: + result = Atspi.Table.get_row_column_extents_at_index(table, index) + except GLib.GError as error: + msg = f"AXTable: Exception in _get_cell_spans_from_table: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return -1, -1 + + if result is None: + tokens = ["AXTable: get_row_column_extents_at_index failed for", cell, + f"at index {index} in", table] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return -1, -1 + + if not result[0]: + return -1, -1 + + row_span = result.row_extents + row_count = AXTable.get_row_count(table, False) + if row_span > row_count: + tokens = ["AXTable: Table iface row span for", cell, + f"{row_span} is greater than row count: {row_count}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + row_span = 1 + + col_span = result.col_extents + col_count = AXTable.get_column_count(table, False) + if col_span > col_count: + tokens = ["AXTable: Table iface col span for", cell, + f"{col_span} is greater than col count: {col_count}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + col_span = 1 + + tokens = ["AXTable: Table iface spans for", cell, + f"are rowspan: {row_span}, colspan: {col_span}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + AXTable.PHYSICAL_SPANS_FROM_TABLE[hash(cell)] = row_span, col_span + return row_span, col_span + + @staticmethod + def _get_cell_spans_from_table_cell(cell: Atspi.Accessible) -> tuple[int, int]: + """Returns the row and column spans of cell via the table cell interface.""" + + if hash(cell) in AXTable.PHYSICAL_SPANS_FROM_CELL: + return AXTable.PHYSICAL_SPANS_FROM_CELL.get(hash(cell), (-1, -1)) + + if not AXObject.supports_table_cell(cell): + return -1, -1 + + try: + # TODO - JD: We get the spans individually due to + # https://bugzilla.mozilla.org/show_bug.cgi?id=1862437 + row_span = Atspi.TableCell.get_row_span(cell) + col_span = Atspi.TableCell.get_column_span(cell) + except GLib.GError as error: + msg = f"AXTable: Exception in _get_cell_spans_from_table_cell: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return -1, -1 + + tokens = ["AXTable: TableCell iface spans for", cell, + f"are rowspan: {row_span}, colspan: {col_span}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + AXTable.PHYSICAL_SPANS_FROM_CELL[hash(cell)] = row_span, col_span + return row_span, col_span + + @staticmethod + def _get_column_headers_from_table( + table: Atspi.Accessible, column: int + ) -> list[Atspi.Accessible]: + """Returns the column headers of the indexed column via the table interface.""" + + if not AXObject.supports_table(table): + return [] + + if column < 0: + return [] + + try: + header = Atspi.Table.get_column_header(table, column) + except GLib.GError as error: + msg = f"AXTable: Exception in _get_column_headers_from_table: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return [] + + tokens = [f"AXTable: Table iface header for column {column} of", table, "is", header] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + if header is not None: + return [header] + + return [] + + @staticmethod + def _get_column_headers_from_table_cell(cell: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns the column headers for cell via the table cell interface.""" + + if not AXObject.supports_table_cell(cell): + return [] + + try: + headers = Atspi.TableCell.get_column_header_cells(cell) + except GLib.GError as error: + msg = f"AXTable: Exception in _get_column_headers_from_table_cell: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return [] + + if headers is None: + tokens = ["AXTable: get_column_header_cells failed for", cell] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return [] + + tokens = ["AXTable: TableCell iface column headers for cell are:", headers] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return headers + + @staticmethod + def _get_row_headers_from_table(table: Atspi.Accessible, row: int) -> list[Atspi.Accessible]: + """Returns the row headers of the indexed row via the table interface.""" + + if not AXObject.supports_table(table): + return [] + + if row < 0: + return [] + + try: + header = Atspi.Table.get_row_header(table, row) + except GLib.GError as error: + msg = f"AXTable: Exception in _get_row_headers_from_table: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return [] + + tokens = [f"AXTable: Table iface header for row {row} of", table, "is", header] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + if header is not None: + return [header] + + return [] + + @staticmethod + def _get_row_headers_from_table_cell(cell: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns the row headers for cell via the table cell interface.""" + + if not AXObject.supports_table_cell(cell): + return [] + + try: + headers = Atspi.TableCell.get_row_header_cells(cell) + except GLib.GError as error: + msg = f"AXTable: Exception in _get_row_headers_from_table_cell: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return [] + + if headers is None: + tokens = ["AXTable: get_row_header_cells failed for", cell] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return [] + + tokens = ["AXTable: TableCell iface row headers for cell are:", headers] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return headers + + @staticmethod + def get_new_row_headers( + cell: Atspi.Accessible, + old_cell: Atspi.Accessible | None + ) -> list[Atspi.Accessible]: + """Returns row headers of cell that are not also headers of old_cell. """ + + if old_cell and not AXUtilitiesRole.is_table_cell_or_header(old_cell): + old_cell = AXObject.find_ancestor(old_cell, AXUtilitiesRole.is_table_cell_or_header) + + headers = AXTable.get_row_headers(cell) + if old_cell is None: + return headers + + old_headers = AXTable.get_row_headers(old_cell) + return list(set(headers).difference(set(old_headers))) + + @staticmethod + def get_new_column_headers( + cell: Atspi.Accessible, + old_cell: Atspi.Accessible | None + ) -> list[Atspi.Accessible]: + """Returns column headers of cell that are not also headers of old_cell. """ + + if old_cell and not AXUtilitiesRole.is_table_cell_or_header(old_cell): + old_cell = AXObject.find_ancestor(old_cell, AXUtilitiesRole.is_table_cell_or_header) + + headers = AXTable.get_column_headers(cell) + if old_cell is None: + return headers + + old_headers = AXTable.get_column_headers(old_cell) + return list(set(headers).difference(set(old_headers))) + + @staticmethod + def get_row_headers(cell: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns the row headers for cell, doing extra work to ensure we have them all.""" + + if not AXUtilitiesRole.is_table_cell(cell): + return [] + + dynamic_header = AXTable.get_dynamic_row_header(cell) + if dynamic_header is not None: + return [dynamic_header] + + # Firefox has the following implementation: + # 1. Only gives us the innermost/closest header for a cell + # 2. Supports returning the header of a header + # Chromium has the following implementation: + # 1. Gives us all the headers for a cell + # 2. Does NOT support returning the header of a header + # The Firefox implementation means we can get all the headers with some work. + # The Chromium implementation means less work, but makes it hard to present + # the changed outer header when navigating among nested row/column headers. + # TODO - JD: Figure out what the rest do, and then try to get the implementations + # aligned. + + result = AXTable.ROW_HEADERS_FOR_CELL.get(hash(cell)) + if result is not None: + return result + + result = AXTable._get_row_headers(cell) + # There either are no headers, or we got all of them. + if len(result) != 1: + AXTable.ROW_HEADERS_FOR_CELL[hash(cell)] = result + return result + + others = AXTable._get_row_headers(result[0]) + while len(others) == 1 and others[0] not in result: + result.insert(0, others[0]) + others = AXTable._get_row_headers(result[0]) + + AXTable.ROW_HEADERS_FOR_CELL[hash(cell)] = result + return result + + @staticmethod + def _get_row_headers(cell: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns the row headers for cell.""" + + if AXObject.supports_table_cell(cell): + return AXTable._get_row_headers_from_table_cell(cell) + + row, column = AXTable._get_cell_coordinates_from_table(cell) + if row < 0 or column < 0: + return [] + + table = AXTable.get_table(cell) + if table is None: + return [] + + headers = [] + rowspan = AXTable._get_cell_spans_from_table(cell)[0] + for index in range(row, row + rowspan): + headers.extend(AXTable._get_row_headers_from_table(table, index)) + + return headers + + @staticmethod + def has_row_headers(table: Atspi.Accessible, stop_after: int = 10) -> bool: + """Returns True if table has any headers for rows 0-stop_after.""" + + if not AXObject.supports_table(table): + return False + + stop_after = min(stop_after + 1, AXTable.get_row_count(table)) + for i in range(stop_after): + if AXTable._get_row_headers_from_table(table, i): + return True + + return False + + @staticmethod + def get_column_headers(cell: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns the column headers for cell, doing extra work to ensure we have them all.""" + + if not AXUtilitiesRole.is_table_cell(cell): + return [] + + dynamic_header = AXTable.get_dynamic_column_header(cell) + if dynamic_header is not None: + return [dynamic_header] + + # Firefox has the following implementation: + # 1. Only gives us the innermost/closest header for a cell + # 2. Supports returning the header of a header + # Chromium has the following implementation: + # 1. Gives us all the headers for a cell + # 2. Does NOT support returning the header of a header + # The Firefox implementation means we can get all the headers with some work. + # The Chromium implementation means less work, but makes it hard to present + # the changed outer header when navigating among nested row/column headers. + # TODO - JD: Figure out what the rest do, and then try to get the implementations + # aligned. + + result = AXTable.COLUMN_HEADERS_FOR_CELL.get(hash(cell)) + if result is not None: + return result + + result = AXTable._get_column_headers(cell) + # There either are no headers, or we got all of them. + if len(result) != 1: + AXTable.COLUMN_HEADERS_FOR_CELL[hash(cell)] = result + return result + + others = AXTable._get_column_headers(result[0]) + while len(others) == 1 and others[0] not in result: + result.insert(0, others[0]) + others = AXTable._get_column_headers(result[0]) + + AXTable.COLUMN_HEADERS_FOR_CELL[hash(cell)] = result + return result + + @staticmethod + def _get_column_headers(cell: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns the column headers for cell.""" + + if AXObject.supports_table_cell(cell): + return AXTable._get_column_headers_from_table_cell(cell) + + row, column = AXTable._get_cell_coordinates_from_table(cell) + if row < 0 or column < 0: + return [] + + table = AXTable.get_table(cell) + if table is None: + return [] + + headers = [] + colspan = AXTable._get_cell_spans_from_table(cell)[1] + for index in range(column, column + colspan): + headers.extend(AXTable._get_column_headers_from_table(table, index)) + + return headers + + @staticmethod + def has_column_headers(table: Atspi.Accessible, stop_after: int = 10) -> bool: + """Returns True if table has any headers for columns 0-stop_after.""" + + if not AXObject.supports_table(table): + return False + + stop_after = min(stop_after + 1, AXTable.get_column_count(table)) + for i in range(stop_after): + if AXTable._get_column_headers_from_table(table, i): + return True + + return False + + @staticmethod + def get_cell_coordinates( + cell: Atspi.Accessible, + prefer_attribute: bool = True, + find_cell: bool = False + ) -> tuple[int, int]: + """Returns the 0-based row and column indices.""" + + if not AXUtilitiesRole.is_table_cell_or_header(cell) and find_cell: + cell = AXObject.find_ancestor(cell, AXUtilitiesRole.is_table_cell_or_header) + + if not AXUtilitiesRole.is_table_cell_or_header(cell): + return -1, -1 + + if AXObject.supports_table_cell(cell): + row, col = AXTable._get_cell_coordinates_from_table_cell(cell) + else: + row, col = AXTable._get_cell_coordinates_from_table(cell) + + if not prefer_attribute: + return row, col + + row_index, col_index = AXTable._get_cell_coordinates_from_attribute(cell) + if row_index is not None: + row = int(row_index) - 1 + if col_index is not None: + col = int(col_index) - 1 + + return row, col + + @staticmethod + def _get_cell_coordinates_from_table(cell: Atspi.Accessible) -> tuple[int, int]: + """Returns the row and column indices of cell via the table interface.""" + + if hash(cell) in AXTable.PHYSICAL_COORDINATES_FROM_TABLE: + return AXTable.PHYSICAL_COORDINATES_FROM_TABLE.get(hash(cell), (-1, -1)) + + index = AXTable._get_cell_index(cell) + if index < 0: + return -1, -1 + + table = AXTable.get_table(cell) + if table is None: + tokens = ["AXTable: Couldn't find table-implementing ancestor for", cell] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return -1, -1 + + try: + row = Atspi.Table.get_row_at_index(table, index) + column = Atspi.Table.get_column_at_index(table, index) + except GLib.GError as error: + msg = f"AXTable: Exception in _get_cell_coordinates_from_table: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return -1, -1 + + tokens = ["AXTable: Table iface coords for", cell, f"are row: {row}, col: {column}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + AXTable.PHYSICAL_COORDINATES_FROM_TABLE[hash(cell)] = row, column + return row, column + + @staticmethod + def _get_cell_coordinates_from_table_cell(cell: Atspi.Accessible) -> tuple[int, int]: + """Returns the row and column indices of cell via the table cell interface.""" + + if hash(cell) in AXTable.PHYSICAL_COORDINATES_FROM_CELL: + return AXTable.PHYSICAL_COORDINATES_FROM_CELL.get(hash(cell), (-1, -1)) + + if not AXObject.supports_table_cell(cell): + return -1, -1 + + try: + success, row, column = Atspi.TableCell.get_position(cell) + except GLib.GError as error: + msg = f"AXTable: Exception in _get_cell_coordinates_from_table_cell: {error}" + + debug.print_message(debug.LEVEL_INFO, msg, True) + return -1, -1 + + if not success: + return -1, -1 + + tokens = ["AXTable: TableCell iface coords for", cell, f"are row: {row}, col: {column}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + AXTable.PHYSICAL_COORDINATES_FROM_CELL[hash(cell)] = row, column + return row, column + + @staticmethod + def _get_cell_coordinates_from_attribute( + cell: Atspi.Accessible + ) -> tuple[str | None, str | None]: + """Returns the 1-based indices for cell exposed via object attribute, or None, None.""" + + if cell is None: + return None, None + + if hash(cell) in AXTable.PRESENTABLE_COORDINATES: + return AXTable.PRESENTABLE_COORDINATES.get(hash(cell), (None, None)) + + attrs = AXObject.get_attributes_dict(cell) + row_index = attrs.get("rowindex") + col_index = attrs.get("colindex") + + tokens = ["AXTable: Row and col index attributes for", cell, ":", row_index, ",", col_index] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + AXTable.PRESENTABLE_COORDINATES[hash(cell)] = row_index, col_index + if row_index is not None and col_index is not None: + return row_index, col_index + + row = AXObject.find_ancestor(cell, AXUtilitiesRole.is_table_row) + if row is None: + return row_index, col_index + + attrs = AXObject.get_attributes_dict(row) + row_index = attrs.get("rowindex", row_index) + col_index = attrs.get("colindex", col_index) + + tokens = ["AXTable: Updated attributes based on", row, ":", row_index, col_index] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + AXTable.PRESENTABLE_COORDINATES[hash(cell)] = row_index, col_index + return row_index, col_index + + @staticmethod + def get_presentable_sort_order_from_header( + obj: Atspi.Accessible, + include_name: bool = False + ) -> str: + """Returns the end-user-consumable row/column sort order from its header.""" + + if not AXUtilitiesRole.is_table_header(obj): + return "" + + sort_order = AXObject.get_attribute(obj, "sort", False) + if not sort_order or sort_order == "none": + return "" + + if sort_order == "ascending": + result = object_properties.SORT_ORDER_ASCENDING + elif sort_order == "descending": + result = object_properties.SORT_ORDER_DESCENDING + else: + result = object_properties.SORT_ORDER_OTHER + + if include_name: + name = AXObject.get_name(obj) + if name: + result = f"{name}. {result}" + + return result + + @staticmethod + def get_table(obj: Atspi.Accessible) -> Atspi.Accessible | None: + """Returns obj if it is a table, otherwise returns the ancestor table of obj.""" + + if obj is None: + return None + + if AXObject.supports_table_cell(obj): + try: + table = Atspi.TableCell.get_table(obj) + except GLib.GError as error: + msg = f"AXTable: Exception in get_table: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + else: + if AXObject.supports_table(table): + return table + + def is_table(x): + if AXUtilitiesRole.is_table(x) \ + or AXUtilitiesRole.is_tree_table(x) or AXUtilitiesRole.is_tree(x): + return AXObject.supports_table(x) + return False + + if is_table(obj): + return obj + + return AXObject.find_ancestor(obj, is_table) + + @staticmethod + def get_table_description_for_presentation(table: Atspi.Accessible) -> str: + """Returns an end-user-consumable string which describes the table.""" + + if not AXObject.supports_table(table): + return "" + + result = messages.table_size(AXTable.get_row_count(table), AXTable.get_column_count(table)) + if AXTable.is_non_uniform_table(table): + result = f"{messages.TABLE_NON_UNIFORM} {result}" + return result + + @staticmethod + def get_first_cell(table: Atspi.Accessible) -> Atspi.Accessible | None: + """Returns the first cell in table.""" + + row, col = 0, 0 + return AXTable.get_cell_at(table, row, col) + + @staticmethod + def get_last_cell(table: Atspi.Accessible) -> Atspi.Accessible | None: + """Returns the last cell in table.""" + + row, col = AXTable.get_row_count(table) - 1, AXTable.get_column_count(table) - 1 + return AXTable.get_cell_at(table, row, col) + + @staticmethod + def get_cell_above(cell: Atspi.Accessible) -> Atspi.Accessible | None: + """Returns the cell above cell in table.""" + + row, col = AXTable.get_cell_coordinates(cell, prefer_attribute=False) + row -= 1 + return AXTable.get_cell_at(AXTable.get_table(cell), row, col) + + @staticmethod + def get_cell_below(cell: Atspi.Accessible) -> Atspi.Accessible | None: + """Returns the cell below cell in table.""" + + row, col = AXTable.get_cell_coordinates(cell, prefer_attribute=False) + row += AXTable.get_cell_spans(cell, prefer_attribute=False)[0] + return AXTable.get_cell_at(AXTable.get_table(cell), row, col) + + @staticmethod + def get_cell_on_left(cell: Atspi.Accessible) -> Atspi.Accessible | None: + """Returns the cell to the left.""" + + row, col = AXTable.get_cell_coordinates(cell, prefer_attribute=False) + col -= 1 + return AXTable.get_cell_at(AXTable.get_table(cell), row, col) + + @staticmethod + def get_cell_on_right(cell: Atspi.Accessible) -> Atspi.Accessible | None: + """Returns the cell to the right.""" + + row, col = AXTable.get_cell_coordinates(cell, prefer_attribute=False) + col += AXTable.get_cell_spans(cell, prefer_attribute=False)[1] + return AXTable.get_cell_at(AXTable.get_table(cell), row, col) + + @staticmethod + def get_start_of_row(cell: Atspi.Accessible) -> Atspi.Accessible | None: + """Returns the cell at the start of cell's row.""" + + row = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[0] + return AXTable.get_cell_at(AXTable.get_table(cell), row, 0) + + @staticmethod + def get_end_of_row(cell: Atspi.Accessible) -> Atspi.Accessible | None: + """Returns the cell at the end of cell's row.""" + + row = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[0] + table = AXTable.get_table(cell) + col = AXTable.get_column_count(table) - 1 + return AXTable.get_cell_at(AXTable.get_table(cell), row, col) + + @staticmethod + def get_top_of_column(cell: Atspi.Accessible) -> Atspi.Accessible | None: + """Returns the cell at the top of cell's column.""" + + col = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[1] + return AXTable.get_cell_at(AXTable.get_table(cell), 0, col) + + @staticmethod + def get_bottom_of_column(cell: Atspi.Accessible) -> Atspi.Accessible | None: + """Returns the cell at the bottom of cell's column.""" + + col = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[1] + table = AXTable.get_table(cell) + row = AXTable.get_row_count(table) - 1 + return AXTable.get_cell_at(AXTable.get_table(cell), row, col) + + @staticmethod + def get_cell_formula(cell: Atspi.Accessible) -> str | None: + """Returns the formula associated with this cell.""" + + attrs = AXObject.get_attributes_dict(cell, use_cache=False) + return attrs.get("formula", attrs.get("Formula")) + + @staticmethod + def is_first_cell(cell: Atspi.Accessible) -> bool: + """Returns True if this is the first cell in its table.""" + + row, col = AXTable.get_cell_coordinates(cell, prefer_attribute=False) + return row == 0 and col == 0 + + @staticmethod + def is_last_cell(cell: Atspi.Accessible) -> bool: + """Returns True if this is the last cell in its table.""" + + row, col = AXTable.get_cell_coordinates(cell, prefer_attribute=False) + if row < 0 or col < 0: + return False + + table = AXTable.get_table(cell) + if table is None: + return False + + return row + 1 == AXTable.get_row_count(table, prefer_attribute=False) \ + and col + 1 == AXTable.get_column_count(table, prefer_attribute=False) + + @staticmethod + def is_start_of_row(cell: Atspi.Accessible) -> bool: + """Returns True if this is the first cell in its row.""" + + col = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[1] + return col == 0 + + @staticmethod + def is_end_of_row(cell: Atspi.Accessible) -> bool: + """Returns True if this is the last cell in its row.""" + + col = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[1] + if col < 0: + return False + + table = AXTable.get_table(cell) + if table is None: + return False + + return col + 1 == AXTable.get_column_count(table, prefer_attribute=False) + + @staticmethod + def is_top_of_column(cell: Atspi.Accessible) -> bool: + """Returns True if this is the first cell in its column.""" + + row = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[0] + return row == 0 + + @staticmethod + def is_bottom_of_column(cell: Atspi.Accessible) -> bool: + """Returns True if this is the last cell in its column.""" + + row = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[0] + if row < 0: + return False + + table = AXTable.get_table(cell) + if table is None: + return False + + return row + 1 == AXTable.get_row_count(table, prefer_attribute=False) + + @staticmethod + def is_layout_table(table: Atspi.Accessible) -> bool: + """Returns True if this table should be treated as layout only.""" + + result, reason = False, "Not enough information" + attrs = AXObject.get_attributes_dict(table) + if AXUtilitiesRole.is_table(table): + if attrs.get("layout-guess") == "true": + result, reason = True, "The layout-guess attribute is true." + elif not AXObject.supports_table(table): + result, reason = True, "Doesn't support table interface." + elif attrs.get("xml-roles") == "table" or attrs.get("tag") == "table": + result, reason = False, "Is a web table without layout-guess set to true." + elif AXTable.has_column_headers(table) or AXTable.has_row_headers(table): + result, reason = False, "Has headers" + elif AXObject.get_name(table) or AXObject.get_description(table): + result, reason = False, "Has name or description" + elif AXTable.get_caption(table): + result, reason = False, "Has caption" + + tokens = ["AXTable:", table, f"is layout only: {result} ({reason})"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def get_label_for_cell_coordinates(cell: Atspi.Accessible) -> str: + """Returns the text that should be used instead of the numeric indices.""" + + if hash(cell) in AXTable.PRESENTABLE_COORDINATES_LABELS: + return AXTable.PRESENTABLE_COORDINATES_LABELS.get(hash(cell), "") + + attrs = AXObject.get_attributes_dict(cell) + result = "" + + # The attribute officially has the word "index" in it for clarity. + # TODO - JD: Google Sheets needs to start using the correct attribute name. + col_label = attrs.get("colindextext", attrs.get("coltext")) + row_label = attrs.get("rowindextext", attrs.get("rowtext")) + if col_label is not None and row_label is not None: + result = f"{col_label}{row_label}" + + tokens = ["AXTable: Coordinates label for", cell, f": {result}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + AXTable.PRESENTABLE_COORDINATES_LABELS[hash(cell)] = result + if result: + return result + + row = AXObject.find_ancestor(cell, AXUtilitiesRole.is_table_row) + if row is None: + return result + + attrs = AXObject.get_attributes_dict(row) + col_label = attrs.get("colindextext", attrs.get("coltext", col_label)) + row_label = attrs.get("rowindextext", attrs.get("rowtext", row_label)) + if col_label is not None and row_label is not None: + result = f"{col_label}{row_label}" + + tokens = ["AXTable: Updated coordinates label based on", row, f": {result}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + AXTable.PRESENTABLE_COORDINATES_LABELS[hash(cell)] = result + return result + + @staticmethod + def get_dynamic_row_header(cell: Atspi.Accessible) -> Atspi.Accessible | None: + """Returns the user-set row header for cell.""" + + table = AXTable.get_table(cell) + headers_column = AXTable.DYNAMIC_ROW_HEADERS_COLUMN.get(hash(table)) + if headers_column is None: + return None + + cell_row, cell_column = AXTable.get_cell_coordinates(cell) + if cell_column == headers_column: + return None + + return AXTable.get_cell_at(table, cell_row, headers_column) + + @staticmethod + def get_dynamic_column_header(cell: Atspi.Accessible) -> Atspi.Accessible | None: + """Returns the user-set column header for cell.""" + + table = AXTable.get_table(cell) + headers_row = AXTable.DYNAMIC_COLUMN_HEADERS_ROW.get(hash(table)) + if headers_row is None: + return None + + cell_row, cell_column = AXTable.get_cell_coordinates(cell) + if cell_row == headers_row: + return None + + return AXTable.get_cell_at(table, headers_row, cell_column) + + @staticmethod + def set_dynamic_row_headers_column(table: Atspi.Accessible, column: int) -> None: + """Sets the dynamic row headers column of table to column.""" + + AXTable.DYNAMIC_ROW_HEADERS_COLUMN[hash(table)] = column + + @staticmethod + def set_dynamic_column_headers_row(table: Atspi.Accessible, row: int) -> None: + """Sets the dynamic column headers row of table to row.""" + + AXTable.DYNAMIC_COLUMN_HEADERS_ROW[hash(table)] = row + + @staticmethod + def clear_dynamic_row_headers_column(table: Atspi.Accessible) -> None: + """Clears the dynamic row headers column of table.""" + + if hash(table) not in AXTable.DYNAMIC_ROW_HEADERS_COLUMN: + return + + AXTable.DYNAMIC_ROW_HEADERS_COLUMN.pop(hash(table)) + + @staticmethod + def clear_dynamic_column_headers_row(table: Atspi.Accessible) -> None: + """Clears the dynamic column headers row of table.""" + + if hash(table) not in AXTable.DYNAMIC_COLUMN_HEADERS_ROW: + return + + AXTable.DYNAMIC_COLUMN_HEADERS_ROW.pop(hash(table)) + + @staticmethod + def _get_visible_cell_range(table: Atspi.Accessible) -> tuple[tuple[int, int], tuple[int, int]]: + """Returns the (row, col) of the first and last visible cells in table.""" + + if not AXObject.supports_table(table): + return (-1, -1), (-1, -1) + + rect = AXComponent.get_rect(table) + tokens = ["AXTable: Rect for", table, "is", rect] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + first_cell = AXComponent.get_descendant_at_point(table, rect.x + 1, rect.y + 1) + tokens = ["AXTable: First visible cell for", table, "is", first_cell] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + start = AXTable.get_cell_coordinates(first_cell, prefer_attribute=False) + tokens = ["AXTable: First visible cell is at row", start[0], "column", start[1]] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + last_cell = AXComponent.get_descendant_at_point( + table, rect.x + rect.width - 1, rect.y + rect.height - 1) + tokens = ["AXTable: Last visible cell for", table, "is", last_cell] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + end = AXTable.get_cell_coordinates(last_cell, prefer_attribute=False) + tokens = ["AXTable: Last visible cell is at row", end[0], "column", end[1]] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + if end == (-1, -1): + last_cell = AXTable.get_last_cell(table) + tokens = ["AXTable: Adjusted lasat visible cell for", table, "is", last_cell] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + end = AXTable.get_cell_coordinates(last_cell, prefer_attribute=False) + tokens = ["AXTable: Adjusted last cell is at row", end[0], "column", end[1]] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + if AXUtilitiesRole.is_table_cell(last_cell) \ + and not AXUtilitiesRole.is_table_cell_or_header(first_cell): + candidate = AXTable.get_cell_above(last_cell) + while candidate and AXComponent.object_intersects_rect(candidate, rect): + first_cell = candidate + candidate = AXTable.get_cell_above(first_cell) + + candidate = AXTable.get_cell_on_left(first_cell) + while candidate and AXComponent.object_intersects_rect(candidate, rect): + first_cell = candidate + candidate = AXTable.get_cell_on_left(first_cell) + + tokens = ["AXTable: Adjusted first visible cell for", table, "is", first_cell] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + start = AXTable.get_cell_coordinates(first_cell, prefer_attribute=False) + tokens = ["AXTable: Adjusted first cell is at row", start[0], "column", start[1]] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + return start, end + + @staticmethod + def iter_visible_cells(table: Atspi.Accessible) -> Generator[Atspi.Accessible, None, None]: + """Yields the visible cells in table.""" + + start, end = AXTable._get_visible_cell_range(table) + if start[0] < 0 or start[1] < 0 or end[0] < 0 or end[1] < 0: + return + + for row in range(start[0], end[0] + 1): + for col in range(start[1], end[1] + 1): + cell = AXTable.get_cell_at(table, row, col) + if cell is None: + continue + for child in AXObject.iter_children(cell, AXUtilitiesRole.is_table_cell): + if AXObject.get_name(child): + cell = child + break + yield cell + + @staticmethod + def get_showing_cells_in_same_row( + cell: Atspi.Accessible, + clip_to_window: bool = False + ) -> list[Atspi.Accessible]: + """Returns a list of all the cells in the same row as obj that are showing.""" + + row = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[0] + if row == -1: + return [] + + table = AXTable.get_table(cell) + start_index, end_index = 0, AXTable.get_column_count(table, False) + if clip_to_window: + rect = AXComponent.get_rect(table) + if (cell := AXComponent.get_descendant_at_point(table, rect.x + 1, rect.y)): + start_index = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[1] + if (cell := AXComponent.get_descendant_at_point( + table, rect.x + rect.width - 1, rect.y)): + end_index = AXTable.get_cell_coordinates(cell, prefer_attribute=False)[1] + 1 + + if start_index == end_index: + return [] + + cells = [] + for i in range(start_index, end_index): + cell = AXTable.get_cell_at(table, row, i) + if AXUtilitiesState.is_showing(cell): + cells.append(cell) + + if not cells: + tokens = ["AXTable: No visible cells found in row with", cell] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return [] + + tokens = ["AXTable: First visible cell in row with", cell, "is", cells[0]] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + tokens = ["AXTable: Last visible cell in row with", cell, "is", cells[-1]] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return cells + + +AXTable.start_cache_clearing_thread() diff --git a/src/cthulhu/ax_text.py b/src/cthulhu/ax_text.py new file mode 100644 index 0000000..610e39b --- /dev/null +++ b/src/cthulhu/ax_text.py @@ -0,0 +1,1610 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 Stormux +# Copyright 2024 Igalia, S.L. +# Copyright 2024 GNOME Foundation Inc. +# Author: Joanmarie Diggs +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., Franklin Street, Fifth Floor, +# Boston MA 02110-1301 USA. +# +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu + +# pylint: disable=wrong-import-position +# pylint: disable=too-many-locals +# pylint: disable=too-many-public-methods +# pylint: disable=too-many-branches +# pylint: disable=too-many-lines + +"""Utilities for obtaining information about accessible text.""" + +# This has to be the first non-docstring line in the module to make linters happy. +from __future__ import annotations + +__id__ = "$Id$" +__version__ = "$Revision$" +__date__ = "$Date$" +__copyright__ = "Copyright (c) 2024 Igalia, S.L." \ + "Copyright (c) 2024 GNOME Foundation Inc." +__license__ = "LGPL" + +import enum +import locale +import re +from typing import Generator + +import gi +gi.require_version("Atspi", "2.0") +from gi.repository import Atspi +from gi.repository import GLib + +from . import colornames +from . import debug +from . import messages +from . import text_attribute_names +from .ax_object import AXObject +from .ax_utilities_role import AXUtilitiesRole +from .ax_utilities_state import AXUtilitiesState + +class AXTextAttribute(enum.Enum): + """Enum representing an accessible text attribute.""" + + # Note: Anything added here should also have an entry in text_attribute_names.py. + # The tuple is (non-localized name, enable by default). + BG_COLOR = ("bg-color", True) + BG_FULL_HEIGHT = ("bg-full-height", False) + BG_STIPPLE = ("bg-stipple", False) + DIRECTION = ("direction", False) + EDITABLE = ("editable", False) + FAMILY_NAME = ("family-name", True) + FG_COLOR = ("fg-color", True) + FG_STIPPLE = ("fg-stipple", False) + FONT_EFFECT = ("font-effect", True) + INDENT = ("indent", True) + INVALID = ("invalid", True) + INVISIBLE = ("invisible", False) + JUSTIFICATION = ("justification", True) + LANGUAGE = ("language", False) + LEFT_MARGIN = ("left-margin", False) + LINE_HEIGHT = ("line-height", False) + MARK = ("mark", True) + PARAGRAPH_STYLE = ("paragraph-style", True) + PIXELS_ABOVE_LINES = ("pixels-above-lines", False) + PIXELS_BELOW_LINES = ("pixels-below-lines", False) + PIXELS_INSIDE_WRAP = ("pixels-inside-wrap", False) + RIGHT_MARGIN = ("right-margin", False) + RISE = ("rise", False) + SCALE = ("scale", False) + SIZE = ("size", True) + STRETCH = ("stretch", False) + STRIKETHROUGH = ("strikethrough", True) + STYLE = ("style", True) + TEXT_DECORATION = ("text-decoration", True) + TEXT_POSITION = ("text-position", False) + TEXT_ROTATION = ("text-rotation", True) + TEXT_SHADOW = ("text-shadow", False) + UNDERLINE = ("underline", True) + VARIANT = ("variant", False) + VERTICAL_ALIGN = ("vertical-align", False) + WEIGHT = ("weight", True) + WRAP_MODE = ("wrap-mode", False) + WRITING_MODE = ("writing-mode", False) + + @classmethod + def from_string(cls, string: str) -> "AXTextAttribute" | None: + """Returns the AXTextAttribute for the specified string.""" + + for attribute in cls: + if attribute.get_attribute_name() == string: + return attribute + + return None + + @classmethod + def from_localized_string(cls, string: str) -> "AXTextAttribute" | None: + """Returns the AXTextAttribute for the specified localized string.""" + + for attribute in cls: + if attribute.get_localized_name() == string: + return attribute + + return None + + def get_attribute_name(self) -> str: + """Returns the non-localized name of the attribute.""" + + return self.value[0] + + def get_localized_name(self) -> str: + """Returns the localized name of the attribute.""" + + name = self.value[0] + return text_attribute_names.attribute_names.get(name, name) + + def get_localized_value(self, value) -> str: + """Returns the localized value of the attribute.""" + + if value is None: + return "" + + if value.endswith("px"): + value = value.split("px")[0] + if locale.localeconv()["decimal_point"] in value: + return messages.pixel_count(float(value)) + return messages.pixel_count(int(value)) + + if self in [AXTextAttribute.BG_COLOR, AXTextAttribute.FG_COLOR]: + return colornames.get_presentable_color_name(value) + + # TODO - JD: Is this still needed? + value = value.replace("-moz", "") + + # TODO - JD: Are these still needed? + if self == AXTextAttribute.JUSTIFICATION: + value = value.replace("justify", "fill") + elif self == AXTextAttribute.FAMILY_NAME: + value = value.split(",")[0].strip().strip('"') + + return text_attribute_names.attribute_values.get(value, value) + + def should_present_by_default(self) -> bool: + """Returns True if the attribute should be presented by default.""" + + return self.value[1] + + def value_is_default(self, value) -> bool: + """Returns True if value should be treated as the default value for this attribute.""" + + null_values = ["0", "0mm", "0px", "none", "false", "normal", "", None] + if value in null_values: + return True + + if self == AXTextAttribute.SCALE: + return float(value) == 1.0 + if self == AXTextAttribute.TEXT_POSITION: + return value == "baseline" + if self == AXTextAttribute.WEIGHT: + return value == "400" + if self == AXTextAttribute.LANGUAGE: + loc = locale.getlocale()[0] or "" + return value == loc[:2] + + return False + +class AXText: + """Utilities for obtaining information about accessible text.""" + + CACHED_TEXT_SELECTION: dict[int, tuple[str, int, int]] = {} + + @staticmethod + def get_character_at_offset( + obj: Atspi.Accessible, + offset: int | None = None + ) -> tuple[str, int, int]: + """Returns the (character, start, end) for the current or specified offset.""" + + length = AXText.get_character_count(obj) + if not length: + return "", 0, 0 + + if offset is None: + offset = AXText.get_caret_offset(obj) + + if not 0 <= offset <= length: + msg = f"WARNING: Offset {offset} is not valid. No character can be provided." + debug.print_message(debug.LEVEL_INFO, msg, True) + return "", 0, 0 + + try: + result = Atspi.Text.get_string_at_offset(obj, offset, Atspi.TextGranularity.CHAR) + except GLib.GError as error: + msg = f"AXText: Exception in get_character_at_offset: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return "", 0, 0 + + if result is None: + tokens = ["AXText: get_string_at_offset (char) failed for", obj, f"at {offset}."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return "", 0, 0 + + debug_string = result.content.replace("\n", "\\n") + tokens = [f"AXText: Character at offset {offset} in", obj, + f"'{debug_string}' ({result.start_offset}-{result.end_offset})"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result.content, result.start_offset, result.end_offset + + @staticmethod + def get_character_at_point(obj: Atspi.Accessible, x: int, y: int) -> tuple[str, int, int]: + """Returns the (character, start, end) at the specified point.""" + + offset = AXText.get_offset_at_point(obj, x, y) + if not 0 <= offset < AXText.get_character_count(obj): + return "", 0, 0 + + return AXText.get_character_at_offset(obj, offset) + + @staticmethod + def get_next_character( + obj: Atspi.Accessible, + offset: int | None = None + ) -> tuple[str, int, int]: + """Returns the next (character, start, end) for the current or specified offset.""" + + if offset is None: + offset = AXText.get_caret_offset(obj) + + current_character, start, end = AXText.get_character_at_offset(obj, offset) + if not current_character: + return "", 0, 0 + + length = AXText.get_character_count(obj) + next_offset = max(end, offset + 1) + + while next_offset < length: + next_character, next_start, next_end = AXText.get_character_at_offset(obj, next_offset) + if (next_character, next_start, next_end) != (current_character, start, end): + return next_character, next_start, next_end + next_offset += 1 + + return "", 0, 0 + + @staticmethod + def get_previous_character( + obj: Atspi.Accessible, + offset: int | None = None + ) -> tuple[str, int, int]: + """Returns the previous (character, start, end) for the current or specified offset.""" + + if offset is None: + offset = AXText.get_caret_offset(obj) + + current_character, start, end = AXText.get_character_at_offset(obj, offset) + if not current_character: + return "", 0, 0 + + if start <= 0: + return "", 0, 0 + + prev_offset = start - 1 + + while prev_offset >= 0: + prev_character, prev_start, prev_end = AXText.get_character_at_offset(obj, prev_offset) + if (prev_character, prev_start, prev_end) != (current_character, start, end): + return prev_character, prev_start, prev_end + prev_offset -= 1 + + return "", 0, 0 + + @staticmethod + def iter_character( + obj: Atspi.Accessible, + offset: int | None = None + ) -> Generator[tuple[str, int, int], None, None]: + """Generator to iterate by character in obj starting with the character at offset.""" + + if offset is None: + offset = AXText.get_caret_offset(obj) + + last_result = None + length = AXText.get_character_count(obj) + while offset < length: + character, start, end = AXText.get_character_at_offset(obj, offset) + if last_result is None and not character: + return + if character and (character, start, end) != last_result: + yield character, start, end + offset = max(end, offset + 1) + last_result = character, start, end + + @staticmethod + def get_word_at_offset( + obj: Atspi.Accessible, + offset: int | None = None + ) -> tuple[str, int, int]: + """Returns the (word, start, end) for the current or specified offset.""" + + length = AXText.get_character_count(obj) + if not length: + return "", 0, 0 + + if offset is None: + offset = AXText.get_caret_offset(obj) + + offset = min(max(0, offset), length - 1) + try: + result = Atspi.Text.get_string_at_offset(obj, offset, Atspi.TextGranularity.WORD) + except GLib.GError as error: + msg = f"AXText: Exception in get_word_at_offset: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return "", 0, 0 + + if result is None: + tokens = ["AXText: get_string_at_offset (word) failed for", obj, f"at {offset}."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return "", 0, 0 + + tokens = [f"AXText: Word at offset {offset} in", obj, + f"'{result.content}' ({result.start_offset}-{result.end_offset})"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result.content, result.start_offset, result.end_offset + + @staticmethod + def get_word_at_point(obj: Atspi.Accessible, x: int, y: int) -> tuple[str, int, int]: + """Returns the (word, start, end) at the specified point.""" + + offset = AXText.get_offset_at_point(obj, x, y) + if not 0 <= offset < AXText.get_character_count(obj): + return "", 0, 0 + + return AXText.get_word_at_offset(obj, offset) + + @staticmethod + def get_next_word( + obj: Atspi.Accessible, + offset: int | None = None + ) -> tuple[str, int, int]: + """Returns the next (word, start, end) for the current or specified offset.""" + + if offset is None: + offset = AXText.get_caret_offset(obj) + + current_word, start, end = AXText.get_word_at_offset(obj, offset) + if not current_word: + return "", 0, 0 + + length = AXText.get_character_count(obj) + next_offset = max(end, offset + 1) + + while next_offset < length: + next_word, next_start, next_end = AXText.get_word_at_offset(obj, next_offset) + if (next_word, next_start, next_end) != (current_word, start, end): + return next_word, next_start, next_end + next_offset += 1 + + return "", 0, 0 + + @staticmethod + def get_previous_word( + obj: Atspi.Accessible, + offset: int | None = None + ) -> tuple[str, int, int]: + """Returns the previous (word, start, end) for the current or specified offset.""" + + if offset is None: + offset = AXText.get_caret_offset(obj) + + current_word, start, end = AXText.get_word_at_offset(obj, offset) + if not current_word: + return "", 0, 0 + + if start <= 0: + return "", 0, 0 + + prev_offset = start - 1 + + while prev_offset >= 0: + prev_word, prev_start, prev_end = AXText.get_word_at_offset(obj, prev_offset) + if (prev_word, prev_start, prev_end) != (current_word, start, end): + return prev_word, prev_start, prev_end + prev_offset -= 1 + + return "", 0, 0 + + @staticmethod + def iter_word( + obj: Atspi.Accessible, + offset: int | None = None + ) -> Generator[tuple[str, int, int], None, None]: + """Generator to iterate by word in obj starting with the word at offset.""" + + if offset is None: + offset = AXText.get_caret_offset(obj) + + last_result = None + length = AXText.get_character_count(obj) + while offset < length: + word, start, end = AXText.get_word_at_offset(obj, offset) + if last_result is None and not word: + return + if word and (word, start, end) != last_result: + yield word, start, end + offset = max(end, offset + 1) + last_result = word, start, end + + @staticmethod + def get_line_at_offset( + obj: Atspi.Accessible, + offset: int | None = None + ) -> tuple[str, int, int]: + """Returns the (line, start, end) for the current or specified offset.""" + + length = AXText.get_character_count(obj) + if not length: + return "", 0, 0 + + if offset is None: + offset = AXText.get_caret_offset(obj) + + # Don't adjust the length in multiline text because we want to say "blank" at the end. + # This may or may not be sufficient. GTK3 seems to give us the correct, empty line. But + # (at least) Chromium does not. See comment below. + if not AXUtilitiesState.is_multi_line(obj) \ + and not AXUtilitiesRole.is_paragraph(obj) and not AXUtilitiesRole.is_section(obj): + offset = min(max(0, offset), length - 1) + else: + offset = max(0, offset) + + try: + result = Atspi.Text.get_string_at_offset(obj, offset, Atspi.TextGranularity.LINE) + except GLib.GError as error: + msg = f"AXText: Exception in get_line_at_offset: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return "", 0, 0 + + if result is None: + tokens = ["AXText: get_string_at_offset (line) failed for", obj, f"at {offset}."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return "", 0, 0 + + # Try again, e.g. Chromium returns "", -1, -1. + if result.start_offset == result.end_offset == -1 and offset == length: + offset -= 1 + result = Atspi.Text.get_string_at_offset(obj, offset, Atspi.TextGranularity.LINE) + + debug_string = result.content.replace("\n", "\\n") + tokens = [f"AXText: Line at offset {offset} in", obj, + f"'{debug_string}' ({result.start_offset}-{result.end_offset})"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + if 0 <= offset < result.start_offset: + offset -= 1 + msg = f"ERROR: Start offset is greater than offset. Trying with offset {offset}" + debug.print_message(debug.LEVEL_INFO, msg, True) + result = Atspi.Text.get_string_at_offset(obj, offset, Atspi.TextGranularity.LINE) + + debug_string = result.content.replace("\n", "\\n") + tokens = [f"AXText: Line at offset {offset} in", obj, + f"'{debug_string}' ({result.start_offset}-{result.end_offset})"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + return result.content, result.start_offset, result.end_offset + + @staticmethod + def get_line_at_point(obj: Atspi.Accessible, x: int, y: int) -> tuple[str, int, int]: + """Returns the (line, start, end) at the specified point.""" + + offset = AXText.get_offset_at_point(obj, x, y) + if not 0 <= offset < AXText.get_character_count(obj): + return "", 0, 0 + + return AXText.get_line_at_offset(obj, offset) + + @staticmethod + def get_next_line( + obj: Atspi.Accessible, + offset: int | None = None + ) -> tuple[str, int, int]: + """Returns the next (line, start, end) for the current or specified offset.""" + + if offset is None: + offset = AXText.get_caret_offset(obj) + + current_line, start, end = AXText.get_line_at_offset(obj, offset) + if not current_line: + return "", 0, 0 + + length = AXText.get_character_count(obj) + next_offset = max(end, offset + 1) + + while next_offset < length: + next_line, next_start, next_end = AXText.get_line_at_offset(obj, next_offset) + if (next_line, next_start, next_end) != (current_line, start, end): + return next_line, next_start, next_end + next_offset += 1 + + return "", 0, 0 + + @staticmethod + def get_previous_line( + obj: Atspi.Accessible, + offset: int | None = None + ) -> tuple[str, int, int]: + """Returns the previous (line, start, end) for the current or specified offset.""" + + if offset is None: + offset = AXText.get_caret_offset(obj) + + current_line, start, end = AXText.get_line_at_offset(obj, offset) + if not current_line and offset == AXText.get_character_count(obj): + current_line, start, end = AXText.get_line_at_offset(obj, offset - 1) + if current_line.endswith("\n"): + start = offset - 1 + + if not current_line or start <= 0: + return "", 0, 0 + + prev_offset = start - 1 + + while prev_offset >= 0: + prev_line, prev_start, prev_end = AXText.get_line_at_offset(obj, prev_offset) + if (prev_line, prev_start, prev_end) != (current_line, start, end): + return prev_line, prev_start, prev_end + prev_offset -= 1 + + return "", 0, 0 + + @staticmethod + def iter_line( + obj: Atspi.Accessible, + offset: int | None = None + ) -> Generator[tuple[str, int, int], None, None]: + """Generator to iterate by line in obj starting with the line at offset.""" + + line, start, end = AXText.get_line_at_offset(obj, offset) + if not line: + return + + # If the caller provides an offset positioned at the end boundary of the + # current line (e.g. start iteration from the previous line's end), some + # implementations of Atspi return the same line again for that offset. + # To avoid yielding duplicates (e.g. in get_visible_lines()), only yield + # the current line when the offset points inside it; otherwise start with + # the next distinct line. + if offset is None or offset < end: + yield line, start, end + current_start = start + + while True: + next_line, next_start, next_end = AXText.get_next_line(obj, current_start) + if not next_line or next_start <= current_start: + break + yield next_line, next_start, next_end + current_start = next_start + + @staticmethod + def _find_sentence_boundaries(text: str) -> list[int]: + """Returns the offsets in text that should be treated as sentence beginnings.""" + + if not text: + return [] + + boundaries = [0] + pattern = r"[.!?]+(?=\s|\ufffc|$)" + for match in re.finditer(pattern, text): + end_pos = match.end() + # Skip whitespace and embedded objects to find start of next sentence. + while end_pos < len(text) and (text[end_pos].isspace() or text[end_pos] == "\ufffc"): + end_pos += 1 + # Only add boundary if we haven't reached the end and it's not a duplicate. + if end_pos < len(text) and end_pos not in boundaries: + boundaries.append(end_pos) + + if boundaries[-1] != len(text): + boundaries.append(len(text)) + + return boundaries + + @staticmethod + def has_sentence_ending(text: str) -> bool: + """Check if text contains a sentence ending.""" + + return bool(text and re.search(r"\S[.!?]+(\s|\ufffc|$)", text)) + + @staticmethod + def _get_sentence_at_offset_fallback( + obj: Atspi.Accessible, + offset: int | None = None + ) -> tuple[str, int, int]: + """Fallback sentence detection for broken implementations.""" + + if offset is None: + offset = AXText.get_caret_offset(obj) + + text = AXText.get_all_text(obj) + if not text or offset < 0 or offset >= len(text): + return "", 0, 0 + + fallback_text, fallback_start, fallback_end = text, 0, len(text) + boundaries = AXText._find_sentence_boundaries(text) + for i in range(len(boundaries) - 1): + start, end = boundaries[i], boundaries[i + 1] + if start <= offset < end: + fallback_text, fallback_start, fallback_end = text[start:end], start, end + break + + tokens = ["AXText: Fallback sentence in", obj, + f" at offset {offset}: '{fallback_text}' ({fallback_start}-{fallback_end})"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return fallback_text, fallback_start, fallback_end + + @staticmethod + def get_sentence_at_offset( + obj: Atspi.Accessible, + offset: int | None = None + ) -> tuple[str, int, int]: + """Returns the (sentence, start, end) for the current or specified offset.""" + + length = AXText.get_character_count(obj) + if not length: + return "", 0, 0 + + if offset is None: + offset = AXText.get_caret_offset(obj) + + offset = min(max(0, offset), length - 1) + try: + result = Atspi.Text.get_string_at_offset(obj, offset, Atspi.TextGranularity.SENTENCE) + except GLib.GError as error: + msg = f"AXText: Exception in get_sentence_at_offset: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return AXText._get_sentence_at_offset_fallback(obj, offset) + + if result is None: + tokens = ["AXText: get_string_at_offset (sentence) failed for", obj, f"at {offset}."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return "", 0, 0 + + if result.start_offset == result.end_offset == -1 or not result.content: + return AXText._get_sentence_at_offset_fallback(obj, offset) + + if (result.start_offset == result.end_offset and + result.start_offset in [0, -1] and not result.content): + return AXText._get_sentence_at_offset_fallback(obj, offset) + + tokens = [f"AXText: Sentence at offset {offset} in", obj, + f"'{result.content}' ({result.start_offset}-{result.end_offset})"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result.content, result.start_offset, result.end_offset + + @staticmethod + def get_sentence_at_point(obj: Atspi.Accessible, x: int, y: int) -> tuple[str, int, int]: + """Returns the (sentence, start, end) at the specified point.""" + + offset = AXText.get_offset_at_point(obj, x, y) + if not 0 <= offset < AXText.get_character_count(obj): + return "", 0, 0 + + return AXText.get_sentence_at_offset(obj, offset) + + @staticmethod + def get_next_sentence( + obj: Atspi.Accessible, + offset: int | None = None + ) -> tuple[str, int, int]: + """Returns the next (sentence, start, end) for the current or specified offset.""" + + if offset is None: + offset = AXText.get_caret_offset(obj) + + current_sentence, start, end = AXText.get_sentence_at_offset(obj, offset) + if not current_sentence: + return "", 0, 0 + + length = AXText.get_character_count(obj) + next_offset = max(end, offset + 1) + + while next_offset < length: + next_sentence, next_start, next_end = AXText.get_sentence_at_offset(obj, next_offset) + if (next_sentence, next_start, next_end) != (current_sentence, start, end): + return next_sentence, next_start, next_end + next_offset += 1 + + return "", 0, 0 + + @staticmethod + def get_previous_sentence( + obj: Atspi.Accessible, + offset: int | None = None + ) -> tuple[str, int, int]: + """Returns the previous (sentence, start, end) for the current or specified offset.""" + + if offset is None: + offset = AXText.get_caret_offset(obj) + + current_sentence, start, end = AXText.get_sentence_at_offset(obj, offset) + if not current_sentence: + return "", 0, 0 + + if start <= 0: + return "", 0, 0 + + prev_offset = start - 1 + + while prev_offset >= 0: + prev_sentence, prev_start, prev_end = AXText.get_sentence_at_offset(obj, prev_offset) + if (prev_sentence, prev_start, prev_end) != (current_sentence, start, end): + return prev_sentence, prev_start, prev_end + prev_offset -= 1 + + return "", 0, 0 + + @staticmethod + def iter_sentence( + obj: Atspi.Accessible, + offset: int | None = None + ) -> Generator[tuple[str, int, int], None, None]: + """Generator to iterate by sentence in obj starting with the sentence at offset.""" + + sentence, start, end = AXText.get_sentence_at_offset(obj, offset) + if not sentence: + return + + # Avoid yielding a duplicate when the starting offset is exactly at the + # end boundary of the current sentence. Some implementations can return + # the same (sentence, start, end) again for that offset. + if offset is None or offset < end: + yield sentence, start, end + current_start = start + + while True: + next_sentence, next_start, next_end = AXText.get_next_sentence(obj, current_start) + if not next_sentence or next_start <= current_start: + break + yield next_sentence, next_start, next_end + current_start = next_start + + @staticmethod + def get_paragraph_at_offset( + obj: Atspi.Accessible, + offset: int | None = None + ) -> tuple[str, int, int]: + """Returns the (paragraph, start, end) for the current or specified offset.""" + + length = AXText.get_character_count(obj) + if not length: + return "", 0, 0 + + if offset is None: + offset = AXText.get_caret_offset(obj) + + offset = min(max(0, offset), length - 1) + try: + result = Atspi.Text.get_string_at_offset(obj, offset, Atspi.TextGranularity.PARAGRAPH) + except GLib.GError as error: + msg = f"AXText: Exception in get_paragraph_at_offset: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return "", 0, 0 + + if result is None: + tokens = ["AXText: get_string_at_offset (paragraph) failed for", obj, f"at {offset}."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return "", 0, 0 + + tokens = [f"AXText: Paragraph at offset {offset} in", obj, + f"'{result.content}' ({result.start_offset}-{result.end_offset})"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result.content, result.start_offset, result.end_offset + + @staticmethod + def get_paragraph_at_point(obj: Atspi.Accessible, x: int, y: int) -> tuple[str, int, int]: + """Returns the (paragraph, start, end) at the specified point.""" + + offset = AXText.get_offset_at_point(obj, x, y) + if not 0 <= offset < AXText.get_character_count(obj): + return "", 0, 0 + + return AXText.get_paragraph_at_offset(obj, offset) + + @staticmethod + def get_next_paragraph( + obj: Atspi.Accessible, + offset: int | None = None + ) -> tuple[str, int, int]: + """Returns the next (paragraph, start, end) for the current or specified offset.""" + + if offset is None: + offset = AXText.get_caret_offset(obj) + + current_paragraph, start, end = AXText.get_paragraph_at_offset(obj, offset) + if not current_paragraph: + return "", 0, 0 + + length = AXText.get_character_count(obj) + next_offset = max(end, offset + 1) + + while next_offset < length: + next_paragraph, next_start, next_end = AXText.get_paragraph_at_offset(obj, next_offset) + if (next_paragraph, next_start, next_end) != (current_paragraph, start, end): + return next_paragraph, next_start, next_end + next_offset += 1 + + return "", 0, 0 + + @staticmethod + def get_previous_paragraph( + obj: Atspi.Accessible, + offset: int | None = None + ) -> tuple[str, int, int]: + """Returns the previous (paragraph, start, end) for the current or specified offset.""" + + if offset is None: + offset = AXText.get_caret_offset(obj) + + current_paragraph, start, end = AXText.get_paragraph_at_offset(obj, offset) + if not current_paragraph: + return "", 0, 0 + + if start <= 0: + return "", 0, 0 + + prev_offset = start - 1 + + while prev_offset >= 0: + prev_paragraph, prev_start, prev_end = AXText.get_paragraph_at_offset(obj, prev_offset) + if (prev_paragraph, prev_start, prev_end) != (current_paragraph, start, end): + return prev_paragraph, prev_start, prev_end + prev_offset -= 1 + + return "", 0, 0 + + @staticmethod + def iter_paragraph( + obj: Atspi.Accessible, offset: int | None = None + ) -> Generator[tuple[str, int, int], None, None]: + """Generator to iterate by paragraph in obj starting with the paragraph at offset.""" + + if offset is None: + offset = AXText.get_caret_offset(obj) + + last_result = None + length = AXText.get_character_count(obj) + while offset < length: + paragraph, start, end = AXText.get_paragraph_at_offset(obj, offset) + if last_result is None and not paragraph: + return + if paragraph and (paragraph, start, end) != last_result: + yield paragraph, start, end + offset = max(end, offset + 1) + last_result = paragraph, start, end + + @staticmethod + def supports_paragraph_iteration(obj: Atspi.Accessible) -> bool: + """Returns True if paragraph iteration is supported on obj.""" + + if not AXObject.supports_text(obj): + return False + + string, start, end = AXText.get_paragraph_at_offset(obj, 0) + result = string and 0 <= start < end + tokens = ["AXText: Paragraph iteration supported on", obj, f": {result}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return bool(result) + + @staticmethod + def get_character_count(obj: Atspi.Accessible) -> int: + """Returns the character count of obj.""" + + if not AXObject.supports_text(obj): + return 0 + + try: + count = Atspi.Text.get_character_count(obj) + except GLib.GError as error: + msg = f"AXText: Exception in get_character_count: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return 0 + + tokens = ["AXText:", obj, f"reports {count} characters."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return count + + @staticmethod + def get_caret_offset(obj: Atspi.Accessible) -> int: + """Returns the caret offset of obj.""" + + if not AXObject.supports_text(obj): + return -1 + + try: + offset = Atspi.Text.get_caret_offset(obj) + except GLib.GError as error: + msg = f"AXText: Exception in get_caret_offset: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return -1 + + tokens = ["AXText:", obj, f"reports caret offset of {offset}."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return offset + + @staticmethod + def set_caret_offset(obj: Atspi.Accessible, offset: int) -> bool: + """Returns False if we definitely failed to set the offset. True cannot be trusted.""" + + if not AXObject.supports_text(obj): + return False + + try: + result = Atspi.Text.set_caret_offset(obj, offset) + except GLib.GError as error: + msg = f"AXText: Exception in set_caret_offset: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + tokens = [f"AXText: Reported result of setting offset to {offset} in", obj, f": {result}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def set_caret_offset_to_start(obj: Atspi.Accessible) -> bool: + """Returns False if we definitely failed to set the offset. True cannot be trusted.""" + + return AXText.set_caret_offset(obj, 0) + + @staticmethod + def set_caret_offset_to_end(obj: Atspi.Accessible) -> bool: + """Returns False if we definitely failed to set the offset. True cannot be trusted.""" + + return AXText.set_caret_offset(obj, AXText.get_character_count(obj)) + + @staticmethod + def get_substring(obj: Atspi.Accessible, start_offset: int, end_offset: int) -> str: + """Returns the text of obj within the specified offsets.""" + + if not AXObject.supports_text(obj): + return "" + + if end_offset == -1: + end_offset = AXText.get_character_count(obj) + + try: + result = Atspi.Text.get_text(obj, start_offset, end_offset) + except GLib.GError as error: + msg = f"AXText: Exception in get_substring: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return "" + + debug_string = result.replace("\n", "\\n") + tokens = ["AXText: Text of", obj, f"({start_offset}-{end_offset}): '{debug_string}'"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def get_all_text(obj: Atspi.Accessible) -> str: + """Returns the text content of obj.""" + + length = AXText.get_character_count(obj) + if not length: + return "" + + try: + result = Atspi.Text.get_text(obj, 0, length) + except GLib.GError as error: + msg = f"AXText: Exception in get_all_text: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return "" + + words = result.split() + if len(words) > 20: + debug_string = f"{' '.join(words[:5])} ... {' '.join(words[-5:])}" + else: + debug_string = result + + debug_string = debug_string.replace("\n", "\\n") + tokens = ["AXText: Text of", obj, f"'{debug_string}'"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def _get_n_selections(obj: Atspi.Accessible) -> int: + """Returns the number of reported selected substrings in obj.""" + + if not AXObject.supports_text(obj): + return 0 + + try: + result = Atspi.Text.get_n_selections(obj) + except GLib.GError as error: + msg = f"AXText: Exception in _get_n_selections: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return 0 + + tokens = ["AXText:", obj, f"reports {result} selection(s)."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def _remove_selection(obj: Atspi.Accessible, selection_number: int) -> None: + """Attempts to remove the specified selection.""" + + if not AXObject.supports_text(obj): + return + + try: + Atspi.Text.remove_selection(obj, selection_number) + except GLib.GError as error: + msg = f"AXText: Exception in _remove_selection: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return + + @staticmethod + def has_selected_text(obj: Atspi.Accessible) -> bool: + """Returns True if obj has selected text.""" + + return bool(AXText.get_selected_ranges(obj)) + + @staticmethod + def is_all_text_selected(obj: Atspi.Accessible) -> bool: + """Returns True of all the text in obj is selected.""" + + length = AXText.get_character_count(obj) + if not length: + return False + + ranges = AXText.get_selected_ranges(obj) + if not ranges: + return False + + return ranges[0][0] == 0 and ranges[-1][1] == length + + @staticmethod + def clear_all_selected_text(obj: Atspi.Accessible) -> None: + """Attempts to clear the selected text.""" + + for i in range(AXText._get_n_selections(obj)): + AXText._remove_selection(obj, i) + + @staticmethod + def get_selection_start_offset(obj: Atspi.Accessible) -> int: + """Returns the leftmost offset of the selected text.""" + + ranges = AXText.get_selected_ranges(obj) + if ranges: + return ranges[0][0] + + return -1 + + @staticmethod + def get_selection_end_offset(obj: Atspi.Accessible) -> int: + """Returns the rightmost offset of the selected text.""" + + ranges = AXText.get_selected_ranges(obj) + if ranges: + return ranges[-1][1] + + return -1 + + @staticmethod + def get_selected_ranges(obj: Atspi.Accessible) -> list[tuple[int, int]]: + """Returns a list of (start_offset, end_offset) tuples reflecting the selected text.""" + + count = AXText._get_n_selections(obj) + if not count: + return [] + + selections = [] + for i in range(count): + try: + result = Atspi.Text.get_selection(obj, i) + except GLib.GError as error: + msg = f"AXText: Exception in get_selected_ranges: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + break + if 0 <= result.start_offset < result.end_offset: + selections.append((result.start_offset, result.end_offset)) + + tokens = ["AXText:", obj, f"reports selected ranges: {selections}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return selections + + @staticmethod + def get_cached_selected_text(obj: Atspi.Accessible) -> tuple[str, int, int]: + """Returns the last known selected string, start, and end for obj.""" + + string, start, end = AXText.CACHED_TEXT_SELECTION.get(hash(obj), ("", 0, 0)) + debug_string = string.replace("\n", "\\n") + tokens = ["AXText: Cached selection for", obj, f"is '{debug_string}' ({start}, {end})"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return string, start, end + + @staticmethod + def update_cached_selected_text(obj: Atspi.Accessible) -> None: + """Updates the last known selected string, start, and end for obj.""" + + AXText.CACHED_TEXT_SELECTION[hash(obj)] = AXText.get_selected_text(obj) + + @staticmethod + def get_selected_text(obj: Atspi.Accessible) -> tuple[str, int, int]: + """Returns the selected string, start, and end for obj.""" + + selections = AXText.get_selected_ranges(obj) + if not selections: + return "", 0, 0 + + strings = [] + start_offset = -1 + end_offset = -1 + for selection in sorted(set(selections)): + strings.append(AXText.get_substring(obj, *selection)) + end_offset = selection[1] + if start_offset == -1: + start_offset = selection[0] + + text = " ".join(strings) + words = text.split() + if len(words) > 20: + debug_string = f"{' '.join(words[:5])} ... {' '.join(words[-5:])}" + else: + debug_string = text + + tokens = ["AXText: Selected text of", obj, + f"'{debug_string}' ({start_offset}-{end_offset})"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return text, start_offset, end_offset + + @staticmethod + def _add_new_selection(obj: Atspi.Accessible, start_offset: int, end_offset: int) -> bool: + """Creates a new selection for the specified range in obj.""" + + if not AXObject.supports_text(obj): + return False + + try: + result = Atspi.Text.add_selection(obj, start_offset, end_offset) + except GLib.GError as error: + msg = f"AXText: Exception in _add_selection: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + return result + + @staticmethod + def _update_existing_selection( + obj: Atspi.Accessible, + start_offset: int, + end_offset: int, + selection_number: int = 0 + ) -> bool: + """Modifies specified selection in obj to the specified range.""" + + if not AXObject.supports_text(obj): + return False + + try: + result = Atspi.Text.set_selection(obj, selection_number, start_offset, end_offset) + except GLib.GError as error: + msg = f"AXText: Exception in set_selected_text: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + return result + + @staticmethod + def set_selected_text(obj: Atspi.Accessible, start_offset: int, end_offset: int) -> bool: + """Returns False if we definitely failed to set the selection. True cannot be trusted.""" + + # TODO - JD: For now we always assume and operate on the first selection. + # This preserves the original functionality prior to the refactor. But whether + # that functionality is what it should be needs investigation. + if AXText._get_n_selections(obj) > 0: + result = AXText._update_existing_selection(obj, start_offset, end_offset) + else: + result = AXText._add_new_selection(obj, start_offset, end_offset) + + if result and debug.LEVEL_INFO >= debug.debugLevel: + substring = AXText.get_substring(obj, start_offset, end_offset) + selection = AXText.get_selected_text(obj)[0] + if substring != selection: + msg = "AXText: Substring and selected text do not match." + debug.print_message(debug.LEVEL_INFO, msg, True) + + return result + + # TODO - JD: This should be converted to return AXTextAttribute values. + @staticmethod + def get_text_attributes_at_offset( + obj: Atspi.Accessible, + offset: int | None = None + ) -> tuple[dict[str, str], int, int]: + """Returns a (dict, start, end) tuple for attributes at offset in obj.""" + + if not AXObject.supports_text(obj): + return {}, 0, 0 + + if offset is None: + offset = AXText.get_caret_offset(obj) + + try: + result = Atspi.Text.get_attribute_run(obj, offset, include_defaults=True) + except GLib.GError as error: + msg = f"AXText: Exception in get_text_attributes_at_offset: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return {}, 0, AXText.get_character_count(obj) + + if result is None: + tokens = ["AXText: get_attribute_run failed for", obj, f"at offset {offset}."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return {}, 0, AXText.get_character_count(obj) + + tokens = ["AXText: Attributes for", obj, f"at offset {offset} : {result}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + # Adjust for web browsers that report indentation and justification at object attributes + # rather than text attributes. + obj_attributes = AXObject.get_attributes_dict(obj, False) + if not result[0].get("justification"): + alternative = obj_attributes.get("text-align") + if alternative: + result[0]["justification"] = alternative + if not result[0].get("indent"): + alternative = obj_attributes.get("text-indent") + if alternative: + result[0]["indent"] = alternative + + return result[0] or {}, result[1] or 0, result[2] or AXText.get_character_count(obj) + + @staticmethod + def get_all_text_attributes( + obj: Atspi.Accessible, + start_offset: int = 0, + end_offset: int = -1 + ) -> list[tuple[int, int, dict[str, str]]]: + """Returns a list of (start, end, attrs dict) tuples for obj.""" + + if not AXObject.supports_text(obj): + return [] + + if end_offset == -1: + end_offset = AXText.get_character_count(obj) + + tokens = ["AXText: Getting attributes for", obj, f"chars: {start_offset}-{end_offset}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + rv = [] + offset = start_offset + while offset < end_offset: + attrs, start, end = AXText.get_text_attributes_at_offset(obj, offset) + if start <= end: + rv.append((max(start, offset), end, attrs)) + else: + # TODO - JD: We're sometimes seeing this from WebKit, e.g. in Evo gitlab messages. + msg = f"AXText: Start offset {start} > end offset {end}" + debug.print_message(debug.LEVEL_INFO, msg, True) + offset = max(end, offset + 1) + + msg = f"AXText: {len(rv)} attribute ranges found." + debug.print_message(debug.LEVEL_INFO, msg, True) + return rv + + @staticmethod + def get_all_supported_text_attributes() -> list[AXTextAttribute]: + """Returns a set of all supported text attribute names.""" + + return list(AXTextAttribute) + + @staticmethod + def get_offset_at_point(obj: Atspi.Accessible, x: int, y: int) -> int: + """Returns the character offset in obj at the specified point.""" + + if not AXObject.supports_text(obj): + return -1 + + try: + offset = Atspi.Text.get_offset_at_point(obj, x, y, Atspi.CoordType.WINDOW) + except GLib.GError as error: + msg = f"AXText: Exception in get_offset_at_point: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return -1 + + tokens = ["AXText: Offset in", obj, f"at {x}, {y} is {offset}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return offset + + @staticmethod + def get_character_rect(obj: Atspi.Accessible, offset: int | None = None) -> Atspi.Rect: + """Returns the Atspi rect of the character at the specified offset in obj.""" + + if not AXObject.supports_text(obj): + return Atspi.Rect() + + if offset is None: + offset = AXText.get_caret_offset(obj) + + try: + rect = Atspi.Text.get_character_extents(obj, offset, Atspi.CoordType.WINDOW) + except GLib.GError as error: + msg = f"AXText: Exception in get_character_rect: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return Atspi.Rect() + + tokens = [f"AXText: Offset {offset} in", obj, "has rect", rect] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return rect + + @staticmethod + def get_range_rect(obj: Atspi.Accessible, start: int, end: int) -> Atspi.Rect: + """Returns the Atspi rect of the string at the specified range in obj.""" + + if not AXObject.supports_text(obj): + return Atspi.Rect() + + if end <= 0: + end = AXText.get_character_count(obj) + + try: + rect = Atspi.Text.get_range_extents(obj, start, end, Atspi.CoordType.WINDOW) + except GLib.GError as error: + msg = f"AXText: Exception in get_range_rect: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return Atspi.Rect() + + tokens = [f"AXText: Range {start}-{end} in", obj, "has rect", rect] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return rect + + @staticmethod + def _rect_is_fully_contained_in(rect1: Atspi.Rect, rect2: Atspi.Rect) -> bool: + """Returns true if rect1 is fully contained in rect2""" + + return rect2.x <= rect1.x and rect2.y <= rect1.y \ + and rect2.x + rect2.width >= rect1.x + rect1.width \ + and rect2.y + rect2.height >= rect1.y + rect1.height + + @staticmethod + def _line_comparison(line_rect: Atspi.Rect, clip_rect: Atspi.Rect) -> int: + """Returns -1 (line above), 1 (line below), or 0 (line inside) clip_rect.""" + + # https://gitlab.gnome.org/GNOME/gtk/-/issues/6419 + clip_rect.y = max(0, clip_rect.y) + + if line_rect.y + line_rect.height / 2 < clip_rect.y: + return -1 + + if line_rect.y + line_rect.height / 2 > clip_rect.y + clip_rect.height: + return 1 + + return 0 + + @staticmethod + def get_visible_lines( + obj: Atspi.Accessible, + clip_rect: Atspi.Rect + ) -> list[tuple[str, int, int]]: + """Returns a list of (string, start, end) for lines of obj inside clip_rect.""" + + tokens = ["AXText: Getting visible lines for", obj, "inside", clip_rect] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + line, start, end = AXText.find_first_visible_line(obj, clip_rect) + debug_string = line.replace("\n", "\\n") + tokens = ["AXText: First visible line in", obj, f"is: '{debug_string}' ({start}-{end})"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + result = [(line, start, end)] + offset = end + for line, start, end in AXText.iter_line(obj, offset): + line_rect = AXText.get_range_rect(obj, start, end) + if AXText._line_comparison(line_rect, clip_rect) > 0: + break + result.append((line, start, end)) + + line, start, end = result[-1] + debug_string = line.replace("\n", "\\n") + tokens = ["AXText: Last visible line in", obj, f"is: '{debug_string}' ({start}-{end})"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def find_first_visible_line( + obj: Atspi.Accessible, + clip_rect: Atspi.Rect + ) -> tuple[str, int, int]: + """Returns the first (string, start, end) visible line of obj inside clip_rect.""" + + result = "", 0, 0 + length = AXText.get_character_count(obj) + low, high = 0, length + while low < high: + mid = (low + high) // 2 + line, start, end = AXText.get_line_at_offset(obj, mid) + if start == 0: + return line, start, end + + if start < 0: + tokens = ["AXText: Treating invalid offset as above", clip_rect] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + low = mid + 1 + continue + + result = line, start, end + previous_line, previous_start, previous_end = AXText.get_line_at_offset(obj, start - 1) + if previous_start <= 0 and previous_end <= 0: + return result + + text_rect = AXText.get_range_rect(obj, start, end) + if AXText._line_comparison(text_rect, clip_rect) < 0: + low = mid + 1 + continue + + if AXText._line_comparison(text_rect, clip_rect) > 0: + high = mid + continue + + previous_rect = AXText.get_range_rect(obj, previous_start, previous_end) + if AXText._line_comparison(previous_rect, clip_rect) != 0: + return result + + result = previous_line, previous_start, previous_end + high = mid + + return result + + @staticmethod + def find_last_visible_line( + obj: Atspi.Accessible, + clip_rect: Atspi.Rect + ) -> tuple[str, int, int]: + """Returns the last (string, start, end) visible line of obj inside clip_rect.""" + + result = "", 0, 0 + length = AXText.get_character_count(obj) + low, high = 0, length + while low < high: + mid = (low + high) // 2 + line, start, end = AXText.get_line_at_offset(obj, mid) + if end >= length: + return line, start, end + + if end <= 0: + tokens = ["AXText: Treating invalid offset as below", clip_rect] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + high = mid + continue + + result = line, start, end + next_line, next_start, next_end = AXText.get_line_at_offset(obj, end) + if next_start <= 0 and next_end <= 0: + return result + + text_rect = AXText.get_range_rect(obj, start, end) + if AXText._line_comparison(text_rect, clip_rect) < 0: + low = mid + 1 + continue + + if AXText._line_comparison(text_rect, clip_rect) > 0: + high = mid + continue + + next_rect = AXText.get_range_rect(obj, next_start, next_end) + if AXText._line_comparison(next_rect, clip_rect) != 0: + return result + + result = next_line, next_start, next_end + low = mid + 1 + + return result + + @staticmethod + def string_has_spelling_error(obj: Atspi.Accessible, offset: int | None = None) -> bool: + """Returns True if the text attributes indicate a spelling error.""" + + attributes = AXText.get_text_attributes_at_offset(obj, offset)[0] + if attributes.get("invalid") == "spelling": + return True + if attributes.get("invalid") == "grammar": + return False + if attributes.get("text-spelling") == "misspelled": + return True + if attributes.get("underline") in ["error", "spelling"]: + return True + return False + + @staticmethod + def string_has_grammar_error(obj: Atspi.Accessible, offset: int | None = None) -> bool: + """Returns True if the text attributes indicate a grammar error.""" + + attributes = AXText.get_text_attributes_at_offset(obj, offset)[0] + if attributes.get("invalid") == "grammar": + return True + if attributes.get("underline") == "grammar": + return True + return False + + @staticmethod + def is_eoc(character: str) -> bool: + """Returns True if character is an embedded object character (\ufffc).""" + + return character == "\ufffc" + + @staticmethod + def character_at_offset_is_eoc(obj: Atspi.Accessible, offset: int) -> bool: + """Returns True if character in obj is an embedded object character (\ufffc).""" + + character, _start, _end = AXText.get_character_at_offset(obj, offset) + return AXText.is_eoc(character) + + @staticmethod + def is_whitespace_or_empty(obj: Atspi.Accessible) -> bool: + """Returns True if obj lacks text, or contains only whitespace.""" + + if not AXObject.supports_text(obj): + return True + + return not AXText.get_all_text(obj).strip() + + @staticmethod + def has_presentable_text(obj: Atspi.Accessible) -> bool: + """Returns True if obj has presentable text.""" + + if not AXObject.supports_text(obj): + return False + + text = AXText.get_all_text(obj).strip() + if not text: + return AXUtilitiesRole.is_paragraph(obj) + + return bool(re.search(r"\w+", text)) + + @staticmethod + def scroll_substring_to_point( + obj: Atspi.Accessible, + x: int, + y: int, + start_offset: int | None = None, + end_offset: int | None = None + ) -> bool: + """Attempts to scroll obj to the specified point.""" + + length = AXText.get_character_count(obj) + if not length: + return False + + if start_offset is None: + start_offset = 0 + if end_offset is None: + end_offset = length - 1 + + try: + result = Atspi.Text.scroll_substring_to_point( + obj, start_offset, end_offset, Atspi.CoordType.WINDOW, x, y) + except GLib.GError as error: + msg = f"AXText: Exception in scroll_substring_to_point: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + tokens = ["AXText: Scrolled", obj, f"substring ({start_offset}-{end_offset}) to", + f"{x}, {y}: {result}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def scroll_substring_to_location( + obj: Atspi.Accessible, + location: Atspi.ScrollType, + start_offset: int | None = None, + end_offset: int | None = None + ) -> bool: + """Attempts to scroll the substring to the specified Atspi.ScrollType location.""" + + length = AXText.get_character_count(obj) + if not length: + return False + + if start_offset is None: + start_offset = 0 + if end_offset is None: + end_offset = length - 1 + + try: + result = Atspi.Text.scroll_substring_to(obj, start_offset, end_offset, location) + except GLib.GError as error: + msg = f"AXText: Exception in scroll_substring_to_location: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + tokens = ["AXText: Scrolled", obj, f"substring ({start_offset}-{end_offset}) to", + location, f": {result}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result diff --git a/src/cthulhu/ax_utilities.py b/src/cthulhu/ax_utilities.py index aebc4f4..5963e4e 100644 --- a/src/cthulhu/ax_utilities.py +++ b/src/cthulhu/ax_utilities.py @@ -1,9 +1,7 @@ -#!/usr/bin/env python3 +# Utilities for performing tasks related to accessibility inspection. # -# Copyright (c) 2024 Stormux -# Copyright (c) 2010-2012 The Orca Team -# Copyright (c) 2012 Igalia, S.L. -# Copyright (c) 2005-2010 Sun Microsystems Inc. +# Copyright 2023-2025 Igalia, S.L. +# Author: Joanmarie Diggs # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -19,35 +17,43 @@ # License along with this library; if not, write to the # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. -# -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca -""" -Utilities for performing tasks related to accessibility inspection. -These utilities are app-type- and toolkit-agnostic. Utilities that might have -different implementations or results depending on the type of app (e.g. terminal, -chat, web) or toolkit (e.g. Qt, Gtk) should be in script_utilities.py file(s). +# pylint: disable=too-many-branches +# pylint: disable=too-many-public-methods +# pylint: disable=too-many-return-statements +# pylint: disable=too-many-statements +# pylint: disable=wrong-import-position -N.B. There are currently utilities that should never have custom implementations -that live in script_utilities.py files. These will be moved over time. -""" +"""Utilities for performing tasks related to accessibility inspection.""" + +from __future__ import annotations __id__ = "$Id$" __version__ = "$Revision$" __date__ = "$Date$" -__copyright__ = "Copyright (c) 2023 Igalia, S.L." +__copyright__ = "Copyright (c) 2023-2025 Igalia, S.L." __license__ = "LGPL" +import functools import inspect +import queue +import threading +import time import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi from . import debug +from .ax_component import AXComponent from .ax_object import AXObject +from .ax_selection import AXSelection +from .ax_table import AXTable +from .ax_text import AXText +from .ax_utilities_application import AXUtilitiesApplication from .ax_utilities_collection import AXUtilitiesCollection +from .ax_utilities_event import AXUtilitiesEvent +from .ax_utilities_relation import AXUtilitiesRelation from .ax_utilities_role import AXUtilitiesRole from .ax_utilities_state import AXUtilitiesState @@ -57,91 +63,187 @@ class AXUtilities: COMPARE_COLLECTION_PERFORMANCE = False - @staticmethod - def get_desktop(): - """Returns the accessible desktop""" + # Things we cache. + SET_MEMBERS: dict[int, list[Atspi.Accessible]] = {} + IS_LAYOUT_ONLY: dict[int, tuple[bool, str]] = {} - try: - desktop = Atspi.get_desktop(0) - except Exception as error: - tokens = ["ERROR: Exception getting desktop from Atspi:", error] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return None - - return desktop + _lock = threading.Lock() @staticmethod - def get_all_applications(must_have_window=False): - """Returns a list of running applications known to Atspi, filtering out - those which have no child windows if must_have_window is True.""" + def start_cache_clearing_thread() -> None: + """Starts thread to periodically clear cached details.""" - desktop = AXUtilities.get_desktop() - if desktop is None: - return [] - - def pred(obj): - if must_have_window: - return AXObject.get_child_count(obj) > 0 - return True - - return list(AXObject.iter_children(desktop, pred)) + thread = threading.Thread(target=AXUtilities._clear_stored_data) + thread.daemon = True + thread.start() @staticmethod - def is_application_in_desktop(app): - """Returns true if app is known to Atspi""" + def _clear_stored_data() -> None: + """Clears any data we have cached for objects""" - desktop = AXUtilities.get_desktop() - if desktop is None: + while True: + time.sleep(60) + AXUtilities._clear_all_dictionaries() + + @staticmethod + def _clear_all_dictionaries(reason: str = "") -> None: + msg = "AXUtilities: Clearing cache." + if reason: + msg += f" Reason: {reason}" + debug.print_message(debug.LEVEL_INFO, msg, True) + + with AXUtilities._lock: + AXUtilities.SET_MEMBERS.clear() + AXUtilities.IS_LAYOUT_ONLY.clear() + + @staticmethod + def clear_all_cache_now(obj: Atspi.Accessible | None = None, reason: str = "") -> None: + """Clears all cached information immediately.""" + + AXUtilities._clear_all_dictionaries(reason) + AXObject.clear_cache_now(reason) + AXUtilitiesRelation.clear_cache_now(reason) + AXUtilitiesEvent.clear_cache_now(reason) + if AXUtilitiesRole.is_table_related(obj): + AXTable.clear_cache_now(reason) + + @staticmethod + def can_be_active_window(window: Atspi.Accessible) -> bool: + """Returns True if window can be the active window based on its state.""" + + if window is None: return False - for child in AXObject.iter_children(desktop): - if child == app: - return True + AXObject.clear_cache(window, False, "Checking if window can be the active window") + app = AXUtilitiesApplication.get_application(window) + tokens = ["AXUtilities:", window, "from", app] - tokens = ["WARNING:", app, "is not in", desktop] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return False + if not AXUtilitiesState.is_active(window): + tokens.append("lacks active state") + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return False + + if not AXUtilitiesState.is_showing(window): + tokens.append("lacks showing state") + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return False + + if AXUtilitiesState.is_iconified(window): + tokens.append("is iconified") + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return False + + if AXObject.get_name(app) == "mutter-x11-frames": + tokens.append("is from app that cannot have the real active window") + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return False + + if app and not AXUtilitiesApplication.is_application_in_desktop(app): + tokens.append("is from app unknown to AT-SPI2") + # Firefox alerts and dialogs suffer from this bug too, but if we ignore these windows + # we'll fail to fully present things like the file chooser dialog and the replace-file + # alert. https://bugzilla.mozilla.org/show_bug.cgi?id=1882794 + if not AXUtilitiesRole.is_dialog_or_alert(window): + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return False + + tokens.append("can be active window") + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return True @staticmethod - def get_application_with_pid(pid): - """Returns the accessible application with the specified pid""" + def find_active_window() -> Atspi.Accessible | None: + """Tries to locate the active window; may or may not succeed.""" - desktop = AXUtilities.get_desktop() - if desktop is None: + candidates = [] + apps = AXUtilitiesApplication.get_all_applications(must_have_window=True) + for app in apps: + candidates.extend(list(AXObject.iter_children(app, AXUtilities.can_be_active_window))) + + if not candidates: + tokens = ["AXUtilities: Unable to find active window from", apps] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return None - for app in AXObject.iter_children(desktop): - if AXObject.get_process_id(app) == pid: - return app + if len(candidates) == 1: + tokens = ["AXUtilities: Active window is", candidates[0]] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return candidates[0] - tokens = ["WARNING: app with pid", pid, "is not in", desktop] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return None + tokens = ["AXUtilities: These windows all claim to be active:", candidates] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + # Some electron apps running in the background claim to be active even when they + # are not. These are the ones we know about. We can add others as we go. + suspect_apps = ["slack", + "discord", + "outline-client", + "whatsapp-desktop-linux"] + filtered = [] + for frame in candidates: + if AXObject.get_name(AXUtilitiesApplication.get_application(frame)) in suspect_apps: + tokens = ["AXUtilities: Suspecting", frame, "is a non-active Electron app"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + else: + filtered.append(frame) + + if len(filtered) == 1: + tokens = ["AXUtilities: Active window is believed to be", filtered[0]] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return filtered[0] + + guess: Atspi.Accessible | None = None + if filtered: + tokens = ["AXUtilities: Still have multiple active windows:", filtered] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + guess = filtered[0] + + if guess is not None: + tokens = ["AXUtilities: Returning", guess, "as active window"] + else: + tokens = ["AXUtilities: No active window found"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return guess @staticmethod - def get_all_static_text_leaf_nodes(obj): - """Returns all the descendants of obj that are static text leaf nodes""" + def is_unfocused_alert_or_dialog(obj: Atspi.Accessible) -> bool: + """Returns True if obj is an unfocused alert or dialog with presentable items.""" - roles = [Atspi.Role.STATIC, Atspi.Role.TEXT] - def is_not_element(acc): - return AXObject.get_attribute(acc, "tag") in (None, "", "br") - - result = None - if AXObject.supports_collection(obj): - result = AXUtilitiesCollection.find_all_with_role(obj, roles, is_not_element) - if not AXUtilities.COMPARE_COLLECTION_PERFORMANCE: - return result - - def is_match(acc): - return AXObject.get_role(acc) in roles and is_not_element(acc) - - return AXObject.find_all_descendants(obj, is_match) + if not AXUtilitiesRole.is_dialog_or_alert(obj): + return False + if not AXObject.get_child_count(obj): + return False + if not AXUtilitiesState.is_showing(obj): + return False + return not AXUtilities.can_be_active_window(obj) @staticmethod - def get_all_widgets(obj, must_be_showing_and_visible=True): + def get_unfocused_alerts_and_dialogs(obj: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns a list of all the unfocused alerts and dialogs in the app and window of obj.""" + + app = AXUtilitiesApplication.get_application(obj) + result = list(AXObject.iter_children(app, AXUtilities.is_unfocused_alert_or_dialog)) + + frame = AXObject.find_ancestor( + obj, lambda x: AXUtilitiesRole.is_application(AXObject.get_parent(x))) + result.extend(list(AXObject.iter_children(frame, AXUtilities.is_unfocused_alert_or_dialog))) + + tokens = ["AXUtilities: Unfocused alerts and dialogs for", obj, ":", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def get_all_widgets( + obj: Atspi.Accessible, + must_be_showing_and_visible: bool = True, + exclude_push_button: bool = False + ) -> list[Atspi.Accessible]: """Returns all the descendants of obj with a widget role""" roles = AXUtilitiesRole.get_widget_roles() + if exclude_push_button and Atspi.Role.BUTTON in roles: + roles.remove(Atspi.Role.BUTTON) + result = None if AXObject.supports_collection(obj): if not must_be_showing_and_visible: @@ -164,7 +266,7 @@ class AXUtilities: return AXObject.find_all_descendants(obj, is_match) @staticmethod - def get_default_button(obj): + def get_default_button(obj: Atspi.Accessible) -> Atspi.Accessible | None: """Returns the default button descendant of obj""" result = None @@ -176,7 +278,7 @@ class AXUtilities: return AXObject.find_descendant(obj, AXUtilitiesRole.is_default_button) @staticmethod - def get_focused_object(obj): + def get_focused_object(obj: Atspi.Accessible) -> Atspi.Accessible | None: """Returns the focused descendant of obj""" result = None @@ -188,7 +290,19 @@ class AXUtilities: return AXObject.find_descendant(obj, AXUtilitiesState.is_focused) @staticmethod - def get_status_bar(obj): + def get_info_bar(obj: Atspi.Accessible) -> Atspi.Accessible | None: + """Returns the info bar descendant of obj""" + + result = None + if AXObject.supports_collection(obj): + result = AXUtilitiesCollection.find_info_bar(obj) + if not AXUtilities.COMPARE_COLLECTION_PERFORMANCE: + return result + + return AXObject.find_descendant(obj, AXUtilitiesRole.is_info_bar) + + @staticmethod + def get_status_bar(obj: Atspi.Accessible) -> Atspi.Accessible | None: """Returns the status bar descendant of obj""" result = None @@ -199,13 +313,684 @@ class AXUtilities: return AXObject.find_descendant(obj, AXUtilitiesRole.is_status_bar) + @staticmethod + def _is_layout_only(obj: Atspi.Accessible) -> tuple[bool, str]: + """Returns True and a string reason if obj is believed to serve only for layout.""" -for name, method in inspect.getmembers(AXUtilitiesRole, predicate=inspect.isfunction): - setattr(AXUtilities, name, method) + reason = "" + role = AXObject.get_role(obj) + if role in AXUtilitiesRole.get_layout_only_roles(): + return True, "has layout-only role" -for name, method in inspect.getmembers(AXUtilitiesState, predicate=inspect.isfunction): - setattr(AXUtilities, name, method) + if AXUtilitiesRole.is_layered_pane(obj, role): + result = AXObject.find_ancestor(obj, AXUtilitiesRole.is_desktop_frame) is not None + if result: + reason = "is inside desktop frame" + return result, reason -for name, method in inspect.getmembers(AXUtilitiesCollection, predicate=inspect.isfunction): - if name.startswith("find"): - setattr(AXUtilities, name, method) + if AXUtilitiesRole.is_menu(obj, role) or AXUtilitiesRole.is_list(obj, role): + result = AXObject.find_ancestor(obj, AXUtilitiesRole.is_combo_box) is not None + if result: + reason = "is inside combo box" + return result, reason + + if AXUtilitiesRole.is_group(obj, role): + result = not AXUtilities.has_explicit_name(obj) + if result: + reason = "lacks explicit name" + return result, reason + + if AXUtilitiesRole.is_panel(obj, role) or AXUtilitiesRole.is_grouping(obj, role): + name = AXObject.get_name(obj) + description = AXObject.get_description(obj) + labelled_by = AXUtilitiesRelation.get_is_labelled_by(obj) + described_by = AXUtilitiesRelation.get_is_described_by(obj) + if not (name or description or labelled_by or described_by): + return True, "lacks name, description, and relations" + if name == AXObject.get_name(AXUtilitiesApplication.get_application(obj)): + return True, "has same name as app" + if AXObject.get_child_count(obj) == 1: + child = AXObject.get_child(obj, 0) + if name == AXObject.get_name(child): + return True, "has same name as its only child" + if not AXUtilitiesRole.is_label(child) and child in labelled_by: + return True, "is labelled by non-label only child" + set_roles = AXUtilitiesRole.get_set_container_roles() + ancestor = AXObject.find_ancestor(obj, lambda x: AXObject.get_role(x) in set_roles) + if ancestor and AXObject.get_name(ancestor) == name: + return True, "is in set container with same name" + return False, reason + + if AXUtilitiesRole.is_section(obj, role) or AXUtilitiesRole.is_document(obj, role): + if AXUtilitiesState.is_focusable(obj): + return False, "is focusable" + if AXObject.has_action(obj, "click"): + return False, "has click action" + return True, "is not interactive" + + if AXUtilitiesRole.is_tool_bar(obj): + result = AXUtilitiesRole.is_page_tab_list(AXObject.get_child(obj, 0)) + if result: + reason = "is parent of page tab list" + return result, reason + + if AXUtilitiesRole.is_table(obj, role): + result = AXTable.is_layout_table(obj) + if result: + reason = "is layout table" + return result, reason + + if AXUtilitiesRole.is_table_row(obj): + if AXUtilitiesState.is_focusable(obj): + return False, "is focusable" + if AXUtilitiesState.is_selectable(obj): + return False, "is selectable" + if AXUtilitiesState.is_expandable(obj): + return False, "is expandable" + if AXUtilities.has_explicit_name(obj): + return False, "has explicit name" + return True, "is not focusable, selectable, or expandable and lacks explicit name" + + if AXUtilitiesRole.is_table_cell(obj, role): + if AXUtilitiesRole.is_table_cell(AXObject.get_child(obj, 0)): + return True, "child of this cell is table cell" + table = AXTable.get_table(obj) + if AXUtilitiesRole.is_table(table): + result = AXTable.is_layout_table(table) + if result: + reason = "is in layout table" + return result, reason + + return False, reason + + @staticmethod + def is_layout_only(obj: Atspi.Accessible) -> bool: + """Returns True if obj is believed to serve only for layout.""" + + if hash(obj) in AXUtilities.IS_LAYOUT_ONLY: + result, reason = AXUtilities.IS_LAYOUT_ONLY.get(hash(obj), (False, "")) + else: + result, reason = AXUtilities._is_layout_only(obj) + AXUtilities.IS_LAYOUT_ONLY[hash(obj)] = result, reason + + if reason: + tokens = ["AXUtilities:", obj, f"believed to be layout only: {result}, {reason}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + return result + + @staticmethod + def is_message_dialog(obj: Atspi.Accessible) -> bool: + """Returns True if obj is a dialog that should be treated as a message dialog""" + + if not AXUtilitiesRole.is_dialog_or_alert(obj): + return False + + if not AXObject.supports_collection(obj): + widgets = AXUtilities.get_all_widgets(obj, exclude_push_button=True) + return not widgets + + if AXUtilitiesCollection.has_scroll_pane(obj): + tokens = ["AXUtilities:", obj, "is not a message dialog: has scroll pane"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return False + + if AXUtilitiesCollection.has_split_pane(obj): + tokens = ["AXUtilities:", obj, "is not a message dialog: has split pane"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return False + + if AXUtilitiesCollection.has_tree_or_tree_table(obj): + tokens = ["AXUtilities:", obj, "is not a message dialog: has tree or tree table"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return False + + if AXUtilitiesCollection.has_combo_box_or_list_box(obj): + tokens = ["AXUtilities:", obj, "is not a message dialog: has combo box or list box"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return False + + if AXUtilitiesCollection.has_editable_object(obj): + tokens = ["AXUtilities:", obj, "is not a message dialog: has editable object"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return False + + tokens = ["AXUtilities:", obj, "is believed to be a message dialog"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return True + + @staticmethod + def is_redundant_object(obj1: Atspi.Accessible, obj2: Atspi.Accessible) -> bool: + """Returns True if obj2 is redundant to obj1.""" + + if obj1 == obj2: + return False + + if AXObject.get_name(obj1) != AXObject.get_name(obj2) \ + or AXObject.get_role(obj1) != AXObject.get_role(obj2): + return False + + tokens = ["AXUtilities:", obj2, "is redundant to", obj1] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return True + + @staticmethod + def _sort_by_child_index(object_list: list[Atspi.Accessible]) -> list[Atspi.Accessible]: + """Returns the list of objects sorted according to child index.""" + + def cmp(x, y): + return AXObject.get_index_in_parent(x) - AXObject.get_index_in_parent(y) + + if not object_list or len(object_list) == 1: + return object_list + + result = sorted(object_list, key=functools.cmp_to_key(cmp)) + + first, second = result[0:2] + if AXUtilitiesRole.is_radio_button(first) and AXObject.get_toolkit_name(first) == "gtk": + # Gtk radio buttons are often in reverse order, except for when they're not. + # See https://gitlab.gnome.org/GNOME/gtk/-/issues/7839. + sorted_first, _sorted_second = AXComponent.sort_objects_by_position([first, second]) + if sorted_first != first: + result.reverse() + + if object_list != result: + tokens = ["AXUtilities: Original list", object_list] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + tokens = ["AXUtilities: Sorted list", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + return result + + @staticmethod + def _get_set_members( + obj: Atspi.Accessible, container: Atspi.Accessible + ) -> list[Atspi.Accessible]: + """Returns the members of the container of obj""" + + if container is None: + tokens = ["AXUtilities: Members of", obj, "not obtainable: container is None"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return [] + + result = AXUtilitiesRelation.get_is_member_of(obj) + if result: + tokens = ["AXUtilities: Members of", obj, "in", container, "via member-of", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return AXUtilities._sort_by_child_index(result) + + result = AXUtilitiesRelation.get_is_node_parent_of(obj) + if result: + tokens = ["AXUtilities: Members of", obj, "in", container, "via node-parent-of", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return AXUtilities._sort_by_child_index(result) + + if AXUtilitiesRole.is_description_value(obj): + previous_sibling = AXObject.get_previous_sibling(obj) + while previous_sibling and AXUtilitiesRole.is_description_value(previous_sibling): + result.append(previous_sibling) + previous_sibling = AXObject.get_previous_sibling(previous_sibling) + result.append(obj) + next_sibling = AXObject.get_next_sibling(obj) + while next_sibling and AXUtilitiesRole.is_description_value(next_sibling): + result.append(next_sibling) + next_sibling = AXObject.get_next_sibling(next_sibling) + tokens = ["AXUtilities: Members of", obj, "in", container, "based on siblings", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + if AXUtilitiesRole.is_menu_related(obj): + result = list(AXObject.iter_children(container, AXUtilitiesRole.is_menu_related)) + tokens = ["AXUtilities: Members of", obj, "in", container, "based on menu role", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + role = AXObject.get_role(obj) + result = list(AXObject.iter_children(container, lambda x: AXObject.get_role(x) == role)) + tokens = ["AXUtilities: Members of", obj, "in", container, "based on role", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def get_set_members(obj: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns the members of the container of obj.""" + + result: list[Atspi.Accessible] = [] + container = AXObject.get_parent_checked(obj) + if hash(container) in AXUtilities.SET_MEMBERS: + result = AXUtilities.SET_MEMBERS.get(hash(container), []) + + if obj not in result: + if result: + tokens = ["AXUtilities:", obj, "not in cached members of", container, ":", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + result = AXUtilities._get_set_members(obj, container) + AXUtilities.SET_MEMBERS[hash(container)] = result + + # In a collapsed combobox, one can arrow to change the selection without showing the items. + must_be_showing = not AXObject.find_ancestor(obj, AXUtilitiesRole.is_combo_box) + if not must_be_showing: + return result + + filtered = list(filter(AXUtilitiesState.is_showing, result)) + if result != filtered: + tokens = ["AXUtilities: Filtered non-showing:", set(result).difference(set(filtered))] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + return filtered + + @staticmethod + def get_set_size(obj: Atspi.Accessible) -> int: + """Returns the total number of objects in this container.""" + + result = AXObject.get_attribute(obj, "setsize", False) + if isinstance(result, str) and result.isnumeric(): + return int(result) + + if AXUtilitiesRole.is_table_row(obj): + return AXTable.get_row_count(AXTable.get_table(obj)) + + if AXUtilitiesRole.is_table_cell_or_header(obj) \ + and not AXUtilitiesRole.is_table_row(AXObject.get_parent(obj)): + return AXTable.get_row_count(AXTable.get_table(obj)) + + if AXUtilitiesRole.is_combo_box(obj): + selected_children = AXSelection.get_selected_children(obj) + if not selected_children: + return -1 + if len(selected_children) == 1: + obj = selected_children[0] + + if AXUtilitiesRole.is_list(obj) or AXUtilitiesRole.is_list_box(obj): + obj = AXObject.find_descendant(obj, AXUtilitiesRole.is_list_item) + + child_count = AXObject.get_child_count(AXObject.get_parent(obj)) + if child_count > 500: + return child_count + + members = AXUtilities.get_set_members(obj) + return len(members) + + @staticmethod + def get_set_size_is_unknown(obj: Atspi.Accessible) -> bool: + """Returns True if the total number of objects in this container is unknown.""" + + if AXUtilitiesState.is_indeterminate(obj): + return True + + attrs = AXObject.get_attributes_dict(obj, False) + if attrs.get("setsize") == "-1": + return True + + if AXUtilitiesRole.is_table(obj): + return attrs.get("rowcount") == "-1" or attrs.get("colcount") == "-1" + + return False + + @staticmethod + def get_position_in_set(obj: Atspi.Accessible) -> int: + """Returns the position of obj with respect to the number of items in its container.""" + + result = AXObject.get_attribute(obj, "posinset", False) + if isinstance(result, str) and result.isnumeric(): + # ARIA posinset is 1-based. + return int(result) - 1 + + if AXUtilitiesRole.is_table_row(obj): + result = AXObject.get_attribute(obj, "rowindex", False) + if isinstance(result, str) and result.isnumeric(): + # ARIA posinset is 1-based. + return int(result) - 1 + + if AXObject.get_child_count(obj): + cell = AXObject.find_descendant(obj, AXUtilitiesRole.is_table_cell_or_header) + result = AXObject.get_attribute(cell, "rowindex", False) + + if isinstance(result, str) and result.isnumeric(): + # ARIA posinset is 1-based. + return int(result) - 1 + + if AXUtilitiesRole.is_table_cell_or_header(obj) \ + and not AXUtilitiesRole.is_table_row(AXObject.get_parent(obj)): + return AXTable.get_cell_coordinates(obj)[0] + + if AXUtilitiesRole.is_combo_box(obj): + selected_children = AXSelection.get_selected_children(obj) + if not selected_children: + return -1 + if len(selected_children) == 1: + obj = selected_children[0] + + child_count = AXObject.get_child_count(AXObject.get_parent(obj)) + if child_count > 500: + return AXObject.get_index_in_parent(obj) + + members = AXUtilities.get_set_members(obj) + if obj not in members: + return -1 + + return members.index(obj) + + @staticmethod + def has_explicit_name(obj: Atspi.Accessible) -> bool: + """Returns True if obj has an author/app-provided name as opposed to a calculated name.""" + + return AXObject.get_attribute(obj, "explicit-name") == "true" + + @staticmethod + def has_visible_caption(obj: Atspi.Accessible) -> bool: + """Returns True if obj has a visible caption.""" + + if not (AXUtilitiesRole.is_figure(obj) or AXObject.supports_table(obj)): + return False + + labels = AXUtilitiesRelation.get_is_labelled_by(obj) + for label in labels: + if AXUtilitiesRole.is_caption(label) \ + and AXUtilitiesState.is_showing(label) and AXUtilitiesState.is_visible(label): + return True + + return False + + @staticmethod + def get_displayed_label(obj: Atspi.Accessible) -> str: + """Returns the displayed label of obj.""" + + labels = AXUtilitiesRelation.get_is_labelled_by(obj) + strings = [AXObject.get_name(label) or AXText.get_all_text(label) for label in labels] + result = " ".join(strings) + return result + + @staticmethod + def get_displayed_description(obj: Atspi.Accessible) -> str: + """Returns the displayed description of obj.""" + + descriptions = AXUtilitiesRelation.get_is_described_by(obj) + strings = [AXObject.get_name(desc) or AXText.get_all_text(desc) for desc in descriptions] + result = " ".join(strings) + return result + + @staticmethod + def get_heading_level(obj: Atspi.Accessible) -> int: + """Returns the heading level of obj.""" + + if not AXUtilitiesRole.is_heading(obj): + return 0 + + use_cache = not AXUtilitiesState.is_editable(obj) + attrs = AXObject.get_attributes_dict(obj, use_cache) + + try: + value = int(attrs.get("level", "0")) + except ValueError: + tokens = ["AXUtilities: Exception getting value for", obj, "(", attrs, ")"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return 0 + + return value + + @staticmethod + def get_nesting_level(obj: Atspi.Accessible) -> int: + """Returns the nesting level of obj.""" + + def pred(x: Atspi.Accessible) -> bool: + if AXUtilitiesRole.is_list_item(obj): + return AXUtilitiesRole.is_list(AXObject.get_parent(x)) + return AXUtilitiesRole.have_same_role(obj, x) + + ancestors = [] + ancestor = AXObject.find_ancestor(obj, pred) + while ancestor: + ancestors.append(ancestor) + ancestor = AXObject.find_ancestor(ancestor, pred) + + return len(ancestors) + + @staticmethod + def get_next_object(obj: Atspi.Accessible) -> Atspi.Accessible | None: + """Returns the next object (depth first, unless there's a flows-to relation)""" + + if not AXObject.is_valid(obj): + return None + + targets = AXUtilitiesRelation.get_flows_to(obj) + for target in targets: + if not AXObject.is_dead(target): + return target + + index = AXObject.get_index_in_parent(obj) + 1 + parent = AXObject.get_parent(obj) + while parent and not 0 < index < AXObject.get_child_count(parent): + obj = parent + index = AXObject.get_index_in_parent(obj) + 1 + parent = AXObject.get_parent(obj) + + if parent is None: + return None + + next_object = AXObject.get_child(parent, index) + if next_object == obj: + tokens = ["AXUtilities:", obj, "claims to be its own next object"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return None + + return next_object + + @staticmethod + def get_previous_object(obj: Atspi.Accessible) -> Atspi.Accessible | None: + """Returns the previous object (depth first, unless there's a flows-from relation)""" + + if not AXObject.is_valid(obj): + return None + + targets = AXUtilitiesRelation.get_flows_from(obj) + for target in targets: + if not AXObject.is_dead(target): + return target + + index = AXObject.get_index_in_parent(obj) - 1 + parent = AXObject.get_parent(obj) + while parent and not 0 <= index < AXObject.get_child_count(parent) - 1: + obj = parent + index = AXObject.get_index_in_parent(obj) - 1 + parent = AXObject.get_parent(obj) + + if parent is None: + return None + + previous_object = AXObject.get_child(parent, index) + if previous_object == obj: + tokens = ["AXUtilities:", obj, "claims to be its own previous object"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return None + + return previous_object + + @staticmethod + def is_on_screen( + obj: Atspi.Accessible, + bounding_box: Atspi.Rect | None = None + ) -> bool: + """Returns true if obj should be treated as being on screen.""" + + AXObject.clear_cache(obj, False, "Updating to check if object is on screen.") + + tokens = ["AXUtilities: Checking if", obj, "is showing and visible...."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + if not (AXUtilitiesState.is_showing(obj) and AXUtilitiesState.is_visible(obj)): + tokens = ["AXUtilities:", obj, "is not showing and visible. Treating as off screen."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return False + + tokens = ["AXUtilities:", obj, "is showing and visible. Checking hidden..."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + if AXUtilitiesState.is_hidden(obj): + tokens = ["AXUtilities:", obj, "is reports being hidden. Treating as off screen."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return False + + tokens = ["AXUtilities:", obj, "is not hidden. Checking size and rect..."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + if AXComponent.has_no_size_or_invalid_rect(obj): + tokens = ["AXUtilities: Rect of", obj, "is unhelpful. Treating as on screen."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return True + + tokens = ["AXUtilities:", obj, "has size and a valid rect. Checking if off screen..."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + if AXComponent.object_is_off_screen(obj): + tokens = ["AXUtilities:", obj, "is believed to be off screen."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return False + + tokens = ["AXUtilities:", obj, "is not off screen. Checking", + bounding_box, "intersection..."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + if bounding_box is not None and not AXComponent.object_intersects_rect(obj, bounding_box): + tokens = ["AXUtilities", obj, "not in", bounding_box, ". Treating as off screen."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return False + + tokens = ["AXUtilities:", obj, "is believed to be on screen."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return True + + @staticmethod + def treat_as_leaf_node(obj: Atspi.Accessible) -> bool: + """Returns True if obj should be treated as a leaf node.""" + + if AXUtilitiesRole.children_are_presentational(obj): + # In GTK, the contents of the page tab descends from the page tab. + if AXUtilitiesRole.is_page_tab(obj): + return False + return True + + role = AXObject.get_role(obj) + if AXUtilitiesRole.is_combo_box(obj, role): + return not AXUtilitiesState.is_expanded(obj) + + if AXUtilitiesRole.is_menu(obj, role) and not AXUtilitiesRole.has_role_from_aria(obj): + return not AXUtilitiesState.is_expanded(obj) + + if AXObject.get_name(obj): + return AXUtilitiesRole.is_link(obj, role) or AXUtilitiesRole.is_label(obj, role) + + return False + + @staticmethod + def _get_on_screen_objects( + root: Atspi.Accessible, + cancellation_event: threading.Event, + bounding_box: Atspi.Rect | None = None + ) -> list: + + tokens = ["AXUtilities: Getting on-screen objects in", root, f"({hex(id(root))})"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + if cancellation_event.is_set(): + msg = "AXUtilities: Cancellation event set. Stopping search." + debug.print_message(debug.LEVEL_INFO, msg, True) + return [] + + if not AXUtilities.is_on_screen(root, bounding_box): + return [] + + if AXUtilities.treat_as_leaf_node(root): + return [root] + + if AXObject.supports_table(root) and AXObject.supports_selection(root): + return list(AXTable.iter_visible_cells(root)) + + objects = [] + root_name = AXObject.get_name(root) + if root_name or AXObject.get_description(root) or AXText.has_presentable_text(root): + objects.append(root) + + if bounding_box is None: + bounding_box = AXComponent.get_rect(root) + + for i, child in enumerate(AXObject.iter_children(root)): + tokens = [f"AXUtilities: Child {i} is", child, f"({hex(id(child))})"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + if cancellation_event.is_set(): + msg = "AXUtilities: Cancellation event set. Stopping search." + debug.print_message(debug.LEVEL_INFO, msg, True) + break + + children = AXUtilities._get_on_screen_objects(child, cancellation_event, bounding_box) + objects.extend(children) + if root_name and children and root in objects and root_name == AXObject.get_name(child): + objects.remove(root) + + if objects: + return objects + + if AXUtilitiesState.is_focusable(root) or AXObject.has_action(root, "click"): + return [root] + + return [] + + @staticmethod + def get_on_screen_objects( + root: Atspi.Accessible, + bounding_box: Atspi.Rect | None = None, + timeout: float = 5.0 + ) -> list: + """Returns a list of onscreen objects in the given root.""" + + result_queue: queue.Queue[list] = queue.Queue() + cancellation_event = threading.Event() + + def _worker(): + result = AXUtilities._get_on_screen_objects(root, cancellation_event, bounding_box) + if not cancellation_event.is_set(): + result_queue.put(result) + + worker_thread = threading.Thread(target=_worker) + worker_thread.start() + + try: + result = result_queue.get(timeout=timeout) + except queue.Empty: + tokens = ["AXUtilities: get_on_screen_objects timed out.", root] + debug.print_tokens(debug.LEVEL_WARNING, tokens, True) + cancellation_event.set() + result = [] + + msg = "AXUtilities: Checking AT-SPI responsiveness...." + debug.print_message(debug.LEVEL_INFO, msg, True) + desktop = AXUtilitiesApplication.get_desktop() + tokens = ["AXUtilities: Desktop is", desktop] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + worker_thread.join() + tokens = [f"AXUtilities: {len(result)} onscreen objects found in", root] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + +for method_name, method in inspect.getmembers(AXUtilitiesApplication, predicate=inspect.isfunction): + setattr(AXUtilities, method_name, method) + +for method_name, method in inspect.getmembers(AXUtilitiesEvent, predicate=inspect.isfunction): + setattr(AXUtilities, method_name, method) + +for method_name, method in inspect.getmembers(AXUtilitiesRelation, predicate=inspect.isfunction): + setattr(AXUtilities, method_name, method) + +for method_name, method in inspect.getmembers(AXUtilitiesRole, predicate=inspect.isfunction): + setattr(AXUtilities, method_name, method) + +for method_name, method in inspect.getmembers(AXUtilitiesState, predicate=inspect.isfunction): + setattr(AXUtilities, method_name, method) + +for method_name, method in inspect.getmembers(AXUtilitiesCollection, predicate=inspect.isfunction): + if method_name.startswith("find"): + setattr(AXUtilities, method_name, method) + +AXUtilities.start_cache_clearing_thread() diff --git a/src/cthulhu/ax_utilities_application.py b/src/cthulhu/ax_utilities_application.py new file mode 100644 index 0000000..dbaca2e --- /dev/null +++ b/src/cthulhu/ax_utilities_application.py @@ -0,0 +1,208 @@ +# Utilities for obtaining information about accessible applications. +# +# Copyright 2023-2024 Igalia, S.L. +# Copyright 2024 GNOME Foundation Inc. +# Author: Joanmarie Diggs +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., Franklin Street, Fifth Floor, +# Boston MA 02110-1301 USA. + +# pylint: disable=wrong-import-position + +"""Utilities for obtaining information about accessible applications.""" + +__id__ = "$Id$" +__version__ = "$Revision$" +__date__ = "$Date$" +__copyright__ = "Copyright (c) 2023-2024 Igalia, S.L." \ + "Copyright (c) 2024 GNOME Foundation Inc." +__license__ = "LGPL" + +import subprocess + +import gi +gi.require_version("Atspi", "2.0") +from gi.repository import Atspi +from gi.repository import GLib + +from . import debug +from .ax_object import AXObject + +class AXUtilitiesApplication: + """Utilities for obtaining information about accessible applications.""" + + @staticmethod + def application_as_string(obj: Atspi.Accessible) -> str: + """Returns the application details of obj as a string.""" + + app = AXUtilitiesApplication.get_application(obj) + if app is None: + return "" + + string = ( + f"{AXObject.get_name(app)} " + f"({AXUtilitiesApplication.get_application_toolkit_name(obj)} " + f"{AXUtilitiesApplication.get_application_toolkit_version(obj)})" + ) + return string + + @staticmethod + def get_all_applications( + must_have_window: bool = False, + exclude_unresponsive: bool = False, + is_debug: bool = False + ) -> list[Atspi.Accessible]: + """Returns a list of running applications known to Atspi.""" + + desktop = AXUtilitiesApplication.get_desktop() + if desktop is None: + return [] + + def pred(obj: Atspi.Accessible) -> bool: + if exclude_unresponsive and AXUtilitiesApplication.is_application_unresponsive(obj): + return False + if AXObject.get_name(obj) == "mutter-x11-frames": + return is_debug + if must_have_window: + return AXObject.get_child_count(obj) > 0 + return True + + return list(AXObject.iter_children(desktop, pred)) + + @staticmethod + def get_application(obj: Atspi.Accessible) -> Atspi.Accessible | None: + """Returns the accessible application associated with obj""" + + if obj is None: + return None + + try: + app = Atspi.Accessible.get_application(obj) + except GLib.GError as error: + msg = f"AXUtilitiesApplication: Exception in get_application: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return None + return app + + @staticmethod + def get_application_toolkit_name(obj: Atspi.Accessible) -> str: + """Returns the toolkit name reported for obj's application.""" + + app = AXUtilitiesApplication.get_application(obj) + if app is None: + return "" + + try: + name = Atspi.Accessible.get_toolkit_name(app) + except GLib.GError as error: + msg = f"AXUtilitiesApplication: Exception in get_application_toolkit_name: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return "" + + return name + + @staticmethod + def get_application_toolkit_version(obj: Atspi.Accessible) -> str: + """Returns the toolkit version reported for obj's application.""" + + app = AXUtilitiesApplication.get_application(obj) + if app is None: + return "" + + try: + version = Atspi.Accessible.get_toolkit_version(app) + except GLib.GError as error: + msg = f"AXUtilitiesApplication: Exception in get_application_toolkit_version: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return "" + + return version + + @staticmethod + def get_application_with_pid(pid: int) -> Atspi.Accessible | None: + """Returns the accessible application with the specified pid""" + + applications = AXUtilitiesApplication.get_all_applications() + for child in applications: + if AXUtilitiesApplication.get_process_id(child) == pid: + return child + + tokens = ["WARNING: app with pid", pid, "is not in the accessible desktop"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return None + + @staticmethod + def get_desktop() -> Atspi.Accessible | None: + """Returns the accessible desktop""" + + try: + desktop = Atspi.get_desktop(0) + except GLib.GError as error: + tokens = ["ERROR: Exception getting desktop from Atspi:", error] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return None + + return desktop + + @staticmethod + def get_process_id(obj: Atspi.Accessible) -> int: + """Returns the process id associated with obj""" + + try: + pid = Atspi.Accessible.get_process_id(obj) + except GLib.GError as error: + msg = f"AXUtilitiesApplication: Exception in get_process_id: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return -1 + + return pid + + @staticmethod + def is_application_in_desktop(app: Atspi.Accessible) -> bool: + """Returns true if app is known to Atspi""" + + applications = AXUtilitiesApplication.get_all_applications() + for child in applications: + if child == app: + return True + + tokens = ["WARNING:", app, "is not in the accessible desktop"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return False + + @staticmethod + def is_application_unresponsive(app: Atspi.Accessible) -> bool: + """Returns true if app's process is known to be unresponsive.""" + + pid = AXUtilitiesApplication.get_process_id(app) + try: + state = subprocess.getoutput(f"cat /proc/{pid}/status | grep State") + state = state.split()[1] + except (GLib.GError, IndexError) as error: + tokens = [f"AXUtilitiesApplication: Exception checking state of pid {pid}: {error}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return False + + if state == "Z": + tokens = [f"AXUtilitiesApplication: pid {pid} is zombie process"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return True + + if state == "T": + tokens = [f"AXUtilitiesApplication: pid {pid} is suspended/stopped process"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return True + + return False diff --git a/src/cthulhu/ax_utilities_collection.py b/src/cthulhu/ax_utilities_collection.py index 7b44591..9a356cf 100644 --- a/src/cthulhu/ax_utilities_collection.py +++ b/src/cthulhu/ax_utilities_collection.py @@ -1,9 +1,7 @@ -#!/usr/bin/env python3 +# Utilities for finding all objects that meet a certain criteria. # -# Copyright (c) 2024 Stormux -# Copyright (c) 2010-2012 The Orca Team -# Copyright (c) 2012 Igalia, S.L. -# Copyright (c) 2005-2010 Sun Microsystems Inc. +# Copyright 2023 Igalia, S.L. +# Author: Joanmarie Diggs # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -19,19 +17,12 @@ # License along with this library; if not, write to the # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. -# -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca -""" -Utilities for finding all objects that meet a certain criteria. -These utilities are app-type- and toolkit-agnostic. Utilities that might have -different implementations or results depending on the type of app (e.g. terminal, -chat, web) or toolkit (e.g. Qt, Gtk) should be in script_utilities.py file(s). +# pylint: disable=wrong-import-position +# pylint: disable=too-many-public-methods +# pylint: disable=too-many-lines -N.B. There are currently utilities that should never have custom implementations -that live in script_utilities.py files. These will be moved over time. -""" +"""Utilities for finding all objects that meet a certain criteria.""" __id__ = "$Id$" __version__ = "$Revision$" @@ -41,6 +32,7 @@ __license__ = "LGPL" import inspect import time +from typing import Callable import gi gi.require_version("Atspi", "2.0") @@ -49,6 +41,8 @@ from gi.repository import Atspi from . import debug from .ax_collection import AXCollection from .ax_object import AXObject +from .ax_utilities_debugging import AXUtilitiesDebugging +from .ax_utilities_relation import AXUtilitiesRelation from .ax_utilities_role import AXUtilitiesRole from .ax_utilities_state import AXUtilitiesState @@ -57,28 +51,36 @@ class AXUtilitiesCollection: """Utilities for finding all objects that meet a certain criteria.""" @staticmethod - def _apply_predicate(matches, pred): + def _apply_predicate( + matches: list[Atspi.Accessible], + pred: Callable[[Atspi.Accessible], bool] + ) -> list[Atspi.Accessible]: if not matches: return [] start = time.time() tokens = ["AXUtilitiesCollection: Applying predicate ", pred] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) matches = list(filter(pred, matches)) msg = f"AXUtilitiesCollection: {len(matches)} matches found in {time.time() - start:.4f}s" - debug.printMessage(debug.LEVEL_INFO, msg, True) + debug.print_message(debug.LEVEL_INFO, msg, True) return matches @staticmethod - def _find_all_with_states(root, state_list, state_match_type, pred=None): + def _find_all_with_states( + root: Atspi.Accessible, + state_list: list[Atspi.StateType], + state_match_type: Atspi.CollectionMatchType, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: if not (root and state_list): return [] state_list = list(state_list) tokens = ["AXUtilitiesCollection:", inspect.currentframe(), "Root:", root, state_match_type, "of:", state_list] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) rule = AXCollection.create_match_rule(states=state_list, state_match_type=state_match_type) matches = AXCollection.get_all_matches(root, rule) @@ -88,14 +90,19 @@ class AXUtilitiesCollection: return matches @staticmethod - def _find_all_with_role(root, role_list, role_match_type, pred=None): + def _find_all_with_role( + root: Atspi.Accessible, + role_list: list[Atspi.Role], + role_match_type: Atspi.CollectionMatchType, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: if not (root and role_list): return [] role_list = list(role_list) tokens = ["AXUtilitiesCollection:", inspect.currentframe(), "Root:", root, role_match_type, "of:", role_list] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) rule = AXCollection.create_match_rule(roles=role_list, role_match_type=role_match_type) matches = AXCollection.get_all_matches(root, rule) @@ -105,7 +112,11 @@ class AXUtilitiesCollection: return matches @staticmethod - def find_all_with_interfaces(root, interface_list, pred=None): + def find_all_with_interfaces( + root: Atspi.Accessible, + interface_list: list[str], + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root which implement all the specified interfaces""" if not (root and interface_list): @@ -114,7 +125,7 @@ class AXUtilitiesCollection: interface_list = list(interface_list) tokens = ["AXUtilitiesCollection:", inspect.currentframe(), "Root:", root, "all of:", interface_list] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) rule = AXCollection.create_match_rule(interfaces=interface_list) matches = AXCollection.get_all_matches(root, rule) @@ -124,21 +135,34 @@ class AXUtilitiesCollection: return matches @staticmethod - def find_all_with_role(root, role_list, pred=None): + def find_all_with_role( + root: Atspi.Accessible, + role_list: list[Atspi.Role], + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with any of the specified roles""" return AXUtilitiesCollection._find_all_with_role( root, role_list, Atspi.CollectionMatchType.ANY, pred) @staticmethod - def find_all_without_roles(root, role_list, pred=None): + def find_all_without_roles( + root: Atspi.Accessible, + role_list: list[Atspi.Role], + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root which have none of the specified roles""" return AXUtilitiesCollection._find_all_with_role( root, role_list, Atspi.CollectionMatchType.NONE, pred) @staticmethod - def find_all_with_role_and_all_states(root, role_list, state_list, pred=None): + def find_all_with_role_and_all_states( + root: Atspi.Accessible, + role_list: list[Atspi.Role], + state_list: list[Atspi.StateType], + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with any of the roles, and all the states""" if not (root and role_list and state_list): @@ -148,10 +172,11 @@ class AXUtilitiesCollection: state_list = list(state_list) tokens = ["AXUtilitiesCollection:", inspect.currentframe(), "Root:", root, "Roles:", role_list, "States:", state_list] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) rule = AXCollection.create_match_rule( - roles=role_list, states=state_list, state_match_type=Atspi.CollectionMatchType.ALL) + roles=role_list, role_match_type=Atspi.CollectionMatchType.ANY, + states=state_list, state_match_type=Atspi.CollectionMatchType.ALL) matches = AXCollection.get_all_matches(root, rule) if pred is not None: matches = AXUtilitiesCollection._apply_predicate(matches, pred) @@ -159,7 +184,12 @@ class AXUtilitiesCollection: return matches @staticmethod - def find_all_with_role_and_any_state(root, role_list, state_list, pred=None): + def find_all_with_role_and_any_state( + root: Atspi.Accessible, + role_list: list[Atspi.Role], + state_list: list[Atspi.StateType], + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with any of the roles, and any of the states""" if not (root and role_list and state_list): @@ -169,10 +199,11 @@ class AXUtilitiesCollection: state_list = list(state_list) tokens = ["AXUtilitiesCollection:", inspect.currentframe(), "Root:", root, "Roles:", role_list, "States:", state_list] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) rule = AXCollection.create_match_rule( - roles=role_list, states=state_list, state_match_type=Atspi.CollectionMatchType.ANY) + roles=role_list, role_match_type=Atspi.CollectionMatchType.ANY, + states=state_list, state_match_type=Atspi.CollectionMatchType.ANY) matches = AXCollection.get_all_matches(root, rule) if pred is not None: matches = AXUtilitiesCollection._apply_predicate(matches, pred) @@ -180,7 +211,12 @@ class AXUtilitiesCollection: return matches @staticmethod - def find_all_with_role_without_states(root, role_list, state_list, pred=None): + def find_all_with_role_without_states( + root: Atspi.Accessible, + role_list: list[Atspi.Role], + state_list: list[Atspi.StateType], + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with any of the roles, and none of the states""" if not (root and role_list and state_list): @@ -190,10 +226,11 @@ class AXUtilitiesCollection: state_list = list(state_list) tokens = ["AXUtilitiesCollection:", inspect.currentframe(), "Root:", root, "Roles:", role_list, "States:", state_list] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) rule = AXCollection.create_match_rule( - roles=role_list, states=state_list, state_match_type=Atspi.CollectionMatchType.NONE) + roles=role_list, role_match_type=Atspi.CollectionMatchType.ANY, + states=state_list, state_match_type=Atspi.CollectionMatchType.NONE) matches = AXCollection.get_all_matches(root, rule) if pred is not None: matches = AXUtilitiesCollection._apply_predicate(matches, pred) @@ -201,133 +238,193 @@ class AXUtilitiesCollection: return matches @staticmethod - def find_all_with_states(root, state_list, pred=None): + def find_all_with_states( + root: Atspi.Accessible, + state_list: list[Atspi.StateType], + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root which have all of the specified states""" return AXUtilitiesCollection._find_all_with_states( root, state_list, Atspi.CollectionMatchType.ALL, pred) @staticmethod - def find_all_with_any_state(root, state_list, pred=None): + def find_all_with_any_state( + root: Atspi.Accessible, + state_list: list[Atspi.StateType], + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root which have any of the specified states""" return AXUtilitiesCollection._find_all_with_states( root, state_list, Atspi.CollectionMatchType.ANY, pred) @staticmethod - def find_all_without_states(root, state_list, pred=None): + def find_all_without_states( + root: Atspi.Accessible, + state_list: list[Atspi.StateType], + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root which have none of the specified states""" return AXUtilitiesCollection._find_all_with_states( root, state_list, Atspi.CollectionMatchType.NONE, pred) @staticmethod - def find_all_accelerator_labels(root, pred=None): + def find_all_accelerator_labels( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the accelerator label role""" roles = [Atspi.Role.ACCELERATOR_LABEL] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_alerts(root, pred=None): + def find_all_alerts( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the alert role""" roles = [Atspi.Role.ALERT] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_animations(root, pred=None): + def find_all_animations( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the animation role""" roles = [Atspi.Role.ANIMATION] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_arrows(root, pred=None): + def find_all_arrows( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the arrow role""" roles = [Atspi.Role.ARROW] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_articles(root, pred=None): + def find_all_articles( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the article role""" roles = [Atspi.Role.ARTICLE] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_audios(root, pred=None): + def find_all_audios( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the audio role""" roles = [Atspi.Role.AUDIO] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_autocompletes(root, pred=None): + def find_all_autocompletes( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the autocomplete role""" roles = [Atspi.Role.AUTOCOMPLETE] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_block_quotes(root, pred=None): + def find_all_block_quotes( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the block quote role""" roles = [Atspi.Role.BLOCK_QUOTE] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_buttons(root, pred=None): + def find_all_buttons( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the push- or toggle-button role""" - roles = [Atspi.Role.PUSH_BUTTON, Atspi.Role.TOGGLE_BUTTON] + roles = [Atspi.Role.BUTTON, Atspi.Role.TOGGLE_BUTTON] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_calendars(root, pred=None): + def find_all_calendars( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the calendar role""" roles = [Atspi.Role.CALENDAR] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_canvases(root, pred=None): + def find_all_canvases( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the canvas role""" roles = [Atspi.Role.CANVAS] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_captions(root, pred=None): + def find_all_captions( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the caption role""" roles = [Atspi.Role.CAPTION] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_charts(root, pred=None): + def find_all_charts( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the chart role""" roles = [Atspi.Role.CHART] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_check_boxes(root, pred=None): + def find_all_check_boxes( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the checkbox role""" roles = [Atspi.Role.CHECK_BOX] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_check_menu_items(root, pred=None): + def find_all_check_menu_items( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the check menuitem role""" roles = [Atspi.Role.CHECK_MENU_ITEM] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_clickables(root, pred=None): + def find_all_clickables( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all non-focusable descendants of root which support the click action""" if root is None: @@ -343,13 +440,13 @@ class AXUtilitiesCollection: tokens = ["AXUtilitiesCollection:", inspect.currentframe(), "Root:", root, roles_match_type, "of:", roles, ". pred:", pred] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) def is_match(obj): result = AXObject.has_action(obj, "click") - tokens = ["AXUtilitiesCollection:", obj, AXObject.actions_as_string(obj), + tokens = ["AXUtilitiesCollection:", obj, AXUtilitiesDebugging.actions_as_string(obj), "has click Action:", result] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) if not result: return False return pred is None or pred(obj) @@ -367,182 +464,261 @@ class AXUtilitiesCollection: return matches @staticmethod - def find_all_color_choosers(root, pred=None): + def find_all_color_choosers( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the color_chooser role""" roles = [Atspi.Role.COLOR_CHOOSER] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_column_headers(root, pred=None): + def find_all_column_headers( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the column header role""" roles = [Atspi.Role.COLUMN_HEADER] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_combo_boxes(root, pred=None): + def find_all_combo_boxes( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the combobox role""" roles = [Atspi.Role.COMBO_BOX] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_comments(root, pred=None): + def find_all_comments( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the comment role""" roles = [Atspi.Role.COMMENT] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_content_deletions(root, pred=None): + def find_all_content_deletions( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the content deletion role""" roles = [Atspi.Role.CONTENT_DELETION] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_content_insertions(root, pred=None): + def find_all_content_insertions( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the content insertion role""" roles = [Atspi.Role.CONTENT_INSERTION] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_date_editors(root, pred=None): + def find_all_date_editors( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the date editor role""" roles = [Atspi.Role.DATE_EDITOR] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_definitions(root, pred=None): + def find_all_definitions( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the definition role""" roles = [Atspi.Role.DEFINITION] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_description_lists(root, pred=None): + def find_all_description_lists( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the description list role""" roles = [Atspi.Role.DESCRIPTION_LIST] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_description_terms(root, pred=None): + def find_all_description_terms( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the description term role""" roles = [Atspi.Role.DESCRIPTION_TERM] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_description_values(root, pred=None): + def find_all_description_values( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the description value role""" roles = [Atspi.Role.DESCRIPTION_VALUE] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_desktop_frames(root, pred=None): + def find_all_desktop_frames( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the desktop frame role""" roles = [Atspi.Role.DESKTOP_FRAME] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_desktop_icons(root, pred=None): + def find_all_desktop_icons( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the desktop icon role""" roles = [Atspi.Role.DESKTOP_ICON] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_dials(root, pred=None): + def find_all_dials( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the dial role""" roles = [Atspi.Role.DIAL] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_dialogs(root, pred=None): + def find_all_dialogs( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the dialog role""" roles = [Atspi.Role.DIALOG] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_dialogs_and_alerts(root, pred=None): + def find_all_dialogs_and_alerts( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root that has any dialog or alert role""" roles = AXUtilitiesRole.get_dialog_roles(True) return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_directory_panes(root, pred=None): + def find_all_directory_panes( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the directory pane role""" roles = [Atspi.Role.DIRECTORY_PANE] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_documents(root, pred=None): + def find_all_documents( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root that has any document-related role""" roles = AXUtilitiesRole.get_document_roles() return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_document_emails(root, pred=None): + def find_all_document_emails( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the document email role""" roles = [Atspi.Role.DOCUMENT_EMAIL] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_document_frames(root, pred=None): + def find_all_document_frames( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the document frame role""" roles = [Atspi.Role.DOCUMENT_FRAME] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_document_presentations(root, pred=None): + def find_all_document_presentations( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the document presentation role""" roles = [Atspi.Role.DOCUMENT_PRESENTATION] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_document_spreadsheets(root, pred=None): + def find_all_document_spreadsheets( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the document spreadsheet role""" roles = [Atspi.Role.DOCUMENT_SPREADSHEET] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_document_texts(root, pred=None): + def find_all_document_texts( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the document text role""" roles = [Atspi.Role.DOCUMENT_TEXT] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_document_webs(root, pred=None): + def find_all_document_webs( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the document web role""" roles = [Atspi.Role.DOCUMENT_WEB] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_drawing_areas(root, pred=None): + def find_all_drawing_areas( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the drawing area role""" roles = [Atspi.Role.DRAWING_AREA] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_editable_objects(root, must_be_focusable=True, pred=None): + def find_all_editable_objects( + root: Atspi.Accessible, + must_be_focusable: bool = True, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root which are editable""" states = [Atspi.StateType.EDITABLE] @@ -551,56 +727,80 @@ class AXUtilitiesCollection: return AXUtilitiesCollection.find_all_with_states(root, states, pred) @staticmethod - def find_all_editbars(root, pred=None): + def find_all_editbars( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the editbar role""" roles = [Atspi.Role.EDITBAR] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_embeddeds(root, pred=None): + def find_all_embeddeds( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the embedded role""" roles = [Atspi.Role.EMBEDDED] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_entries(root, pred=None): + def find_all_entries( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the entry role""" roles = [Atspi.Role.ENTRY] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_extendeds(root, pred=None): + def find_all_extendeds( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the extended role""" roles = [Atspi.Role.EXTENDED] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_file_choosers(root, pred=None): + def find_all_file_choosers( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the file chooser role""" roles = [Atspi.Role.FILE_CHOOSER] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_fillers(root, pred=None): + def find_all_fillers( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the filler role""" roles = [Atspi.Role.FILLER] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_focusable_objects(root, pred=None): + def find_all_focusable_objects( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root which are focusable""" states = [Atspi.StateType.FOCUSABLE] return AXUtilitiesCollection.find_all_with_states(root, states, pred) @staticmethod - def find_all_focusable_objects_with_click_ancestor(root, pred=None): + def find_all_focusable_objects_with_click_ancestor( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all focusable descendants of root which support the click-ancestor action""" if root is None: @@ -614,13 +814,13 @@ class AXUtilitiesCollection: tokens = ["AXUtilitiesCollection:", inspect.currentframe(), "Root:", root, roles_match_type, "of:", roles, ". pred:", pred] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) def is_match(obj): result = AXObject.has_action(obj, "click-ancestor") - tokens = ["AXUtilitiesCollection:", obj, AXObject.actions_as_string(obj), + tokens = ["AXUtilitiesCollection:", obj, AXUtilitiesDebugging.actions_as_string(obj), "has click-ancestor Action:", result] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) if not result: return False return pred is None or pred(obj) @@ -636,49 +836,71 @@ class AXUtilitiesCollection: return matches @staticmethod - def find_all_focused_objects(root, pred=None): + def find_all_focused_objects( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root which are focused""" states = [Atspi.StateType.FOCUSED] return AXUtilitiesCollection.find_all_with_states(root, states, pred) @staticmethod - def find_all_focus_traversables(root, pred=None): + def find_all_focus_traversables( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the focus traversable role""" roles = [Atspi.Role.FOCUS_TRAVERSABLE] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_font_choosers(root, pred=None): + def find_all_font_choosers( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the font chooser role""" roles = [Atspi.Role.FONT_CHOOSER] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_footers(root, pred=None): + def find_all_footers( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the footer role""" roles = [Atspi.Role.FOOTER] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_footnotes(root, pred=None): + def find_all_footnotes( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the footnote role""" roles = [Atspi.Role.FOOTNOTE] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_forms(root, pred=None): + def find_all_forms( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the form role""" roles = [Atspi.Role.FORM] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_form_fields(root, must_be_focusable=True, pred=None): + def find_all_form_fields( + root: Atspi.Accessible, + must_be_focusable: bool = True, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with a form-field-related role""" roles = AXUtilitiesRole.get_form_field_roles() @@ -689,21 +911,30 @@ class AXUtilitiesCollection: return AXUtilitiesCollection.find_all_with_role_and_all_states(root, roles, states, pred) @staticmethod - def find_all_frames(root, pred=None): + def find_all_frames( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the frame role""" roles = [Atspi.Role.FRAME] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_glass_panes(root, pred=None): + def find_all_glass_panes( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the glass pane role""" roles = [Atspi.Role.GLASS_PANE] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_grids(root, pred=None): + def find_all_grids( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root that are grids""" if root is None: @@ -711,7 +942,7 @@ class AXUtilitiesCollection: tokens = ["AXUtilitiesCollection:", inspect.currentframe(), "Root:", root, "pred:", pred] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) roles = [Atspi.Role.TABLE] attributes = ["xml-roles:grid"] @@ -723,7 +954,10 @@ class AXUtilitiesCollection: return grids @staticmethod - def find_all_grid_cells(root, pred=None): + def find_all_grid_cells( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root that are grid cells""" if root is None: @@ -735,7 +969,7 @@ class AXUtilitiesCollection: tokens = ["AXUtilitiesCollection:", inspect.currentframe(), "Root:", root, "pred:", pred] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) cells = [] for grid in grids: @@ -747,28 +981,41 @@ class AXUtilitiesCollection: return cells @staticmethod - def find_all_groupings(root, pred=None): + def find_all_groupings( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the grouping role""" roles = [Atspi.Role.GROUPING] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_headers(root, pred=None): + def find_all_headers( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the header role""" roles = [Atspi.Role.HEADER] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_headings(root, pred=None): + def find_all_headings( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the heading role""" roles = [Atspi.Role.HEADING] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_headings_at_level(root, level, pred=None): + def find_all_headings_at_level( + root: Atspi.Accessible, + level: int, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the heading role""" if root is None: @@ -776,7 +1023,7 @@ class AXUtilitiesCollection: tokens = ["AXUtilitiesCollection:", inspect.currentframe(), "Root:", root, "Level:", level, "pred:", pred] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) roles = [Atspi.Role.HEADING] attributes = [f"level:{level}"] @@ -787,14 +1034,20 @@ class AXUtilitiesCollection: return matches @staticmethod - def find_all_html_containers(root, pred=None): + def find_all_html_containers( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the html container role""" roles = [Atspi.Role.HTML_CONTAINER] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_horizontal_scrollbars(root, pred=None): + def find_all_horizontal_scrollbars( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root that is a horizontal scrollbar""" roles = [Atspi.Role.SCROLL_BAR] @@ -802,7 +1055,10 @@ class AXUtilitiesCollection: return AXUtilitiesCollection.find_all_with_role_and_all_states(root, roles, states, pred) @staticmethod - def find_all_horizontal_separators(root, pred=None): + def find_all_horizontal_separators( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root that is a horizontal separator""" roles = [Atspi.Role.SEPARATOR] @@ -810,7 +1066,10 @@ class AXUtilitiesCollection: return AXUtilitiesCollection.find_all_with_role_and_all_states(root, roles, states, pred) @staticmethod - def find_all_horizontal_sliders(root, pred=None): + def find_all_horizontal_sliders( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root that is a horizontal slider""" roles = [Atspi.Role.SLIDER] @@ -818,105 +1077,161 @@ class AXUtilitiesCollection: return AXUtilitiesCollection.find_all_with_role_and_all_states(root, roles, states, pred) @staticmethod - def find_all_icons(root, pred=None): + def find_all_icons( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the icon role""" roles = [Atspi.Role.ICON] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_icons_and_canvases(root, pred=None): + def find_all_icons_and_canvases( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the icon or canvas role""" roles = [Atspi.Role.ICON, Atspi.Role.CANVAS] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_images(root, pred=None): + def find_all_images( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the image role""" roles = [Atspi.Role.IMAGE] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_images_and_canvases(root, pred=None): + def find_all_images_and_canvases( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the image or canvas role""" roles = [Atspi.Role.IMAGE, Atspi.Role.CANVAS] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_images_and_image_maps(root, pred=None): + def find_all_images_and_image_maps( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the image or image map role""" roles = [Atspi.Role.IMAGE, Atspi.Role.IMAGE_MAP] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_image_maps(root, pred=None): + def find_all_image_maps( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the image map role""" roles = [Atspi.Role.IMAGE_MAP] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_info_bars(root, pred=None): + def find_all_info_bars( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the info bar role""" roles = [Atspi.Role.INFO_BAR] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_input_method_windows(root, pred=None): + def find_all_input_method_windows( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the input method window role""" roles = [Atspi.Role.INPUT_METHOD_WINDOW] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_internal_frames(root, pred=None): + def find_all_internal_frames( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the internal frame role""" roles = [Atspi.Role.INTERNAL_FRAME] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_labels(root, pred=None): + def find_all_labels( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the label role""" roles = [Atspi.Role.LABEL] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_labels_and_captions(root, pred=None): + def find_all_labels_and_captions( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the label or caption role""" roles = [Atspi.Role.LABEL, Atspi.Role.CAPTION] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_landmarks(root, pred=None): + def find_all_landmarks( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the landmark role""" roles = [Atspi.Role.LANDMARK] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_layered_panes(root, pred=None): + def find_all_large_containers( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: + """Returns all descendants of root that we consider a large container""" + + roles = AXUtilitiesRole.get_large_container_roles() + return AXUtilitiesCollection.find_all_with_role(root, roles, pred) + + @staticmethod + def find_all_layered_panes( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the layered pane role""" roles = [Atspi.Role.LAYERED_PANE] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_level_bars(root, pred=None): + def find_all_level_bars( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the level bar role""" roles = [Atspi.Role.LEVEL_BAR] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_links(root, must_be_focusable=True, pred=None): + def find_all_links( + root: Atspi.Accessible, + must_be_focusable: bool = True, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the link role""" roles = [Atspi.Role.LINK] @@ -927,7 +1242,10 @@ class AXUtilitiesCollection: return AXUtilitiesCollection.find_all_with_role_and_all_states(root, roles, states, pred) @staticmethod - def find_all_live_regions(root, pred=None): + def find_all_live_regions( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root that are live regions""" if root is None: @@ -935,14 +1253,15 @@ class AXUtilitiesCollection: tokens = ["AXUtilitiesCollection:", inspect.currentframe(), "Root:", root, "pred:", pred] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) attributes = [] levels = ["off", "polite", "assertive"] for level in levels: attributes.append('container-live:' + level) - rule = AXCollection.create_match_rule(attributes=attributes) + rule = AXCollection.create_match_rule(attributes=attributes, + attribute_match_type=Atspi.CollectionMatchType.ANY) matches = AXCollection.get_all_matches(root, rule) if pred is not None: matches = AXUtilitiesCollection._apply_predicate(matches, pred) @@ -950,105 +1269,162 @@ class AXUtilitiesCollection: return matches @staticmethod - def find_all_lists(root, pred=None): + def find_all_lists( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None, + include_description_lists: bool = False, + include_tab_lists: bool = False + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the list role""" roles = [Atspi.Role.LIST] + if include_description_lists: + roles.append(Atspi.Role.DESCRIPTION_LIST) + if include_tab_lists: + roles.append(Atspi.Role.PAGE_TAB_LIST) return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_list_boxes(root, pred=None): + def find_all_list_boxes( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the list box role""" roles = [Atspi.Role.LIST_BOX] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_list_items(root, pred=None): + def find_all_list_items( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None, + include_description_terms: bool = False, + include_tabs: bool = False + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the list item role""" roles = [Atspi.Role.LIST_ITEM] + if include_description_terms: + roles.append(Atspi.Role.DESCRIPTION_TERM) + if include_tabs: + roles.append(Atspi.Role.PAGE_TAB) return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_logs(root, pred=None): + def find_all_logs( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the log role""" roles = [Atspi.Role.LOG] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_marks(root, pred=None): + def find_all_marks( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the mark role""" roles = [Atspi.Role.MARK] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_marquees(root, pred=None): + def find_all_marquees( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the marquee role""" roles = [Atspi.Role.MARQUEE] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_maths(root, pred=None): + def find_all_maths( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the math role""" roles = [Atspi.Role.MATH] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_math_fractions(root, pred=None): + def find_all_math_fractions( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the math fraction role""" roles = [Atspi.Role.MATH_FRACTION] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_math_roots(root, pred=None): + def find_all_math_roots( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the math root role""" roles = [Atspi.Role.MATH_ROOT] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_menus(root, pred=None): + def find_all_menus( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the menu role""" roles = [Atspi.Role.MENU] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_menu_bars(root, pred=None): + def find_all_menu_bars( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the menubar role""" roles = [Atspi.Role.MENU_BAR] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_menu_items(root, pred=None): + def find_all_menu_items( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the menu item role""" roles = [Atspi.Role.MENU_ITEM] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_menu_items_of_any_kind(root, pred=None): + def find_all_menu_items_of_any_kind( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root that has any menu item role""" roles = AXUtilitiesRole.get_menu_item_roles() return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_menu_related_objects(root, pred=None): + def find_all_menu_related_objects( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root that has any menu-related role""" roles = AXUtilitiesRole.get_menu_related_roles() return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_modal_dialogs(root, pred=None): + def find_all_modal_dialogs( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the alert or dialog role and modal state""" roles = AXUtilitiesRole.get_dialog_roles(True) @@ -1056,7 +1432,10 @@ class AXUtilitiesCollection: return AXUtilitiesCollection.find_all_with_role_and_all_states(root, roles, states, pred) @staticmethod - def find_all_multi_line_entries(root, pred=None): + def find_all_multi_line_entries( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the entry role and multiline state""" roles = [Atspi.Role.ENTRY] @@ -1064,56 +1443,81 @@ class AXUtilitiesCollection: return AXUtilitiesCollection.find_all_with_role_and_all_states(root, roles, states, pred) @staticmethod - def find_all_notifications(root, pred=None): + def find_all_notifications( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the notification role""" roles = [Atspi.Role.NOTIFICATION] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_option_panes(root, pred=None): + def find_all_option_panes( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the option pane role""" roles = [Atspi.Role.OPTION_PANE] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_pages(root, pred=None): + def find_all_pages( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the page role""" roles = [Atspi.Role.PAGE] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_page_tabs(root, pred=None): + def find_all_page_tabs( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the page tab role""" roles = [Atspi.Role.PAGE_TAB] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_page_tab_lists(root, pred=None): + def find_all_page_tab_lists( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the page tab list role""" roles = [Atspi.Role.PAGE_TAB_LIST] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_page_tab_list_related_objects(root, pred=None): + def find_all_page_tab_list_related_objects( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the page tab or page tab list role""" roles = [Atspi.Role.PAGE_TAB_LIST, Atspi.Role.PAGE_TAB] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_panels(root, pred=None): + def find_all_panels( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the panel role""" roles = [Atspi.Role.PANEL] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_paragraphs(root, treat_headings_as_paragraphs=False, pred=None): + def find_all_paragraphs( + root: Atspi.Accessible, + treat_headings_as_paragraphs: bool = False, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the paragraph role""" roles = [Atspi.Role.PARAGRAPH] @@ -1122,154 +1526,220 @@ class AXUtilitiesCollection: return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_password_texts(root, pred=None): + def find_all_password_texts( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the password text role""" roles = [Atspi.Role.PASSWORD_TEXT] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_popup_menus(root, pred=None): + def find_all_popup_menus( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the popup menu role""" roles = [Atspi.Role.POPUP_MENU] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_progress_bars(root, pred=None): + def find_all_progress_bars( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the progress bar role""" roles = [Atspi.Role.PROGRESS_BAR] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_push_buttons(root, pred=None): + def find_all_push_buttons( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the push button role""" - roles = [Atspi.Role.PUSH_BUTTON] + roles = [Atspi.Role.BUTTON] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_push_button_menus(root, pred=None): + def find_all_push_button_menus( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the push button menu role""" roles = [Atspi.Role.PUSH_BUTTON_MENU] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_radio_buttons(root, pred=None): + def find_all_radio_buttons( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the radio button role""" roles = [Atspi.Role.RADIO_BUTTON] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_radio_menu_items(root, pred=None): + def find_all_radio_menu_items( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the radio menu item role""" roles = [Atspi.Role.RADIO_MENU_ITEM] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_ratings(root, pred=None): + def find_all_ratings( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the rating role""" roles = [Atspi.Role.RATING] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_root_panes(root, pred=None): + def find_all_root_panes( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the root pane role""" roles = [Atspi.Role.ROOT_PANE] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_row_headers(root, pred=None): + def find_all_row_headers( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the row header role""" roles = [Atspi.Role.ROW_HEADER] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_rulers(root, pred=None): + def find_all_rulers( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the ruler role""" roles = [Atspi.Role.RULER] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_scroll_bars(root, pred=None): + def find_all_scroll_bars( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the scrollbar role""" roles = [Atspi.Role.SCROLL_BAR] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_scroll_panes(root, pred=None): + def find_all_scroll_panes( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the scroll pane role""" roles = [Atspi.Role.SCROLL_PANE] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_sections(root, pred=None): + def find_all_sections( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the section role""" roles = [Atspi.Role.SECTION] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_selectable_objects(root, pred=None): + def find_all_selectable_objects( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root which are selectable""" states = [Atspi.StateType.SELECTABLE] return AXUtilitiesCollection.find_all_with_states(root, states, pred) @staticmethod - def find_all_selected_objects(root, pred=None): + def find_all_selected_objects( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root which are selected""" states = [Atspi.StateType.SELECTED] return AXUtilitiesCollection.find_all_with_states(root, states, pred) @staticmethod - def find_all_separators(root, pred=None): + def find_all_separators( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the separator role""" roles = [Atspi.Role.SEPARATOR] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_set_containers(root, pred=None): + def find_all_set_containers( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with a set container role""" roles = AXUtilitiesRole.get_set_container_roles() return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_showing_objects(root, pred=None): + def find_all_showing_objects( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root which are showing""" states = [Atspi.StateType.SHOWING] return AXUtilitiesCollection.find_all_with_states(root, states, pred) @staticmethod - def find_all_showing_and_visible_objects(root, pred=None): + def find_all_showing_and_visible_objects( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root which are showing and visible""" states = [Atspi.StateType.SHOWING, Atspi.StateType.VISIBLE] return AXUtilitiesCollection.find_all_with_states(root, states, pred) @staticmethod - def find_all_showing_or_visible_objects(root, pred=None): + def find_all_showing_or_visible_objects( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root which are showing or visible""" states = [Atspi.StateType.SHOWING, Atspi.StateType.VISIBLE] return AXUtilitiesCollection.find_all_with_any_state(root, states, pred) @staticmethod - def find_all_single_line_entries(root, pred=None): + def find_all_single_line_entries( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the entry role and multiline state""" roles = [Atspi.Role.ENTRY] @@ -1277,298 +1747,443 @@ class AXUtilitiesCollection: return AXUtilitiesCollection.find_all_with_role_and_all_states(root, roles, states, pred) @staticmethod - def find_all_sliders(root, pred=None): + def find_all_sliders( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the slider role""" roles = [Atspi.Role.SLIDER] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_spin_buttons(root, pred=None): + def find_all_spin_buttons( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the spin button role""" roles = [Atspi.Role.SPIN_BUTTON] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_split_panes(root, pred=None): + def find_all_split_panes( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the split pane role""" roles = [Atspi.Role.SPLIT_PANE] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_statics(root, pred=None): + def find_all_statics( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the static role""" roles = [Atspi.Role.STATIC] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_status_bars(root, pred=None): + def find_all_status_bars( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the statusbar role""" roles = [Atspi.Role.STATUS_BAR] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_subscripts(root, pred=None): + def find_all_subscripts( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the subscript role""" roles = [Atspi.Role.SUBSCRIPT] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_subscripts_and_superscripts(root, pred=None): + def find_all_subscripts_and_superscripts( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the subscript or superscript role""" roles = [Atspi.Role.SUBSCRIPT, Atspi.Role.SUPERSCRIPT] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_suggestions(root, pred=None): + def find_all_suggestions( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the suggestion role""" roles = [Atspi.Role.SUGGESTION] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_superscripts(root, pred=None): + def find_all_superscripts( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the superscript role""" roles = [Atspi.Role.SUPERSCRIPT] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_supports_action(root, pred=None): + def find_all_supports_action( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root which support the action interface""" interfaces = ["Action"] return AXUtilitiesCollection.find_all_with_interfaces(root, interfaces, pred) @staticmethod - def find_all_supports_document(root, pred=None): + def find_all_supports_document( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root which support the document interface""" interfaces = ["Document"] return AXUtilitiesCollection.find_all_with_interfaces(root, interfaces, pred) @staticmethod - def find_all_supports_editable_text(root, pred=None): + def find_all_supports_editable_text( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root which support the editable text interface""" interfaces = ["EditableText"] return AXUtilitiesCollection.find_all_with_interfaces(root, interfaces, pred) @staticmethod - def find_all_supports_hypertext(root, pred=None): + def find_all_supports_hypertext( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root which support the hypertext interface""" interfaces = ["Hypertext"] return AXUtilitiesCollection.find_all_with_interfaces(root, interfaces, pred) @staticmethod - def find_all_supports_hyperlink(root, pred=None): + def find_all_supports_hyperlink( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root which support the hyperlink interface""" interfaces = ["Hyperlink"] return AXUtilitiesCollection.find_all_with_interfaces(root, interfaces, pred) @staticmethod - def find_all_supports_selection(root, pred=None): + def find_all_supports_selection( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root which support the selection interface""" interfaces = ["Selection"] return AXUtilitiesCollection.find_all_with_interfaces(root, interfaces, pred) @staticmethod - def find_all_supports_table(root, pred=None): + def find_all_supports_table( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root which support the table interface""" interfaces = ["Table"] return AXUtilitiesCollection.find_all_with_interfaces(root, interfaces, pred) @staticmethod - def find_all_supports_table_cell(root, pred=None): + def find_all_supports_table_cell( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root which support the table cell interface""" interfaces = ["TableCell"] return AXUtilitiesCollection.find_all_with_interfaces(root, interfaces, pred) @staticmethod - def find_all_supports_text(root, pred=None): + def find_all_supports_text( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root which support the text interface""" interfaces = ["Text"] return AXUtilitiesCollection.find_all_with_interfaces(root, interfaces, pred) @staticmethod - def find_all_supports_value(root, pred=None): + def find_all_supports_value( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root which support the value interface""" interfaces = ["Value"] return AXUtilitiesCollection.find_all_with_interfaces(root, interfaces, pred) @staticmethod - def find_all_tables(root, pred=None): + def find_all_tables( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the table role""" + if root is None: + return [] + + tokens = ["AXUtilitiesCollection:", inspect.currentframe(), "Root:", root, "pred:", pred] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + roles = [Atspi.Role.TABLE] - return AXUtilitiesCollection.find_all_with_role(root, roles, pred) + attributes = ["layout-guess:true"] + attribute_match_type = Atspi.CollectionMatchType.NONE + rule = AXCollection.create_match_rule( + roles=roles, + attributes=attributes, + attribute_match_type=attribute_match_type) + + tables = AXCollection.get_all_matches(root, rule) + if pred is not None: + AXUtilitiesCollection._apply_predicate(tables, pred) + + return tables @staticmethod - def find_all_table_cells(root, pred=None): + def find_all_table_cells( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the table cell role""" roles = [Atspi.Role.TABLE_CELL] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_table_cells_and_headers(root, pred=None): + def find_all_table_cells_and_headers( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the table cell or a header-related role""" roles = AXUtilitiesRole.get_table_cell_roles() return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_table_column_headers(root, pred=None): + def find_all_table_column_headers( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the table column header role""" roles = [Atspi.Role.TABLE_COLUMN_HEADER] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_table_headers(root, pred=None): + def find_all_table_headers( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root that has a table header related role""" roles = AXUtilitiesRole.get_table_header_roles() return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_table_related_objects(root, pred=None, include_caption=False): + def find_all_table_related_objects( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None, + include_caption: bool = False + ) -> list[Atspi.Accessible]: """Returns all descendants of root that has a table related role""" roles = AXUtilitiesRole.get_table_related_roles(include_caption) return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_table_rows(root, pred=None): + def find_all_table_rows( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the table row role""" roles = [Atspi.Role.TABLE_ROW] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_table_row_headers(root, pred=None): + def find_all_table_row_headers( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the table row header role""" roles = [Atspi.Role.TABLE_ROW_HEADER] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_tearoff_menu_items(root, pred=None): + def find_all_tearoff_menu_items( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the tearoff menu item role""" roles = [Atspi.Role.TEAROFF_MENU_ITEM] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_terminals(root, pred=None): + def find_all_terminals( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the terminal role""" roles = [Atspi.Role.TERMINAL] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_texts(root, pred=None): + def find_all_texts( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the text role""" roles = [Atspi.Role.TEXT] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_text_inputs(root, pred=None): + def find_all_text_inputs( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root that has any role associated with textual input""" roles = [Atspi.Role.ENTRY, Atspi.Role.PASSWORD_TEXT, Atspi.Role.SPIN_BUTTON] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_timers(root, pred=None): + def find_all_timers( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the timer role""" roles = [Atspi.Role.TIMER] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_title_bars(root, pred=None): + def find_all_title_bars( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the titlebar role""" roles = [Atspi.Role.TITLE_BAR] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_toggle_buttons(root, pred=None): + def find_all_toggle_buttons( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the toggle button role""" roles = [Atspi.Role.TOGGLE_BUTTON] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_tool_bars(root, pred=None): + def find_all_tool_bars( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the toolbar role""" roles = [Atspi.Role.TOOL_BAR] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_tool_tips(root, pred=None): + def find_all_tool_tips( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the tooltip role""" roles = [Atspi.Role.TOOL_TIP] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_trees(root, pred=None): + def find_all_trees( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the tree role""" roles = [Atspi.Role.TREE] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_trees_and_tree_tables(root, pred=None): + def find_all_trees_and_tree_tables( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the tree or tree table role""" roles = [Atspi.Role.TREE, Atspi.Role.TREE_TABLE] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_tree_related_objects(root, pred=None): + def find_all_tree_related_objects( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root that has a tree related role""" roles = AXUtilitiesRole.get_tree_related_roles() return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_tree_items(root, pred=None): + def find_all_tree_items( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the tree item role""" roles = [Atspi.Role.TREE_ITEM] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_tree_tables(root, pred=None): + def find_all_tree_tables( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the tree table role""" roles = [Atspi.Role.TREE_TABLE] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_unrelated_labels(root, must_be_showing=True, pred=None): + def find_all_unrelated_labels( + root: Atspi.Accessible, + must_be_showing: bool = True, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all the descendants of root that have a label role, but no relations""" def _pred(obj): - if AXObject.get_relations(obj): + if not AXUtilitiesRelation.object_is_unrelated(obj): return False if pred is not None: return pred(obj) @@ -1585,7 +2200,11 @@ class AXUtilitiesCollection: return matches @staticmethod - def find_all_unvisited_links(root, must_be_focusable=True, pred=None): + def find_all_unvisited_links( + root: Atspi.Accessible, + must_be_focusable: bool = True, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the link role and without the visited state""" roles = [Atspi.Role.LINK] @@ -1596,7 +2215,10 @@ class AXUtilitiesCollection: return result @staticmethod - def find_all_vertical_scrollbars(root, pred=None): + def find_all_vertical_scrollbars( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root that is a vertical scrollbar""" roles = [Atspi.Role.SCROLL_BAR] @@ -1604,7 +2226,10 @@ class AXUtilitiesCollection: return AXUtilitiesCollection.find_all_with_role_and_all_states(root, roles, states, pred) @staticmethod - def find_all_vertical_separators(root, pred=None): + def find_all_vertical_separators( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root that is a vertical separator""" roles = [Atspi.Role.SEPARATOR] @@ -1612,7 +2237,10 @@ class AXUtilitiesCollection: return AXUtilitiesCollection.find_all_with_role_and_all_states(root, roles, states, pred) @staticmethod - def find_all_vertical_sliders(root, pred=None): + def find_all_vertical_sliders( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root that is a vertical slider""" roles = [Atspi.Role.SLIDER] @@ -1620,28 +2248,41 @@ class AXUtilitiesCollection: return AXUtilitiesCollection.find_all_with_role_and_all_states(root, roles, states, pred) @staticmethod - def find_all_visible_objects(root, pred=None): + def find_all_visible_objects( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root which are visible""" states = [Atspi.StateType.VISIBLE] return AXUtilitiesCollection.find_all_with_states(root, states, pred) @staticmethod - def find_all_videos(root, pred=None): + def find_all_videos( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the video role""" roles = [Atspi.Role.VIDEO] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_viewports(root, pred=None): + def find_all_viewports( + root: Atspi.Accessible, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the viewport role""" roles = [Atspi.Role.VIEWPORT] return AXUtilitiesCollection.find_all_with_role(root, roles, pred) @staticmethod - def find_all_visited_links(root, must_be_focusable=True, pred=None): + def find_all_visited_links( + root: Atspi.Accessible, + must_be_focusable: bool = True, + pred: Callable[[Atspi.Accessible], bool] | None = None + ) -> list[Atspi.Accessible]: """Returns all descendants of root with the link role and focused and visited states""" roles = [Atspi.Role.LINK] @@ -1651,16 +2292,16 @@ class AXUtilitiesCollection: return AXUtilitiesCollection.find_all_with_role_and_all_states(root, roles, states, pred) @staticmethod - def find_default_button(root): + def find_default_button(root: Atspi.Accessible) -> Atspi.Accessible | None: """Returns the default button inside root""" - roles = [Atspi.Role.PUSH_BUTTON] + roles = [Atspi.Role.BUTTON] states = [Atspi.StateType.IS_DEFAULT] rule = AXCollection.create_match_rule(roles=roles, states=states) return AXCollection.get_first_match(root, rule) @staticmethod - def find_focused_object(root): + def find_focused_object(root: Atspi.Accessible) -> Atspi.Accessible | None: """Returns the focused object inside root""" states = [Atspi.StateType.FOCUSED] @@ -1668,10 +2309,69 @@ class AXUtilitiesCollection: return AXCollection.get_first_match(root, rule) @staticmethod - def find_status_bar(root): + def find_info_bar(root: Atspi.Accessible) -> Atspi.Accessible | None: + """Returns the info bar inside root""" + + roles = [Atspi.Role.INFO_BAR] + states = [Atspi.StateType.SHOWING, Atspi.StateType.VISIBLE] + rule = AXCollection.create_match_rule(roles=roles, states=states) + return AXCollection.get_first_match(root, rule) + + @staticmethod + def find_status_bar(root: Atspi.Accessible) -> Atspi.Accessible | None: """Returns the status bar inside root""" roles = [Atspi.Role.STATUS_BAR] states = [Atspi.StateType.SHOWING, Atspi.StateType.VISIBLE] rule = AXCollection.create_match_rule(roles=roles, states=states) return AXCollection.get_first_match(root, rule) + + @staticmethod + def has_combo_box_or_list_box(root: Atspi.Accessible) -> bool: + """Returns True if there's a showing, visible combobox or listbox inside root""" + + roles = [Atspi.Role.COMBO_BOX, Atspi.Role.LIST_BOX] + states = [Atspi.StateType.SHOWING, Atspi.StateType.VISIBLE] + rule = AXCollection.create_match_rule(roles=roles, + role_match_type=Atspi.CollectionMatchType.ANY, + states=states) + return bool(AXCollection.get_first_match(root, rule)) + + @staticmethod + def has_editable_object(root: Atspi.Accessible) -> bool: + """Returns True if there's a showing, visible, editable object inside root""" + + states = [Atspi.StateType.SHOWING, Atspi.StateType.VISIBLE, Atspi.StateType.EDITABLE] + rule = AXCollection.create_match_rule(states=states) + return bool(AXCollection.get_first_match(root, rule)) + + @staticmethod + def has_scroll_pane(root: Atspi.Accessible) -> bool: + """Returns True if there's a showing, visible scroll pane inside root""" + + roles = [Atspi.Role.SCROLL_PANE] + states = [Atspi.StateType.SHOWING, Atspi.StateType.VISIBLE] + rule = AXCollection.create_match_rule(roles=roles, + role_match_type=Atspi.CollectionMatchType.ANY, + states=states) + return bool(AXCollection.get_first_match(root, rule)) + + @staticmethod + def has_split_pane(root: Atspi.Accessible) -> bool: + """Returns True if there's a showing, visible split pane inside root""" + + roles = [Atspi.Role.SPLIT_PANE] + states = [Atspi.StateType.SHOWING, Atspi.StateType.VISIBLE] + rule = AXCollection.create_match_rule(roles=roles, states=states) + return bool(AXCollection.get_first_match(root, rule)) + + @staticmethod + def has_tree_or_tree_table(root: Atspi.Accessible) -> bool: + """Returns True if there's a showing, visible tree or tree table inside root""" + + roles = [Atspi.Role.TREE, Atspi.Role.TREE_TABLE] + states = [Atspi.StateType.SHOWING, Atspi.StateType.VISIBLE] + rule = AXCollection.create_match_rule(roles=roles, + role_match_type=Atspi.CollectionMatchType.ANY, + states=states) + return bool(AXCollection.get_first_match(root, rule)) diff --git a/src/cthulhu/ax_utilities_debugging.py b/src/cthulhu/ax_utilities_debugging.py new file mode 100644 index 0000000..3bb1dde --- /dev/null +++ b/src/cthulhu/ax_utilities_debugging.py @@ -0,0 +1,285 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 Stormux +# Copyright 2024 Igalia, S.L. +# Copyright 2024 GNOME Foundation Inc. +# Author: Joanmarie Diggs +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., Franklin Street, Fifth Floor, +# Boston MA 02110-1301 USA. +# +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu + +# pylint: disable=wrong-import-position +# pylint: disable=too-many-branches +# pylint: disable=too-many-public-methods +# pylint: disable=too-many-return-statements + +"""Utilities for obtaining accessibility information for debugging.""" + +__id__ = "$Id$" +__version__ = "$Revision$" +__date__ = "$Date$" +__copyright__ = "Copyright (c) 2024 Igalia, S.L." \ + "Copyright (c) 2024 GNOME Foundation Inc." +__license__ = "LGPL" + +import inspect +import pprint +import types +from typing import Any + +import gi +gi.require_version("Atspi", "2.0") +from gi.repository import Atspi +from gi.repository import GLib + +from .ax_object import AXObject +from .ax_utilities_application import AXUtilitiesApplication +from .ax_utilities_relation import AXUtilitiesRelation + + +class AXUtilitiesDebugging: + """Utilities for obtaining accessibility information for debugging.""" + + @staticmethod + def _format_string(string: str = "") -> str: + if not string: + return "" + + string = string.replace("\n", "\\n").replace("\ufffc", "[OBJ]") + if len(string) < 100: + return string + + words = string.split() + string = f"{' '.join(words[:5])} ... {' '.join(words[-5:])} ({len(string)} chars.)" + return string + + @staticmethod + def as_string(obj: Any) -> str: + """Turns obj into a human-consumable string.""" + + if isinstance(obj, Atspi.Accessible): + result = AXObject.get_role_name(obj) + name = AXObject.get_name(obj) + if name: + result += f": '{AXUtilitiesDebugging._format_string(name)}'" + if not result: + result = "DEAD" + + return f"[{result} ({hex(id(obj))})] " + + if isinstance(obj, Atspi.Event): + any_data = AXUtilitiesDebugging._format_string( + AXUtilitiesDebugging.as_string(obj.any_data)) + return ( + f"{obj.type} for {AXUtilitiesDebugging.as_string(obj.source)} in " + f"{AXUtilitiesApplication.application_as_string(obj.source)} " + f"({obj.detail1}, {obj.detail2}, {any_data})" + ) + + if isinstance(obj, (Atspi.Role, Atspi.StateType, Atspi.CollectionMatchType, + Atspi.TextGranularity, Atspi.ScrollType)): + return obj.value_nick + + if isinstance(obj, Atspi.Rect): + return f"(x:{obj.x}, y:{obj.y}, width:{obj.width}, height:{obj.height})" + + if isinstance(obj, (list, set)): + return f"[{', '.join(map(AXUtilitiesDebugging.as_string, obj))}]" + + if isinstance(obj, dict): + stringified = {key: AXUtilitiesDebugging.as_string(value) for key, value in obj.items()} + formatter = pprint.PrettyPrinter(width=150) + return f"{formatter.pformat(stringified)}" + + if isinstance(obj, types.FunctionType): + if hasattr(obj, "__self__"): + return f"{obj.__module__}.{obj.__self__.__class__.__name__}.{obj.__name__}" + return f"{obj.__module__}.{obj.__name__}" + + if isinstance(obj, types.MethodType): + if hasattr(obj, "__self__"): + return f"{obj.__self__.__class__.__name__}.{obj.__name__}" + return f"{obj.__name__}" + + if isinstance(obj, types.FrameType): + module_name = inspect.getmodulename(obj.f_code.co_filename) + return f"{module_name}.{obj.f_code.co_name}" + + if isinstance(obj, inspect.FrameInfo): + module_name = inspect.getmodulename(obj.filename) or "" + return f"{module_name}.{obj.function}:{obj.lineno}" + + return str(obj) + + @staticmethod + def actions_as_string(obj: Atspi.Accessible) -> str: + """Returns information about the actions as a string.""" + + results = [] + for i in range(AXObject.get_n_actions(obj)): + result = AXObject.get_action_name(obj, i) + keybinding = AXObject.get_action_key_binding(obj, i) + if keybinding: + result += f" ({keybinding})" + results.append(result) + + return "; ".join(results) + + @staticmethod + def attributes_as_string(obj: Atspi.Accessible) -> str: + """Returns the object attributes of obj as a string.""" + + def as_string(attribute): + return f"{attribute[0]}:{attribute[1]}" + + return ", ".join(map(as_string, AXObject.get_attributes_dict(obj).items())) + + @staticmethod + def interfaces_as_string(obj: Atspi.Accessible) -> str: + """Returns the supported interfaces of obj as a string.""" + + if not AXObject.is_valid(obj): + return "" + + iface_checks = [ + (AXObject.supports_action, "Action"), + (AXObject.supports_collection, "Collection"), + (AXObject.supports_component, "Component"), + (AXObject.supports_document, "Document"), + (AXObject.supports_editable_text, "EditableText"), + (AXObject.supports_hyperlink, "Hyperlink"), + (AXObject.supports_hypertext, "Hypertext"), + (AXObject.supports_image, "Image"), + (AXObject.supports_selection, "Selection"), + (AXObject.supports_table, "Table"), + (AXObject.supports_table_cell, "TableCell"), + (AXObject.supports_text, "Text"), + (AXObject.supports_value, "Value"), + ] + + ifaces = [iface for check, iface in iface_checks if check(obj)] + return ", ".join(ifaces) + + @staticmethod + def relations_as_string(obj: Atspi.Accessible) -> str: + """Returns the relations associated with obj as a string.""" + + if not AXObject.is_valid(obj): + return "" + + def as_string(relations): + return relations.value_name[15:].replace("_", "-").lower() + + def obj_as_string(acc): + result = AXObject.get_role_name(acc) + name = AXObject.get_name(acc) + if name: + result += f": '{name}'" + if not result: + result = "DEAD" + return f"[{result}]" + + results = [] + for rel in AXUtilitiesRelation.get_relations(obj): + type_string = as_string(rel.get_relation_type()) + targets = AXUtilitiesRelation.get_relation_targets_for_debugging( + obj, rel.get_relation_type()) + target_string = ",".join(map(obj_as_string, targets)) + results.append(f"{type_string}: {target_string}") + + return "; ".join(results) + + @staticmethod + def state_set_as_string(obj: Atspi.Accessible) -> str: + """Returns the state set associated with obj as a string.""" + + if not AXObject.is_valid(obj): + return "" + + def as_string(state): + return state.value_name[12:].replace("_", "-").lower() + + return ", ".join(map(as_string, AXObject.get_state_set(obj).get_states())) + + @staticmethod + def text_for_debugging(obj: Atspi.Accessible) -> str: + """Returns the text content of obj for debugging.""" + + if not AXObject.supports_text(obj): + return "" + + try: + result = Atspi.Text.get_text(obj, 0, Atspi.Text.get_character_count(obj)) + except GLib.GError: + return "" + + return AXUtilitiesDebugging._format_string(result) + + @staticmethod + def object_details_as_string( + obj: Atspi.Accessible, + indent: str = "", + include_app: bool = True + ) -> str: + """Returns a string, suitable for printing, that describes details about obj.""" + + if not isinstance(obj, Atspi.Accessible): + return "" + + if AXObject.is_dead(obj): + return "(exception fetching data)" + + if include_app: + string = f"{indent}app='{AXUtilitiesApplication.application_as_string(obj)}' " + else: + string = indent + + name = AXUtilitiesDebugging._format_string(AXObject.get_name(obj)) + desc = AXUtilitiesDebugging._format_string(AXObject.get_description(obj)) + help_text = AXUtilitiesDebugging._format_string(AXObject.get_help_text(obj)) + obj_locale = AXObject.get_locale(obj) + ax_id = AXObject.get_accessible_id(obj) + string += ( + f"name='{name}' role='{AXObject.get_role_name(obj)}'" + f" axid='{ax_id}' id={hex(id(obj))}\n" + f"{indent}description='{desc}'\n" + f"{indent}help='{help_text}'\n" + f"{indent}locale='{obj_locale}'\n" + f"{indent}states='{AXUtilitiesDebugging.state_set_as_string(obj)}'\n" + f"{indent}relations='{AXUtilitiesDebugging.relations_as_string(obj)}'\n" + f"{indent}actions='{AXUtilitiesDebugging.actions_as_string(obj)}'\n" + f"{indent}interfaces='{AXUtilitiesDebugging.interfaces_as_string(obj)}'\n" + f"{indent}attributes='{AXUtilitiesDebugging.attributes_as_string(obj)}'\n" + f"{indent}text='{AXUtilitiesDebugging.text_for_debugging(obj)}'\n" + f"{indent}path={AXObject.get_path(obj)}" + ) + return string + + @staticmethod + def object_event_details_as_string(event: Atspi.Event, indent: str = "") -> str: + """Returns a string, suitable for printing, with details about event.""" + + if event.type.startswith("mouse:"): + return "" + + source = AXUtilitiesDebugging.object_details_as_string(event.source, indent, True) + any_data = AXUtilitiesDebugging.object_details_as_string(event.any_data, indent, False) + string = f"EVENT SOURCE:\n{source}\n" + if any_data: + string += f"\nEVENT ANY DATA:\n{any_data}\n" + return string diff --git a/src/cthulhu/ax_utilities_event.py b/src/cthulhu/ax_utilities_event.py new file mode 100644 index 0000000..29b0e9c --- /dev/null +++ b/src/cthulhu/ax_utilities_event.py @@ -0,0 +1,838 @@ +# Utilities for obtaining event-related information. +# +# Copyright 2024 Igalia, S.L. +# Copyright 2024 GNOME Foundation Inc. +# Author: Joanmarie Diggs +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., Franklin Street, Fifth Floor, +# Boston MA 02110-1301 USA. + +# pylint: disable=wrong-import-position +# pylint: disable=too-many-return-statements +# pylint: disable=too-many-branches +# pylint: disable=too-many-statements + +"""Utilities for obtaining event-related information.""" + +__id__ = "$Id$" +__version__ = "$Revision$" +__date__ = "$Date$" +__copyright__ = "Copyright (c) 2024 Igalia, S.L." \ + "Copyright (c) 2024 GNOME Foundation Inc." +__license__ = "LGPL" + +import enum +import threading +import time + +import gi +gi.require_version("Atspi", "2.0") +from gi.repository import Atspi + +from . import debug +from . import focus_manager + +from .ax_object import AXObject +from .ax_text import AXText +from .ax_utilities_role import AXUtilitiesRole +from .ax_utilities_state import AXUtilitiesState + +class TextEventReason(enum.Enum): + """Enum representing the reason for an object:text- event.""" + + UNKNOWN = enum.auto() + AUTO_DELETION = enum.auto() + AUTO_INSERTION_PRESENTABLE = enum.auto() + AUTO_INSERTION_UNPRESENTABLE = enum.auto() + BACKSPACE = enum.auto() + CHILDREN_CHANGE = enum.auto() + CUT = enum.auto() + DELETE = enum.auto() + FOCUS_CHANGE = enum.auto() + LIVE_REGION_UPDATE = enum.auto() + MOUSE_MIDDLE_BUTTON = enum.auto() + MOUSE_PRIMARY_BUTTON = enum.auto() + NAVIGATION_BY_CHARACTER = enum.auto() + NAVIGATION_BY_LINE = enum.auto() + NAVIGATION_BY_PARAGRAPH = enum.auto() + NAVIGATION_BY_PAGE = enum.auto() + NAVIGATION_BY_WORD = enum.auto() + NAVIGATION_TO_FILE_BOUNDARY = enum.auto() + NAVIGATION_TO_LINE_BOUNDARY = enum.auto() + PAGE_SWITCH = enum.auto() + PASTE = enum.auto() + REDO = enum.auto() + SAY_ALL = enum.auto() + SEARCH_PRESENTABLE = enum.auto() + SEARCH_UNPRESENTABLE = enum.auto() + SELECT_ALL = enum.auto() + SELECTED_TEXT_DELETION = enum.auto() + SELECTED_TEXT_INSERTION = enum.auto() + SELECTED_TEXT_RESTORATION = enum.auto() + SELECTION_BY_CHARACTER = enum.auto() + SELECTION_BY_LINE = enum.auto() + SELECTION_BY_PARAGRAPH = enum.auto() + SELECTION_BY_PAGE = enum.auto() + SELECTION_BY_WORD = enum.auto() + SELECTION_TO_FILE_BOUNDARY = enum.auto() + SELECTION_TO_LINE_BOUNDARY = enum.auto() + SPIN_BUTTON_VALUE_CHANGE = enum.auto() + TYPING = enum.auto() + TYPING_ECHOABLE = enum.auto() + UI_UPDATE = enum.auto() + UNDO = enum.auto() + UNSPECIFIED_COMMAND = enum.auto() + UNSPECIFIED_NAVIGATION = enum.auto() + UNSPECIFIED_SELECTION = enum.auto() + + +class AXUtilitiesEvent: + """Utilities for obtaining event-related information.""" + + LAST_KNOWN_DESCRIPTION: dict[int, str] = {} + LAST_KNOWN_NAME: dict[int, str] = {} + + LAST_KNOWN_CHECKED: dict[int, bool] = {} + LAST_KNOWN_EXPANDED: dict[int, bool] = {} + LAST_KNOWN_INDETERMINATE: dict[int, bool] = {} + LAST_KNOWN_INVALID_ENTRY: dict[int, bool] = {} + LAST_KNOWN_PRESSED: dict[int, bool] = {} + LAST_KNOWN_SELECTED: dict[int, bool] = {} + + IGNORE_NAME_CHANGES_FOR: list[int] = [] + + TEXT_EVENT_REASON: dict[Atspi.Event, TextEventReason] = {} + + _lock = threading.Lock() + + @staticmethod + def _clear_stored_data() -> None: + """Clears any data we have cached for objects""" + + while True: + time.sleep(60) + AXUtilitiesEvent._clear_all_dictionaries() + + @staticmethod + def _clear_all_dictionaries(reason: str = "") -> None: + msg = "AXUtilitiesEvent: Clearing local cache." + if reason: + msg += f" Reason: {reason}" + debug.print_message(debug.LEVEL_INFO, msg, True) + AXUtilitiesEvent.LAST_KNOWN_DESCRIPTION.clear() + AXUtilitiesEvent.LAST_KNOWN_NAME.clear() + AXUtilitiesEvent.LAST_KNOWN_CHECKED.clear() + AXUtilitiesEvent.LAST_KNOWN_EXPANDED.clear() + AXUtilitiesEvent.LAST_KNOWN_INDETERMINATE.clear() + AXUtilitiesEvent.LAST_KNOWN_INVALID_ENTRY.clear() + AXUtilitiesEvent.LAST_KNOWN_PRESSED.clear() + AXUtilitiesEvent.LAST_KNOWN_SELECTED.clear() + AXUtilitiesEvent.TEXT_EVENT_REASON.clear() + AXUtilitiesEvent.IGNORE_NAME_CHANGES_FOR.clear() + + @staticmethod + def clear_cache_now(reason: str = "") -> None: + """Clears all cached information immediately.""" + + AXUtilitiesEvent._clear_all_dictionaries(reason) + + @staticmethod + def save_object_info_for_events(obj: Atspi.Accessible) -> None: + """Saves properties, states, etc. of obj for later use in event processing.""" + + if obj is None: + return + + AXUtilitiesEvent.LAST_KNOWN_DESCRIPTION[hash(obj)] = AXObject.get_description(obj) + AXUtilitiesEvent.LAST_KNOWN_NAME[hash(obj)] = AXObject.get_name(obj) + AXUtilitiesEvent.LAST_KNOWN_CHECKED[hash(obj)] = AXUtilitiesState.is_checked(obj) + AXUtilitiesEvent.LAST_KNOWN_EXPANDED[hash(obj)] = AXUtilitiesState.is_expanded(obj) + AXUtilitiesEvent.LAST_KNOWN_INDETERMINATE[hash(obj)] = \ + AXUtilitiesState.is_indeterminate(obj) + AXUtilitiesEvent.LAST_KNOWN_PRESSED[hash(obj)] = AXUtilitiesState.is_pressed(obj) + AXUtilitiesEvent.LAST_KNOWN_SELECTED[hash(obj)] = AXUtilitiesState.is_selected(obj) + + window = focus_manager.get_manager().get_active_window() + AXUtilitiesEvent.LAST_KNOWN_NAME[hash(window)] = AXObject.get_name(window) + AXUtilitiesEvent.LAST_KNOWN_DESCRIPTION[hash(window)] = AXObject.get_description(window) + + @staticmethod + def start_cache_clearing_thread() -> None: + """Starts thread to periodically clear cached details.""" + + thread = threading.Thread(target=AXUtilitiesEvent._clear_stored_data) + thread.daemon = True + thread.start() + + @staticmethod + def get_text_event_reason(event: Atspi.Event) -> TextEventReason: + """Returns the TextEventReason for the given event.""" + + reason = AXUtilitiesEvent.TEXT_EVENT_REASON.get(event) + if reason is not None: + tokens = ["AXUtilitiesEvent: Cached reason for", event, f": {reason}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return reason + + reason = TextEventReason.UNKNOWN + if event.type.startswith("object:text-changed:insert"): + reason = AXUtilitiesEvent._get_text_insertion_event_reason(event) + elif event.type.startswith("object:text-caret-moved"): + reason = AXUtilitiesEvent._get_caret_moved_event_reason(event) + elif event.type.startswith("object:text-changed:delete"): + reason = AXUtilitiesEvent._get_text_deletion_event_reason(event) + elif event.type.startswith("object:text-selection-changed"): + reason = AXUtilitiesEvent._get_text_selection_changed_event_reason(event) + else: + raise ValueError(f"Unexpected event type: {event.type}") + + AXUtilitiesEvent.TEXT_EVENT_REASON[event] = reason + tokens = ["AXUtilitiesEvent: Reason for", event, f": {reason}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return reason + + @staticmethod + def _get_caret_moved_event_reason(event: Atspi.Event) -> TextEventReason: + """Returns the TextEventReason for the given event.""" + + from . import input_event_manager # pylint: disable=import-outside-toplevel + mgr = input_event_manager.get_manager() + + reason = TextEventReason.UNKNOWN + obj = event.source + mode, focus = focus_manager.get_manager().get_active_mode_and_object_of_interest() + if mode == focus_manager.SAY_ALL: + reason = TextEventReason.SAY_ALL + elif focus != obj and AXUtilitiesRole.is_text_input_search(focus): + if mgr.last_event_was_backspace() or mgr.last_event_was_delete(): + reason = TextEventReason.SEARCH_UNPRESENTABLE + else: + reason = TextEventReason.SEARCH_PRESENTABLE + elif mgr.last_event_was_caret_selection(): + if mgr.last_event_was_line_navigation(): + reason = TextEventReason.SELECTION_BY_LINE + elif mgr.last_event_was_word_navigation(): + reason = TextEventReason.SELECTION_BY_WORD + elif mgr.last_event_was_character_navigation(): + reason = TextEventReason.SELECTION_BY_CHARACTER + elif mgr.last_event_was_page_navigation(): + reason = TextEventReason.SELECTION_BY_PAGE + elif mgr.last_event_was_line_boundary_navigation(): + reason = TextEventReason.SELECTION_TO_LINE_BOUNDARY + elif mgr.last_event_was_file_boundary_navigation(): + reason = TextEventReason.SELECTION_TO_FILE_BOUNDARY + else: + reason = TextEventReason.UNSPECIFIED_SELECTION + elif mgr.last_event_was_caret_navigation(): + if mgr.last_event_was_line_navigation(): + reason = TextEventReason.NAVIGATION_BY_LINE + elif mgr.last_event_was_word_navigation(): + reason = TextEventReason.NAVIGATION_BY_WORD + elif mgr.last_event_was_character_navigation(): + reason = TextEventReason.NAVIGATION_BY_CHARACTER + elif mgr.last_event_was_page_navigation(): + reason = TextEventReason.NAVIGATION_BY_PAGE + elif mgr.last_event_was_line_boundary_navigation(): + reason = TextEventReason.NAVIGATION_TO_LINE_BOUNDARY + elif mgr.last_event_was_file_boundary_navigation(): + reason = TextEventReason.NAVIGATION_TO_FILE_BOUNDARY + else: + reason = TextEventReason.UNSPECIFIED_NAVIGATION + elif mgr.last_event_was_select_all(): + reason = TextEventReason.SELECT_ALL + elif mgr.last_event_was_primary_click_or_release(): + reason = TextEventReason.MOUSE_PRIMARY_BUTTON + elif AXUtilitiesState.is_editable(obj) or AXUtilitiesRole.is_terminal(obj): + if mgr.last_event_was_backspace(): + reason = TextEventReason.BACKSPACE + elif mgr.last_event_was_delete(): + reason = TextEventReason.DELETE + elif mgr.last_event_was_cut(): + reason = TextEventReason.CUT + elif mgr.last_event_was_paste(): + reason = TextEventReason.PASTE + elif mgr.last_event_was_undo(): + reason = TextEventReason.UNDO + elif mgr.last_event_was_redo(): + reason = TextEventReason.REDO + elif mgr.last_event_was_page_switch(): + reason = TextEventReason.PAGE_SWITCH + elif mgr.last_event_was_command(): + reason = TextEventReason.UNSPECIFIED_COMMAND + elif mgr.last_event_was_printable_key(): + reason = TextEventReason.TYPING + elif mgr.last_event_was_tab_navigation(): + reason = TextEventReason.FOCUS_CHANGE + elif AXObject.find_ancestor(obj, AXUtilitiesRole.children_are_presentational): + reason = TextEventReason.UI_UPDATE + return reason + + @staticmethod + def _get_text_deletion_event_reason(event: Atspi.Event) -> TextEventReason: + """Returns the TextEventReason for the given event.""" + + from . import input_event_manager # pylint: disable=import-outside-toplevel + mgr = input_event_manager.get_manager() + + reason = TextEventReason.UNKNOWN + obj = event.source + if AXObject.get_role(obj) in AXUtilitiesRole.get_text_ui_roles(): + reason = TextEventReason.UI_UPDATE + elif AXUtilitiesRole.is_live_region(obj): + reason = TextEventReason.LIVE_REGION_UPDATE + elif mgr.last_event_was_page_switch(): + reason = TextEventReason.PAGE_SWITCH + elif AXUtilitiesState.is_editable(obj) or AXUtilitiesRole.is_terminal(obj): + if mgr.last_event_was_backspace(): + reason = TextEventReason.BACKSPACE + elif mgr.last_event_was_delete(): + reason = TextEventReason.DELETE + elif mgr.last_event_was_cut(): + reason = TextEventReason.CUT + elif mgr.last_event_was_paste(): + reason = TextEventReason.PASTE + elif mgr.last_event_was_undo(): + reason = TextEventReason.UNDO + elif mgr.last_event_was_redo(): + reason = TextEventReason.REDO + elif mgr.last_event_was_command(): + reason = TextEventReason.UNSPECIFIED_COMMAND + elif mgr.last_event_was_printable_key(): + reason = TextEventReason.TYPING + elif mgr.last_event_was_up_or_down() or mgr.last_event_was_page_up_or_page_down(): + if AXUtilitiesRole.is_spin_button(obj) \ + or AXObject.find_ancestor(obj, AXUtilitiesRole.is_spin_button): + reason = TextEventReason.SPIN_BUTTON_VALUE_CHANGE + else: + reason = TextEventReason.AUTO_DELETION + if reason == TextEventReason.UNKNOWN: + selected_text, _start, _end = AXText.get_cached_selected_text(obj) + if selected_text and event.any_data.strip() == selected_text.strip(): + reason = TextEventReason.SELECTED_TEXT_DELETION + elif mgr.last_event_was_command(): + reason = TextEventReason.UNSPECIFIED_COMMAND + elif "\ufffc" in event.any_data and not event.any_data.replace("\ufffc", ""): + reason = TextEventReason.CHILDREN_CHANGE + return reason + + @staticmethod + def _get_text_insertion_event_reason(event: Atspi.Event) -> TextEventReason: + """Returns the TextEventReason for the given event.""" + + from . import input_event_manager # pylint: disable=import-outside-toplevel + mgr = input_event_manager.get_manager() + + reason = TextEventReason.UNKNOWN + obj = event.source + if AXObject.get_role(obj) in AXUtilitiesRole.get_text_ui_roles(): + reason = TextEventReason.UI_UPDATE + elif AXUtilitiesRole.is_live_region(obj): + reason = TextEventReason.LIVE_REGION_UPDATE + elif mgr.last_event_was_page_switch(): + reason = TextEventReason.PAGE_SWITCH + elif AXUtilitiesState.is_editable(obj) \ + or AXUtilitiesRole.is_terminal(obj): + selected_text, _start, _end = AXText.get_selected_text(obj) + if selected_text and event.any_data == selected_text: + reason = TextEventReason.SELECTED_TEXT_INSERTION + if mgr.last_event_was_backspace(): + reason = TextEventReason.BACKSPACE + elif mgr.last_event_was_delete(): + reason = TextEventReason.DELETE + elif mgr.last_event_was_cut(): + reason = TextEventReason.CUT + elif mgr.last_event_was_paste(): + reason = TextEventReason.PASTE + elif mgr.last_event_was_undo(): + if reason == TextEventReason.SELECTED_TEXT_INSERTION: + reason = TextEventReason.SELECTED_TEXT_RESTORATION + else: + reason = TextEventReason.UNDO + elif mgr.last_event_was_redo(): + if reason == TextEventReason.SELECTED_TEXT_INSERTION: + reason = TextEventReason.SELECTED_TEXT_RESTORATION + else: + reason = TextEventReason.REDO + elif mgr.last_event_was_command(): + reason = TextEventReason.UNSPECIFIED_COMMAND + elif mgr.last_event_was_space() and not AXUtilitiesRole.is_password_text(obj): + # Gecko inserts a newline at the offset past the space in contenteditables. + if event.any_data == "\n": + reason = TextEventReason.AUTO_INSERTION_UNPRESENTABLE + else: + reason = TextEventReason.TYPING + elif mgr.last_event_was_tab() or mgr.last_event_was_return(): + if not event.any_data.strip(): + reason = TextEventReason.TYPING + elif mgr.last_event_was_printable_key() or mgr.last_event_was_space(): + if reason == TextEventReason.SELECTED_TEXT_INSERTION: + reason = TextEventReason.AUTO_INSERTION_PRESENTABLE + else: + reason = TextEventReason.TYPING + from . import typing_echo_presenter # pylint: disable=import-outside-toplevel + presenter = typing_echo_presenter.get_presenter() + if AXUtilitiesRole.is_password_text(obj): + echo = presenter.get_key_echo_enabled() + else: + echo = presenter.get_character_echo_enabled() + if echo: + reason = TextEventReason.TYPING_ECHOABLE + elif mgr.last_event_was_middle_click() or mgr.last_event_was_middle_release(): + reason = TextEventReason.MOUSE_MIDDLE_BUTTON + elif mgr.last_event_was_up_or_down() or mgr.last_event_was_page_up_or_page_down(): + if AXUtilitiesRole.is_spin_button(obj) \ + or AXObject.find_ancestor(obj, AXUtilitiesRole.is_spin_button): + reason = TextEventReason.SPIN_BUTTON_VALUE_CHANGE + else: + reason = TextEventReason.AUTO_INSERTION_PRESENTABLE + if reason == TextEventReason.UNKNOWN: + if len(event.any_data) == 1: + pass + elif mgr.last_event_was_tab() and event.any_data != "\t": + reason = TextEventReason.AUTO_INSERTION_PRESENTABLE + elif mgr.last_event_was_return() and event.any_data != "\n": + if AXUtilitiesState.is_single_line(event.source): + # Example: The browser's address bar in response to return on a link. + reason = TextEventReason.AUTO_INSERTION_UNPRESENTABLE + else: + reason = TextEventReason.AUTO_INSERTION_PRESENTABLE + elif mgr.last_event_was_command(): + reason = TextEventReason.UNSPECIFIED_COMMAND + elif "\ufffc" in event.any_data and not event.any_data.replace("\ufffc", ""): + reason = TextEventReason.CHILDREN_CHANGE + + return reason + + @staticmethod + def _get_text_selection_changed_event_reason(event: Atspi.Event) -> TextEventReason: + """Returns the TextEventReason for the given event.""" + + from . import input_event_manager # pylint: disable=import-outside-toplevel + mgr = input_event_manager.get_manager() + + reason = TextEventReason.UNKNOWN + obj = event.source + focus = focus_manager.get_manager().get_locus_of_focus() + if focus != obj and AXUtilitiesRole.is_text_input_search(focus): + if mgr.last_event_was_backspace() or mgr.last_event_was_delete(): + reason = TextEventReason.SEARCH_UNPRESENTABLE + else: + reason = TextEventReason.SEARCH_PRESENTABLE + elif mgr.last_event_was_caret_selection(): + if mgr.last_event_was_line_navigation(): + reason = TextEventReason.SELECTION_BY_LINE + elif mgr.last_event_was_word_navigation(): + reason = TextEventReason.SELECTION_BY_WORD + elif mgr.last_event_was_character_navigation(): + reason = TextEventReason.SELECTION_BY_CHARACTER + elif mgr.last_event_was_page_navigation(): + reason = TextEventReason.SELECTION_BY_PAGE + elif mgr.last_event_was_line_boundary_navigation(): + reason = TextEventReason.SELECTION_TO_LINE_BOUNDARY + elif mgr.last_event_was_file_boundary_navigation(): + reason = TextEventReason.SELECTION_TO_FILE_BOUNDARY + else: + reason = TextEventReason.UNSPECIFIED_SELECTION + elif mgr.last_event_was_caret_navigation(): + if mgr.last_event_was_line_navigation(): + reason = TextEventReason.NAVIGATION_BY_LINE + elif mgr.last_event_was_word_navigation(): + reason = TextEventReason.NAVIGATION_BY_WORD + elif mgr.last_event_was_character_navigation(): + reason = TextEventReason.NAVIGATION_BY_CHARACTER + elif mgr.last_event_was_page_navigation(): + reason = TextEventReason.NAVIGATION_BY_PAGE + elif mgr.last_event_was_line_boundary_navigation(): + reason = TextEventReason.NAVIGATION_TO_LINE_BOUNDARY + elif mgr.last_event_was_file_boundary_navigation(): + reason = TextEventReason.NAVIGATION_TO_FILE_BOUNDARY + else: + reason = TextEventReason.UNSPECIFIED_NAVIGATION + elif mgr.last_event_was_select_all(): + reason = TextEventReason.SELECT_ALL + elif mgr.last_event_was_primary_click_or_release(): + reason = TextEventReason.MOUSE_PRIMARY_BUTTON + elif AXUtilitiesState.is_editable(obj) or AXUtilitiesRole.is_terminal(obj): + if mgr.last_event_was_backspace(): + reason = TextEventReason.BACKSPACE + elif mgr.last_event_was_delete(): + reason = TextEventReason.DELETE + elif mgr.last_event_was_cut(): + reason = TextEventReason.CUT + elif mgr.last_event_was_paste(): + reason = TextEventReason.PASTE + elif mgr.last_event_was_undo(): + reason = TextEventReason.UNDO + elif mgr.last_event_was_redo(): + reason = TextEventReason.REDO + elif mgr.last_event_was_page_switch(): + reason = TextEventReason.PAGE_SWITCH + elif mgr.last_event_was_command(): + reason = TextEventReason.UNSPECIFIED_COMMAND + elif mgr.last_event_was_printable_key(): + reason = TextEventReason.TYPING + elif mgr.last_event_was_up_or_down() or mgr.last_event_was_page_up_or_page_down(): + if AXUtilitiesRole.is_spin_button(obj) \ + or AXObject.find_ancestor(obj, AXUtilitiesRole.is_spin_button): + reason = TextEventReason.SPIN_BUTTON_VALUE_CHANGE + return reason + + @staticmethod + def is_presentable_active_descendant_change(event: Atspi.Event) -> bool: + """Returns True if this event should be presented as an active-descendant change.""" + + if not event.any_data: + msg = "AXUtilitiesEvent: No any_data." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + if not (AXUtilitiesState.is_focused(event.source) \ + or AXUtilitiesState.is_focused(event.any_data)): + msg = "AXUtilitiesEvent: Neither source nor child have focused state." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + focus = focus_manager.get_manager().get_locus_of_focus() + if AXUtilitiesRole.is_table_cell(focus): + table = AXObject.find_ancestor(focus, AXUtilitiesRole.is_tree_or_tree_table) + if table is not None and table != event.source: + msg = "AXUtilitiesEvent: Event is from a different tree or tree table." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + msg = "AXUtilitiesEvent: Event is presentable." + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + + @staticmethod + def is_presentable_checked_change(event: Atspi.Event) -> bool: + """Returns True if this event should be presented as a checked-state change.""" + + old_state = AXUtilitiesEvent.LAST_KNOWN_CHECKED.get(hash(event.source)) + new_state = AXUtilitiesState.is_checked(event.source) + if old_state == new_state: + msg = "AXUtilitiesEvent: The new state matches the old state." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + AXUtilitiesEvent.LAST_KNOWN_CHECKED[hash(event.source)] = new_state + focus = focus_manager.get_manager().get_locus_of_focus() + if event.source != focus: + if not AXObject.is_ancestor(event.source, focus): + msg = "AXUtilitiesEvent: The source is not the locus of focus or its descendant." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + if not (AXUtilitiesRole.is_list_item(focus) or AXUtilitiesRole.is_tree_item(focus)): + msg = "AXUtilitiesEvent: The source descends from non-interactive-item focus." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + from . import input_event_manager # pylint: disable=import-outside-toplevel + mgr = input_event_manager.get_manager() + + # Radio buttons normally change their state when you arrow to them, so we handle the + # announcement of their state changes in the focus handling code. + if AXUtilitiesRole.is_radio_button(event.source) and not mgr.last_event_was_space(): + msg = "AXUtilitiesEvent: Only presentable for this role if toggled by user." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + msg = "AXUtilitiesEvent: Event is presentable." + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + + @staticmethod + def is_presentable_description_change(event: Atspi.Event) -> bool: + """Returns True if this event should be presented as a description change.""" + + if not isinstance(event.any_data, str): + msg = "AXUtilitiesEvent: The any_data is not a string." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + old_description = AXUtilitiesEvent.LAST_KNOWN_DESCRIPTION.get(hash(event.source)) + new_description = event.any_data + if old_description == new_description: + msg = "AXUtilitiesEvent: The new description matches the old description." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + AXUtilitiesEvent.LAST_KNOWN_DESCRIPTION[hash(event.source)] = new_description + if not new_description: + msg = "AXUtilitiesEvent: The description is empty." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + if not AXUtilitiesState.is_showing(event.source): + msg = "AXUtilitiesEvent: The event source is not showing." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + focus = focus_manager.get_manager().get_locus_of_focus() + if event.source != focus and not AXObject.is_ancestor(focus, event.source): + msg = "AXUtilitiesEvent: The event is not from the locus of focus or ancestor." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + msg = "AXUtilitiesEvent: Event is presentable." + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + + @staticmethod + def is_presentable_expanded_change(event: Atspi.Event) -> bool: + """Returns True if this event should be presented as an expanded-state change.""" + + old_state = AXUtilitiesEvent.LAST_KNOWN_EXPANDED.get(hash(event.source)) + new_state = AXUtilitiesState.is_expanded(event.source) + if old_state == new_state: + msg = "AXUtilitiesEvent: The new state matches the old state." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + AXUtilitiesEvent.LAST_KNOWN_EXPANDED[hash(event.source)] = new_state + focus = focus_manager.get_manager().get_locus_of_focus() + if event.source == focus: + msg = "AXUtilitiesEvent: Event is presentable, from the locus of focus." + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + + if not event.detail1 and not AXObject.is_ancestor(focus, event.source): + msg = "AXUtilitiesEvent: Event is not from the locus of focus or ancestor." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + if AXUtilitiesRole.is_table_row(event.source) or AXUtilitiesRole.is_list_box(event.source): + msg = "AXUtilitiesEvent: Event is presentable based on role." + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + + if AXUtilitiesRole.is_combo_box(event.source) or AXUtilitiesRole.is_button(event.source): + if not AXUtilitiesState.is_focused(event.source): + msg = "AXUtilitiesEvent: Only presentable for this role if focused." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + msg = "AXUtilitiesEvent: Event is presentable." + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + + @staticmethod + def is_presentable_indeterminate_change(event: Atspi.Event) -> bool: + """Returns True if this event should be presented as an indeterminate-state change.""" + + old_state = AXUtilitiesEvent.LAST_KNOWN_INDETERMINATE.get(hash(event.source)) + new_state = AXUtilitiesState.is_indeterminate(event.source) + if old_state == new_state: + msg = "AXUtilitiesEvent: The new state matches the old state." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + AXUtilitiesEvent.LAST_KNOWN_INDETERMINATE[hash(event.source)] = new_state + + # If this state is cleared, the new state will become checked or unchecked + # and we should get object:state-changed:checked events for those cases. + if not new_state: + msg = "AXUtilitiesEvent: The new state should be presented as a checked-change." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + if event.source != focus_manager.get_manager().get_locus_of_focus(): + msg = "AXUtilitiesEvent: The event is not from the locus of focus." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + msg = "AXUtilitiesEvent: Event is presentable." + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + + @staticmethod + def is_presentable_invalid_entry_change(event: Atspi.Event) -> bool: + """Returns True if this event should be presented as an invalid-entry-state change.""" + + old_state = AXUtilitiesEvent.LAST_KNOWN_INVALID_ENTRY.get(hash(event.source)) + new_state = AXUtilitiesState.is_invalid_entry(event.source) + if old_state == new_state: + msg = "AXUtilitiesEvent: The new state matches the old state." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + AXUtilitiesEvent.LAST_KNOWN_INVALID_ENTRY[hash(event.source)] = new_state + if event.source != focus_manager.get_manager().get_locus_of_focus(): + msg = "AXUtilitiesEvent: The event is not from the locus of focus." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + msg = "AXUtilitiesEvent: Event is presentable." + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + + @staticmethod + def is_presentable_name_change(event: Atspi.Event) -> bool: + """Returns True if this event should be presented as a name change.""" + + if hash(event.source) in AXUtilitiesEvent.IGNORE_NAME_CHANGES_FOR: + msg = "AXUtilitiesEvent: Ignoring name change for this source." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + if not isinstance(event.any_data, str): + msg = "AXUtilitiesEvent: The any_data is not a string." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + old_name = AXUtilitiesEvent.LAST_KNOWN_NAME.get(hash(event.source)) + new_name = event.any_data + if old_name == new_name: + msg = "AXUtilitiesEvent: The new name matches the old name." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + AXUtilitiesEvent.LAST_KNOWN_NAME[hash(event.source)] = new_name + if not new_name: + msg = "AXUtilitiesEvent: The name is empty." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + if not AXUtilitiesState.is_showing(event.source): + msg = "AXUtilitiesEvent: The event source is not showing." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + if AXUtilitiesRole.is_frame(event.source): + if event.source != focus_manager.get_manager().get_active_window(): + msg = "AXUtilitiesEvent: Event is for frame other than the active window." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + # Example: Typing the subject in an email client causing the window name to change. + focus = focus_manager.get_manager().get_locus_of_focus() + if AXUtilitiesState.is_editable(focus) and AXText.get_character_count(focus) \ + and AXText.get_all_text(focus) in event.any_data: + msg = "AXUtilitiesEvent: Event is redundant notification for the locus of focus." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + msg = "AXUtilitiesEvent: Event is presentable." + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + + if event.source != focus_manager.get_manager().get_locus_of_focus(): + msg = "AXUtilitiesEvent: The event is not from the locus of focus." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + if AXUtilitiesRole.is_list_item(event.source): + # Inspired by Firefox's downloads list in which the list item's name changes to show + # download progress. It's super chatty and stomps on the progress bar announcements. + if AXObject.find_descendant(event.source, AXUtilitiesRole.is_progress_bar): + msg = "AXUtilitiesEvent: The list item contains a progress bar." + debug.print_message(debug.LEVEL_INFO, msg, True) + AXUtilitiesEvent.IGNORE_NAME_CHANGES_FOR.append(hash(event.source)) + return False + + msg = "AXUtilitiesEvent: Event is presentable." + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + + @staticmethod + def is_presentable_pressed_change(event: Atspi.Event) -> bool: + """Returns True if this event should be presented as a pressed-state change.""" + + old_state = AXUtilitiesEvent.LAST_KNOWN_PRESSED.get(hash(event.source)) + new_state = AXUtilitiesState.is_pressed(event.source) + if old_state == new_state: + msg = "AXUtilitiesEvent: The new state matches the old state." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + AXUtilitiesEvent.LAST_KNOWN_PRESSED[hash(event.source)] = new_state + if event.source != focus_manager.get_manager().get_locus_of_focus(): + msg = "AXUtilitiesEvent: The event is not from the locus of focus." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + msg = "AXUtilitiesEvent: Event is presentable." + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + + @staticmethod + def is_presentable_selected_change(event: Atspi.Event) -> bool: + """Returns True if this event should be presented as a selected-state change.""" + + old_state = AXUtilitiesEvent.LAST_KNOWN_SELECTED.get(hash(event.source)) + new_state = AXUtilitiesState.is_selected(event.source) + if old_state == new_state: + msg = "AXUtilitiesEvent: The new state matches the old state." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + AXUtilitiesEvent.LAST_KNOWN_SELECTED[hash(event.source)] = new_state + if event.source != focus_manager.get_manager().get_locus_of_focus(): + msg = "AXUtilitiesEvent: The event is not from the locus of focus." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + msg = "AXUtilitiesEvent: Event is presentable." + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + + @staticmethod + def _is_presentable_text_event(event: Atspi.Event) -> bool: + """Returns True if this text event should be presented.""" + + if not (AXUtilitiesState.is_editable(event.source) or \ + AXUtilitiesRole.is_terminal(event.source)): + msg = "AXUtilitiesEvent: The source is neither editable nor a terminal." + debug.print_message(debug.LEVEL_INFO, msg, True) + return False + + focus = focus_manager.get_manager().get_locus_of_focus() + if focus != event.source and not AXUtilitiesState.is_focused(event.source): + msg = "AXUtilitiesEvent: The source is neither focused, nor the locus of focus" + debug.print_message(debug.LEVEL_INFO, msg, True) + + # This can happen in web content where the focus is a contenteditable element and a + # new child element is created for new or changed text. + if AXObject.is_ancestor(event.source, focus): + msg = "AXUtilitiesEvent: The locus of focus is an ancestor of the source." + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + + return False + + msg = "AXUtilitiesEvent: Event is presentable." + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + + @staticmethod + def is_presentable_text_attributes_change(event: Atspi.Event) -> bool: + """Returns True if this text-attributes-change event should be presented.""" + + return AXUtilitiesEvent._is_presentable_text_event(event) + + @staticmethod + def is_presentable_text_deletion(event: Atspi.Event) -> bool: + """Returns True if this text-deletion event should be presented.""" + + return AXUtilitiesEvent._is_presentable_text_event(event) + + @staticmethod + def is_presentable_text_insertion(event: Atspi.Event) -> bool: + """Returns True if this text-insertion event should be presented.""" + + return AXUtilitiesEvent._is_presentable_text_event(event) + + +AXUtilitiesEvent.start_cache_clearing_thread() diff --git a/src/cthulhu/ax_utilities_relation.py b/src/cthulhu/ax_utilities_relation.py new file mode 100644 index 0000000..0947f74 --- /dev/null +++ b/src/cthulhu/ax_utilities_relation.py @@ -0,0 +1,388 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 Stormux +# Copyright 2024 Igalia, S.L. +# Copyright 2024 GNOME Foundation Inc. +# Author: Joanmarie Diggs +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., Franklin Street, Fifth Floor, +# Boston MA 02110-1301 USA. +# +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu + +# pylint: disable=wrong-import-position +# pylint: disable=too-many-public-methods + +"""Utilities for obtaining relation-related information.""" + +__id__ = "$Id$" +__version__ = "$Revision$" +__date__ = "$Date$" +__copyright__ = "Copyright (c) 2024 Igalia, S.L." \ + "Copyright (c) 2024 GNOME Foundation Inc." +__license__ = "LGPL" + +import threading +import time + +import gi +gi.require_version("Atspi", "2.0") +from gi.repository import Atspi +from gi.repository import GLib + +from . import debug +from .ax_object import AXObject + + +class AXUtilitiesRelation: + """Utilities for obtaining relation-related information.""" + + RELATIONS: dict[int, list[Atspi.Relation]] = {} + TARGETS: dict[int, dict[Atspi.RelationType, list[Atspi.Accessible]]] = {} + + _lock = threading.Lock() + + @staticmethod + def _clear_stored_data() -> None: + """Clears any data we have cached for objects""" + + while True: + time.sleep(60) + AXUtilitiesRelation._clear_all_dictionaries() + + @staticmethod + def _clear_all_dictionaries(reason: str = "") -> None: + msg = "AXUtilitiesRelation: Clearing local cache." + if reason: + msg += f" Reason: {reason}" + debug.print_message(debug.LEVEL_INFO, msg, True) + + with AXUtilitiesRelation._lock: + AXUtilitiesRelation.RELATIONS.clear() + AXUtilitiesRelation.TARGETS.clear() + + @staticmethod + def clear_cache_now(reason: str = "") -> None: + """Clears all cached information immediately.""" + + AXUtilitiesRelation._clear_all_dictionaries(reason) + + @staticmethod + def start_cache_clearing_thread() -> None: + """Starts thread to periodically clear cached details.""" + + thread = threading.Thread(target=AXUtilitiesRelation._clear_stored_data) + thread.daemon = True + thread.start() + + @staticmethod + def get_relations(obj: Atspi.Accessible) -> list[Atspi.Relation]: + """Returns the list of Atspi.Relation objects associated with obj""" + + if not AXObject.is_valid(obj): + return [] + + relations = AXUtilitiesRelation.RELATIONS.get(hash(obj)) + if relations is not None: + return relations + + try: + relations = Atspi.Accessible.get_relation_set(obj) + except GLib.GError as error: + msg = f"AXUtilitiesRelation: Exception in get_relations: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return [] + + if relations is None: + tokens = ["AXUtilitiesRelation: get_relation_set failed for", obj] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return [] + + AXUtilitiesRelation.RELATIONS[hash(obj)] = relations + return relations + + @staticmethod + def _get_relation( + obj: Atspi.Accessible, + relation_type: Atspi.RelationType + ) -> Atspi.Relation | None: + """Returns the specified Atspi.Relation for obj""" + + for relation in AXUtilitiesRelation.get_relations(obj): + if relation and relation.get_relation_type() == relation_type: + return relation + + return None + + + @staticmethod + def get_relation_targets_for_debugging( + obj: Atspi.Accessible, relation_type: Atspi.RelationType + ) -> list[Atspi.Accessible]: + """Returns the list of targets with the specified relation type to obj.""" + + return AXUtilitiesRelation._get_relation_targets(obj, relation_type) + + @staticmethod + def _get_relation_targets( + obj: Atspi.Accessible, + relation_type: Atspi.RelationType + ) -> list[Atspi.Accessible]: + """Returns the list of targets with the specified relation type to obj.""" + + cached_targets = AXUtilitiesRelation.TARGETS.get(hash(obj), {}) + cached_relation = cached_targets.get(relation_type) + if isinstance(cached_relation, list): + return cached_relation + + relation = AXUtilitiesRelation._get_relation(obj, relation_type) + if relation is None: + cached_targets[relation_type] = [] + AXUtilitiesRelation.TARGETS[hash(obj)] = cached_targets + return [] + + targets = set() + for i in range(relation.get_n_targets()): + if target := relation.get_target(i): + targets.add(target) + + # We want to avoid self-referential relationships. + type_includes_object = [Atspi.RelationType.MEMBER_OF] + if relation_type not in type_includes_object and obj in targets: + tokens = ["AXUtilitiesRelation: ", obj, "is in its own", relation_type, "target list"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + targets.remove(obj) + + result = list(targets) + cached_targets[relation_type] = result + AXUtilitiesRelation.TARGETS[hash(obj)] = cached_targets + return result + + @staticmethod + def get_is_controlled_by(obj: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns a list of accessible objects that obj is controlled by.""" + + result = AXUtilitiesRelation._get_relation_targets(obj, Atspi.RelationType.CONTROLLED_BY) + tokens = ["AXUtilitiesRelation:", obj, "is controlled by:", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def get_is_controller_for(obj: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns a list of accessible objects that obj is the controller for.""" + + result = AXUtilitiesRelation._get_relation_targets(obj, Atspi.RelationType.CONTROLLER_FOR) + tokens = ["AXUtilitiesRelation:", obj, "is controller for:", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def get_is_described_by(obj: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns a list of accessible objects that obj is described by.""" + + result = AXUtilitiesRelation._get_relation_targets(obj, Atspi.RelationType.DESCRIBED_BY) + tokens = ["AXUtilitiesRelation:", obj, "is described by:", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def get_is_description_for(obj: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns a list of accessible objects that obj is the description for.""" + + result = AXUtilitiesRelation._get_relation_targets(obj, Atspi.RelationType.DESCRIPTION_FOR) + tokens = ["AXUtilitiesRelation:", obj, "is description for:", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def get_details(obj: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns a list of accessible objects that contain details for obj.""" + + result = AXUtilitiesRelation._get_relation_targets(obj, Atspi.RelationType.DETAILS) + tokens = ["AXUtilitiesRelation:", obj, "has details in:", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def get_is_details_for(obj: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns a list of accessible objects that obj contains details for.""" + + result = AXUtilitiesRelation._get_relation_targets(obj, Atspi.RelationType.DETAILS_FOR) + tokens = ["AXUtilitiesRelation:", obj, "contains details for:", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def get_is_embedded_by(obj: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns a list of accessible objects that obj is embedded by.""" + + result = AXUtilitiesRelation._get_relation_targets(obj, Atspi.RelationType.EMBEDDED_BY) + tokens = ["AXUtilitiesRelation:", obj, "is embedded by:", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def get_embeds(obj: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns a list of accessible objects that obj embeds.""" + + result = AXUtilitiesRelation._get_relation_targets(obj, Atspi.RelationType.EMBEDS) + tokens = ["AXUtilitiesRelation:", obj, "embeds:", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def get_is_error_for(obj: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns a list of accessible objects that obj contains an error message for.""" + + result = AXUtilitiesRelation._get_relation_targets(obj, Atspi.RelationType.ERROR_FOR) + tokens = ["AXUtilitiesRelation:", obj, "is error for:", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def get_error_message(obj: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns a list of accessible objects that contain an error message for obj.""" + + result = AXUtilitiesRelation._get_relation_targets(obj, Atspi.RelationType.ERROR_MESSAGE) + tokens = ["AXUtilitiesRelation:", obj, "has error messages in:", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def get_flows_from(obj: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns a list of accessible objects that obj flows from.""" + + result = AXUtilitiesRelation._get_relation_targets(obj, Atspi.RelationType.FLOWS_FROM) + tokens = ["AXUtilitiesRelation:", obj, "flows from:", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def get_flows_to(obj: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns a list of accessible objects that obj flows to.""" + + result = AXUtilitiesRelation._get_relation_targets(obj, Atspi.RelationType.FLOWS_TO) + tokens = ["AXUtilitiesRelation:", obj, "flows to:", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def get_is_label_for(obj: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns a list of accessible objects that obj is the label for.""" + + result = AXUtilitiesRelation._get_relation_targets(obj, Atspi.RelationType.LABEL_FOR) + tokens = ["AXUtilitiesRelation:", obj, "is label for:", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def get_is_labelled_by( + obj: Atspi.Accessible, + exclude_ancestors: bool = True + ) -> list[Atspi.Accessible]: + """Returns a list of accessible objects that obj is labelled by.""" + + def is_not_ancestor(acc): + return not AXObject.is_ancestor(obj, acc) + + result = AXUtilitiesRelation._get_relation_targets(obj, Atspi.RelationType.LABELLED_BY) + if exclude_ancestors: + result = list(filter(is_not_ancestor, result)) + + tokens = ["AXUtilitiesRelation:", obj, "is labelled by:", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def get_is_member_of(obj: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns a list of accessible objects that obj is a member of.""" + + result = AXUtilitiesRelation._get_relation_targets(obj, Atspi.RelationType.MEMBER_OF) + tokens = ["AXUtilitiesRelation:", obj, "is member of:", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def get_is_node_child_of(obj: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns a list of accessible objects that obj is the node child of.""" + + result = AXUtilitiesRelation._get_relation_targets(obj, Atspi.RelationType.NODE_CHILD_OF) + tokens = ["AXUtilitiesRelation:", obj, "is node child of:", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def get_is_node_parent_of(obj: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns a list of accessible objects that obj is the node parent of.""" + + result = AXUtilitiesRelation._get_relation_targets(obj, Atspi.RelationType.NODE_PARENT_OF) + tokens = ["AXUtilitiesRelation:", obj, "is node parent of:", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def get_is_parent_window_of(obj: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns a list of accessible objects that obj is a parent window of.""" + + result = AXUtilitiesRelation._get_relation_targets(obj, Atspi.RelationType.PARENT_WINDOW_OF) + tokens = ["AXUtilitiesRelation:", obj, "is parent window of:", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def get_is_popup_for(obj: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns a list of accessible objects that obj is the popup for.""" + + result = AXUtilitiesRelation._get_relation_targets(obj, Atspi.RelationType.POPUP_FOR) + tokens = ["AXUtilitiesRelation:", obj, "is popup for:", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def get_is_subwindow_of(obj: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns a list of accessible objects that obj is a subwindow of.""" + + result = AXUtilitiesRelation._get_relation_targets(obj, Atspi.RelationType.SUBWINDOW_OF) + tokens = ["AXUtilitiesRelation:", obj, "is subwindow of:", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def get_is_tooltip_for(obj: Atspi.Accessible) -> list[Atspi.Accessible]: + """Returns a list of accessible objects that obj is the tooltip for.""" + + result = AXUtilitiesRelation._get_relation_targets(obj, Atspi.RelationType.TOOLTIP_FOR) + tokens = ["AXUtilitiesRelation:", obj, "is tooltip for:", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def object_is_controlled_by(obj1: Atspi.Accessible, obj2: Atspi.Accessible) -> bool: + """Returns True if obj1 is controlled by obj2.""" + + targets = AXUtilitiesRelation._get_relation_targets(obj1, Atspi.RelationType.CONTROLLED_BY) + result = obj2 in targets + tokens = ["AXUtilitiesRelation:", obj1, "is controlled by", obj2, f": {result}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def object_is_unrelated(obj: Atspi.Accessible) -> bool: + """Returns True if obj does not have any relations.""" + + return not AXUtilitiesRelation.get_relations(obj) + +AXUtilitiesRelation.start_cache_clearing_thread() diff --git a/src/cthulhu/ax_utilities_role.py b/src/cthulhu/ax_utilities_role.py index fdb7f6a..e4901d3 100644 --- a/src/cthulhu/ax_utilities_role.py +++ b/src/cthulhu/ax_utilities_role.py @@ -1,9 +1,7 @@ -#!/usr/bin/env python3 +# Utilities for obtaining role-related information. # -# Copyright (c) 2024 Stormux -# Copyright (c) 2010-2012 The Orca Team -# Copyright (c) 2012 Igalia, S.L. -# Copyright (c) 2005-2010 Sun Microsystems Inc. +# Copyright 2023 Igalia, S.L. +# Author: Joanmarie Diggs # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -19,19 +17,15 @@ # License along with this library; if not, write to the # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. -# -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca -""" -Utilities for obtaining role-related information. -These utilities are app-type- and toolkit-agnostic. Utilities that might have -different implementations or results depending on the type of app (e.g. terminal, -chat, web) or toolkit (e.g. Qt, Gtk) should be in script_utilities.py file(s). +# pylint: disable=wrong-import-position +# pylint: disable=too-many-public-methods +# pylint: disable=too-many-lines +# pylint: disable=too-many-branches +# pylint: disable=too-many-return-statements +# pylint: disable=too-many-statements -N.B. There are currently utilities that should never have custom implementations -that live in script_utilities.py files. These will be moved over time. -""" +"""Utilities for obtaining role-related information.""" __id__ = "$Id$" __version__ = "$Revision$" @@ -43,13 +37,68 @@ import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi +from . import debug +from . import object_properties from .ax_object import AXObject +from .ax_utilities_state import AXUtilitiesState class AXUtilitiesRole: """Utilities for obtaining role-related information.""" @staticmethod - def get_dialog_roles(include_alert_as_dialog=True): + def _get_display_style(obj: Atspi.Accessible) -> str: + attrs = AXObject.get_attributes_dict(obj) + return attrs.get("display", "") + + @staticmethod + def _get_tag(obj: Atspi.Accessible) -> str | None: + attrs = AXObject.get_attributes_dict(obj) + return attrs.get("tag") + + @staticmethod + def _get_xml_roles(obj: Atspi.Accessible) -> list[str]: + attrs = AXObject.get_attributes_dict(obj) + return attrs.get("xml-roles", "").split() + + @staticmethod + def children_are_presentational( + obj: Atspi.Accessible, + role: Atspi.Role | None = None + ) -> bool: + """Returns True if the descendants of obj should be ignored. See ARIA spec.""" + + # Note: We are deliberately leaving out listbox options because they can be complex, + # both in ARIA and in GTK. + + roles = [ + Atspi.Role.BUTTON, + Atspi.Role.CHECK_BOX, + Atspi.Role.CHECK_MENU_ITEM, + Atspi.Role.IMAGE, + Atspi.Role.LEVEL_BAR, + Atspi.Role.PAGE_TAB, + Atspi.Role.PROGRESS_BAR, + Atspi.Role.RADIO_BUTTON, + Atspi.Role.RADIO_MENU_ITEM, + Atspi.Role.SCROLL_BAR, + Atspi.Role.SEPARATOR, + Atspi.Role.SLIDER, + Atspi.Role.SWITCH, + Atspi.Role.TOGGLE_BUTTON, + ] + + if role is None: + role = AXObject.get_role(obj) + + if role in roles: + tokens = ["AXUtilitiesRole:", obj, "has presentational children."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return True + + return False + + @staticmethod + def get_dialog_roles(include_alert_as_dialog: bool = True) -> list[Atspi.Role]: """Returns the list of roles we consider documents""" roles = [Atspi.Role.COLOR_CHOOSER, @@ -60,7 +109,7 @@ class AXUtilitiesRole: return roles @staticmethod - def get_document_roles(): + def get_document_roles() -> list[Atspi.Role]: """Returns the list of roles we consider documents""" roles = [Atspi.Role.DOCUMENT_EMAIL, @@ -72,10 +121,11 @@ class AXUtilitiesRole: return roles @staticmethod - def get_form_field_roles(): + def get_form_field_roles() -> list[Atspi.Role]: """Returns the list of roles we consider form fields""" - roles = [Atspi.Role.CHECK_BOX, + roles = [Atspi.Role.BUTTON, + Atspi.Role.CHECK_BOX, Atspi.Role.RADIO_BUTTON, Atspi.Role.COMBO_BOX, Atspi.Role.DOCUMENT_FRAME, # rich text editing pred recommended @@ -83,12 +133,50 @@ class AXUtilitiesRole: Atspi.Role.LIST_BOX, Atspi.Role.ENTRY, Atspi.Role.PASSWORD_TEXT, - Atspi.Role.PUSH_BUTTON, - Atspi.Role.SPIN_BUTTON] + Atspi.Role.SPIN_BUTTON, + Atspi.Role.TOGGLE_BUTTON] return roles @staticmethod - def get_menu_item_roles(): + def get_large_container_roles() -> list[Atspi.Role]: + """Returns the list of roles we consider a large container.""" + + # Note: We are deliberately leaving out sections because those are often DIVs + # which are generic and often not large. The primary consumer of this function + # is structural navigation which uses it for the jump-to-edge functionality. + roles = [Atspi.Role.ARTICLE, + Atspi.Role.BLOCK_QUOTE, + Atspi.Role.DESCRIPTION_LIST, + Atspi.Role.FORM, + Atspi.Role.FOOTER, + Atspi.Role.GROUPING, + Atspi.Role.HEADER, + Atspi.Role.HTML_CONTAINER, + Atspi.Role.LANDMARK, + Atspi.Role.LOG, + Atspi.Role.LIST, + Atspi.Role.MARQUEE, + Atspi.Role.PANEL, + Atspi.Role.TABLE, + Atspi.Role.TREE, + Atspi.Role.TREE_TABLE] + + return roles + + @staticmethod + def get_layout_only_roles() -> list[Atspi.Role]: + """Returns the list of roles we consider are for layout only""" + + roles = [Atspi.Role.AUTOCOMPLETE, + Atspi.Role.FILLER, + Atspi.Role.REDUNDANT_OBJECT, + Atspi.Role.UNKNOWN, + Atspi.Role.SCROLL_PANE, + Atspi.Role.TEAROFF_MENU_ITEM] + return roles + + @staticmethod + def get_menu_item_roles() -> list[Atspi.Role]: """Returns the list of roles we consider menu items""" roles = [Atspi.Role.MENU_ITEM, @@ -98,7 +186,7 @@ class AXUtilitiesRole: return roles @staticmethod - def get_menu_related_roles(): + def get_menu_related_roles() -> list[Atspi.Role]: """Returns the list of roles we consider menu related""" roles = [Atspi.Role.MENU, @@ -111,7 +199,7 @@ class AXUtilitiesRole: return roles @staticmethod - def get_roles_to_exclude_from_clickables_list(): + def get_roles_to_exclude_from_clickables_list() -> list[Atspi.Role]: """Returns the list of roles we want to exclude from the list of clickables""" roles = [Atspi.Role.COMBO_BOX, @@ -134,7 +222,7 @@ class AXUtilitiesRole: return roles @staticmethod - def get_set_container_roles(): + def get_set_container_roles() -> list[Atspi.Role]: """Returns the list of roles we consider a set container""" roles = [Atspi.Role.LIST, @@ -146,7 +234,7 @@ class AXUtilitiesRole: return roles @staticmethod - def get_table_cell_roles(include_headers=True): + def get_table_cell_roles(include_headers: bool = True) -> list[Atspi.Role]: """Returns the list of roles we consider table cells""" roles = [Atspi.Role.TABLE_CELL] @@ -158,7 +246,7 @@ class AXUtilitiesRole: return roles @staticmethod - def get_table_header_roles(): + def get_table_header_roles() -> list[Atspi.Role]: """Returns the list of roles we consider table headers""" roles = [Atspi.Role.TABLE_COLUMN_HEADER, @@ -168,7 +256,7 @@ class AXUtilitiesRole: return roles @staticmethod - def get_table_related_roles(include_caption=False): + def get_table_related_roles(include_caption: bool = False) -> list[Atspi.Role]: """Returns the list of roles we consider table related""" roles = [Atspi.Role.TABLE, @@ -182,7 +270,17 @@ class AXUtilitiesRole: return roles @staticmethod - def get_tree_related_roles(): + def get_text_ui_roles() -> list[Atspi.Role]: + """Returns the list of roles we consider UI that displays static text""" + + roles = [Atspi.Role.INFO_BAR, + Atspi.Role.LABEL, + Atspi.Role.PAGE_TAB, + Atspi.Role.STATUS_BAR] + return roles + + @staticmethod + def get_tree_related_roles() -> list[Atspi.Role]: """Returns the list of roles we consider tree related""" roles = [Atspi.Role.TREE, @@ -191,26 +289,173 @@ class AXUtilitiesRole: return roles @staticmethod - def get_widget_roles(): + def get_widget_roles() -> list[Atspi.Role]: """Returns the list of roles we consider widgets""" - roles = [Atspi.Role.CHECK_BOX, + roles = [Atspi.Role.BUTTON, + Atspi.Role.CHECK_BOX, Atspi.Role.COMBO_BOX, - Atspi.Role.PUSH_BUTTON, + Atspi.Role.ENTRY, + Atspi.Role.LIST_BOX, + Atspi.Role.PASSWORD_TEXT, Atspi.Role.RADIO_BUTTON, Atspi.Role.SLIDER, + Atspi.Role.SPIN_BUTTON, + Atspi.Role.SWITCH, Atspi.Role.TEXT, # predicate recommended to check it is editable Atspi.Role.TOGGLE_BUTTON] return roles @staticmethod - def have_same_role(obj1, obj2): + def get_localized_role_name(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> str: + """Returns a string representing the localized role name of obj.""" + + if role is None: + role = AXObject.get_role(obj) + + if AXObject.supports_value(obj): + if AXUtilitiesRole.is_horizontal_slider(obj, role): + return object_properties.ROLE_SLIDER_HORIZONTAL + if AXUtilitiesRole.is_vertical_slider(obj, role): + return object_properties.ROLE_SLIDER_VERTICAL + if AXUtilitiesRole.is_horizontal_scrollbar(obj, role): + return object_properties.ROLE_SCROLL_BAR_HORIZONTAL + if AXUtilitiesRole.is_vertical_scrollbar(obj, role): + return object_properties.ROLE_SCROLL_BAR_VERTICAL + if AXUtilitiesRole.is_horizontal_separator(obj, role): + return object_properties.ROLE_SPLITTER_HORIZONTAL + if AXUtilitiesRole.is_vertical_separator(obj, role): + return object_properties.ROLE_SPLITTER_VERTICAL + if AXUtilitiesRole.is_split_pane(obj, role): + # The splitter has the opposite orientation of the split pane. + if AXObject.has_state(obj, Atspi.StateType.HORIZONTAL): + return object_properties.ROLE_SPLITTER_VERTICAL + if AXObject.has_state(obj, Atspi.StateType.VERTICAL): + return object_properties.ROLE_SPLITTER_HORIZONTAL + + if AXUtilitiesRole.is_suggestion(obj, role): + return object_properties.ROLE_CONTENT_SUGGESTION + + if AXUtilitiesRole.is_feed(obj, role): + return object_properties.ROLE_FEED + + if AXUtilitiesRole.is_figure(obj, role): + return object_properties.ROLE_FIGURE + + if AXUtilitiesRole.is_switch(obj, role): + return object_properties.ROLE_SWITCH + + if AXUtilitiesRole.is_dpub(obj): + if AXUtilitiesRole.is_landmark(obj, role): + if AXUtilitiesRole.is_dpub_acknowledgments(obj, role): + return object_properties.ROLE_ACKNOWLEDGMENTS + if AXUtilitiesRole.is_dpub_afterword(obj, role): + return object_properties.ROLE_AFTERWORD + if AXUtilitiesRole.is_dpub_appendix(obj, role): + return object_properties.ROLE_APPENDIX + if AXUtilitiesRole.is_dpub_bibliography(obj, role): + return object_properties.ROLE_BIBLIOGRAPHY + if AXUtilitiesRole.is_dpub_chapter(obj, role): + return object_properties.ROLE_CHAPTER + if AXUtilitiesRole.is_dpub_conclusion(obj, role): + return object_properties.ROLE_CONCLUSION + if AXUtilitiesRole.is_dpub_credits(obj, role): + return object_properties.ROLE_CREDITS + if AXUtilitiesRole.is_dpub_endnotes(obj, role): + return object_properties.ROLE_ENDNOTES + if AXUtilitiesRole.is_dpub_epilogue(obj, role): + return object_properties.ROLE_EPILOGUE + if AXUtilitiesRole.is_dpub_errata(obj, role): + return object_properties.ROLE_ERRATA + if AXUtilitiesRole.is_dpub_foreword(obj, role): + return object_properties.ROLE_FOREWORD + if AXUtilitiesRole.is_dpub_glossary(obj, role): + return object_properties.ROLE_GLOSSARY + if AXUtilitiesRole.is_dpub_index(obj, role): + return object_properties.ROLE_INDEX + if AXUtilitiesRole.is_dpub_introduction(obj, role): + return object_properties.ROLE_INTRODUCTION + if AXUtilitiesRole.is_dpub_pagelist(obj, role): + return object_properties.ROLE_PAGELIST + if AXUtilitiesRole.is_dpub_part(obj, role): + return object_properties.ROLE_PART + if AXUtilitiesRole.is_dpub_preface(obj, role): + return object_properties.ROLE_PREFACE + if AXUtilitiesRole.is_dpub_prologue(obj, role): + return object_properties.ROLE_PROLOGUE + if AXUtilitiesRole.is_dpub_toc(obj, role): + return object_properties.ROLE_TOC + elif role == "ROLE_DPUB_SECTION": + if AXUtilitiesRole.is_dpub_abstract(obj, role): + return object_properties.ROLE_ABSTRACT + if AXUtilitiesRole.is_dpub_colophon(obj, role): + return object_properties.ROLE_COLOPHON + if AXUtilitiesRole.is_dpub_credit(obj, role): + return object_properties.ROLE_CREDIT + if AXUtilitiesRole.is_dpub_dedication(obj, role): + return object_properties.ROLE_DEDICATION + if AXUtilitiesRole.is_dpub_epigraph(obj, role): + return object_properties.ROLE_EPIGRAPH + if AXUtilitiesRole.is_dpub_example(obj, role): + return object_properties.ROLE_EXAMPLE + if AXUtilitiesRole.is_dpub_pullquote(obj, role): + return object_properties.ROLE_PULLQUOTE + if AXUtilitiesRole.is_dpub_qna(obj, role): + return object_properties.ROLE_QNA + elif AXUtilitiesRole.is_list_item(obj, role): + if AXUtilitiesRole.is_dpub_biblioref(obj, role): + return object_properties.ROLE_BIBLIOENTRY + if AXUtilitiesRole.is_dpub_endnote(obj, role): + return object_properties.ROLE_ENDNOTE + else: + if AXUtilitiesRole.is_dpub_cover(obj, role): + return object_properties.ROLE_COVER + if AXUtilitiesRole.is_dpub_pagebreak(obj, role): + return object_properties.ROLE_PAGEBREAK + if AXUtilitiesRole.is_dpub_subtitle(obj, role): + return object_properties.ROLE_SUBTITLE + + if AXUtilitiesRole.is_landmark(obj, role): + if AXUtilitiesRole.is_landmark_without_type(obj, role): + return "" + if AXUtilitiesRole.is_landmark_banner(obj, role): + return object_properties.ROLE_LANDMARK_BANNER + if AXUtilitiesRole.is_landmark_complementary(obj, role): + return object_properties.ROLE_LANDMARK_COMPLEMENTARY + if AXUtilitiesRole.is_landmark_contentinfo(obj, role): + return object_properties.ROLE_LANDMARK_CONTENTINFO + if AXUtilitiesRole.is_landmark_main(obj, role): + return object_properties.ROLE_LANDMARK_MAIN + if AXUtilitiesRole.is_landmark_navigation(obj, role): + return object_properties.ROLE_LANDMARK_NAVIGATION + if AXUtilitiesRole.is_landmark_region(obj, role): + return object_properties.ROLE_LANDMARK_REGION + if AXUtilitiesRole.is_landmark_search(obj, role): + return object_properties.ROLE_LANDMARK_SEARCH + if AXUtilitiesRole.is_landmark_form(obj, role): + role = Atspi.Role.FORM + elif AXUtilitiesRole.is_comment(obj, role): + role = Atspi.Role.COMMENT + + if not isinstance(role, Atspi.Role): + return AXObject.get_role_name(obj, True) + + return Atspi.role_get_localized_name(role) + + @staticmethod + def has_role_from_aria(obj: Atspi.Accessible) -> bool: + """Returns True if obj's role comes from ARIA""" + + return bool(AXUtilitiesRole._get_xml_roles(obj)) + + @staticmethod + def have_same_role(obj1: Atspi.Accessible, obj2: Atspi.Accessible) -> bool: """Returns True if obj1 and obj2 have the same role""" return AXObject.get_role(obj1) == AXObject.get_role(obj2) @staticmethod - def is_accelerator_label(obj, role=None): + def is_accelerator_label(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the accelerator label role""" if role is None: @@ -218,15 +463,31 @@ class AXUtilitiesRole: return role == Atspi.Role.ACCELERATOR_LABEL @staticmethod - def is_alert(obj, role=None): - """Returns True if obj has the alert role""" + def is_alert(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the alert (a type of dialog) role""" if role is None: role = AXObject.get_role(obj) return role == Atspi.Role.ALERT @staticmethod - def is_animation(obj, role=None): + def is_aria_alert(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: + """Returns True if obj is an ARIA alert (should have notification role)""" + + if "alert" not in AXUtilitiesRole._get_xml_roles(obj): + return False + + if role is None: + role = AXObject.get_role(obj) + + if role != Atspi.Role.NOTIFICATION: + tokens = ["AXUtilitiesRole: Unexpected role for ARIA alert", obj] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + return True + + @staticmethod + def is_animation(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the animation role""" if role is None: @@ -234,7 +495,7 @@ class AXUtilitiesRole: return role == Atspi.Role.ANIMATION @staticmethod - def is_application(obj, role=None): + def is_application(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the application role""" if role is None: @@ -242,7 +503,7 @@ class AXUtilitiesRole: return role == Atspi.Role.APPLICATION @staticmethod - def is_arrow(obj, role=None): + def is_arrow(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the arrow role""" if role is None: @@ -250,7 +511,7 @@ class AXUtilitiesRole: return role == Atspi.Role.ARROW @staticmethod - def is_article(obj, role=None): + def is_article(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the article role""" if role is None: @@ -258,7 +519,7 @@ class AXUtilitiesRole: return role == Atspi.Role.ARTICLE @staticmethod - def is_audio(obj, role=None): + def is_audio(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the audio role""" if role is None: @@ -266,7 +527,7 @@ class AXUtilitiesRole: return role == Atspi.Role.AUDIO @staticmethod - def is_autocomplete(obj, role=None): + def is_autocomplete(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the autocomplete role""" if role is None: @@ -274,23 +535,31 @@ class AXUtilitiesRole: return role == Atspi.Role.AUTOCOMPLETE @staticmethod - def is_block_quote(obj, role=None): + def is_block_quote(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the block quote role""" if role is None: role = AXObject.get_role(obj) - return role == Atspi.Role.BLOCK_QUOTE + return role == Atspi.Role.BLOCK_QUOTE or AXUtilitiesRole._get_tag(obj) == "blockquote" @staticmethod - def is_button(obj, role=None): + def is_button(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the push- or toggle-button role""" if role is None: role = AXObject.get_role(obj) - return role in [Atspi.Role.PUSH_BUTTON, Atspi.Role.TOGGLE_BUTTON] + return role in [Atspi.Role.BUTTON, Atspi.Role.TOGGLE_BUTTON] @staticmethod - def is_calendar(obj, role=None): + def is_button_with_popup(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the push- or toggle-button role and a popup""" + + if not AXUtilitiesRole.is_button(obj, role): + return False + return AXUtilitiesState.has_popup(obj) + + @staticmethod + def is_calendar(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the calendar role""" if role is None: @@ -298,7 +567,7 @@ class AXUtilitiesRole: return role == Atspi.Role.CALENDAR @staticmethod - def is_canvas(obj, role=None): + def is_canvas(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the canvas role""" if role is None: @@ -306,7 +575,7 @@ class AXUtilitiesRole: return role == Atspi.Role.CANVAS @staticmethod - def is_caption(obj, role=None): + def is_caption(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the caption role""" if role is None: @@ -314,7 +583,7 @@ class AXUtilitiesRole: return role == Atspi.Role.CAPTION @staticmethod - def is_chart(obj, role=None): + def is_chart(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the chart role""" if role is None: @@ -322,7 +591,7 @@ class AXUtilitiesRole: return role == Atspi.Role.CHART @staticmethod - def is_check_box(obj, role=None): + def is_check_box(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the checkbox role""" if role is None: @@ -330,7 +599,7 @@ class AXUtilitiesRole: return role == Atspi.Role.CHECK_BOX @staticmethod - def is_check_menu_item(obj, role=None): + def is_check_menu_item(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the check menuitem role""" if role is None: @@ -338,7 +607,14 @@ class AXUtilitiesRole: return role == Atspi.Role.CHECK_MENU_ITEM @staticmethod - def is_color_chooser(obj, role=None): + def is_code(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the code or code-like role""" + + return "code" in AXUtilitiesRole._get_xml_roles(obj) \ + or AXUtilitiesRole._get_tag(obj) in ["code", "pre"] + + @staticmethod + def is_color_chooser(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the color_chooser role""" if role is None: @@ -346,7 +622,7 @@ class AXUtilitiesRole: return role == Atspi.Role.COLOR_CHOOSER @staticmethod - def is_column_header(obj, role=None): + def is_column_header(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the column header role""" if role is None: @@ -354,7 +630,7 @@ class AXUtilitiesRole: return role == Atspi.Role.COLUMN_HEADER @staticmethod - def is_combo_box(obj, role=None): + def is_combo_box(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the combobox role""" if role is None: @@ -362,31 +638,35 @@ class AXUtilitiesRole: return role == Atspi.Role.COMBO_BOX @staticmethod - def is_comment(obj, role=None): + def is_comment(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the comment role""" if role is None: role = AXObject.get_role(obj) - return role == Atspi.Role.COMMENT + return role == Atspi.Role.COMMENT or "comment" in AXUtilitiesRole._get_xml_roles(obj) @staticmethod - def is_content_deletion(obj, role=None): + def is_content_deletion(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the content deletion role""" if role is None: role = AXObject.get_role(obj) - return role == Atspi.Role.CONTENT_DELETION + return role == Atspi.Role.CONTENT_DELETION \ + or "deletion" in AXUtilitiesRole._get_xml_roles(obj) \ + or "del" == AXUtilitiesRole._get_tag(obj) @staticmethod - def is_content_insertion(obj, role=None): + def is_content_insertion(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the content insertion role""" if role is None: role = AXObject.get_role(obj) - return role == Atspi.Role.CONTENT_INSERTION + return role == Atspi.Role.CONTENT_INSERTION \ + or "insertion" in AXUtilitiesRole._get_xml_roles(obj) \ + or "ins" == AXUtilitiesRole._get_tag(obj) @staticmethod - def is_date_editor(obj, role=None): + def is_date_editor(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the date editor role""" if role is None: @@ -394,14 +674,14 @@ class AXUtilitiesRole: return role == Atspi.Role.DATE_EDITOR @staticmethod - def is_default_button(obj, role=None): + def is_default_button(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the push button role the is-default state""" return AXUtilitiesRole.is_push_button(obj, role) \ and AXObject.has_state(obj, Atspi.StateType.IS_DEFAULT) @staticmethod - def is_definition(obj, role=None): + def is_definition(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the definition role""" if role is None: @@ -409,39 +689,47 @@ class AXUtilitiesRole: return role == Atspi.Role.DEFINITION @staticmethod - def is_description_list(obj, role=None): + def is_description_list(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the description list role""" if role is None: role = AXObject.get_role(obj) - return role == Atspi.Role.DESCRIPTION_LIST + return role == Atspi.Role.DESCRIPTION_LIST \ + or "dl" == AXUtilitiesRole._get_tag(obj) @staticmethod - def is_description_term(obj, role=None): + def is_description_term(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the description term role""" if role is None: role = AXObject.get_role(obj) - return role == Atspi.Role.DESCRIPTION_TERM + return role == Atspi.Role.DESCRIPTION_TERM \ + or "dt" == AXUtilitiesRole._get_tag(obj) @staticmethod - def is_description_value(obj, role=None): + def is_description_value(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the description value role""" if role is None: role = AXObject.get_role(obj) - return role == Atspi.Role.DESCRIPTION_VALUE + return role == Atspi.Role.DESCRIPTION_VALUE \ + or "dd" == AXUtilitiesRole._get_tag(obj) @staticmethod - def is_desktop_frame(obj, role=None): + def is_desktop_frame(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the desktop frame role""" if role is None: role = AXObject.get_role(obj) - return role == Atspi.Role.DESKTOP_FRAME + if role == Atspi.Role.DESKTOP_FRAME: + return True + if role == Atspi.Role.FRAME: + attrs = AXObject.get_attributes_dict(obj) + return attrs.get("is-desktop") == "true" + return False @staticmethod - def is_desktop_icon(obj, role=None): + def is_desktop_icon(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the desktop icon role""" if role is None: @@ -449,7 +737,7 @@ class AXUtilitiesRole: return role == Atspi.Role.DESKTOP_ICON @staticmethod - def is_dial(obj, role=None): + def is_dial(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the dial role""" if role is None: @@ -457,7 +745,7 @@ class AXUtilitiesRole: return role == Atspi.Role.DIAL @staticmethod - def is_dialog(obj, role=None): + def is_dialog(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the dialog role""" if role is None: @@ -465,7 +753,7 @@ class AXUtilitiesRole: return role == Atspi.Role.DIALOG @staticmethod - def is_dialog_or_alert(obj, role=None): + def is_dialog_or_alert(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has any dialog or alert role""" roles = AXUtilitiesRole.get_dialog_roles(True) @@ -474,7 +762,17 @@ class AXUtilitiesRole: return role in roles @staticmethod - def is_directory_pane(obj, role=None): + def is_dialog_or_window(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: + """Returns True if obj has any dialog or window-related role""" + + roles = AXUtilitiesRole.get_dialog_roles(False) + roles.extend((Atspi.Role.FRAME, Atspi.Role.WINDOW)) + if role is None: + role = AXObject.get_role(obj) + return role in roles + + @staticmethod + def is_directory_pane(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the directory pane role""" if role is None: @@ -482,7 +780,17 @@ class AXUtilitiesRole: return role == Atspi.Role.DIRECTORY_PANE @staticmethod - def is_document(obj, role=None): + def is_docked_frame(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the frame role and is docked.""" + + if not AXUtilitiesRole.is_frame(obj, role): + return False + + attrs = AXObject.get_attributes_dict(obj) + return attrs.get("window-type") == "dock" + + @staticmethod + def is_document(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has any document-related role""" roles = AXUtilitiesRole.get_document_roles() @@ -491,7 +799,7 @@ class AXUtilitiesRole: return role in roles @staticmethod - def is_document_email(obj, role=None): + def is_document_email(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the document email role""" if role is None: @@ -499,7 +807,7 @@ class AXUtilitiesRole: return role == Atspi.Role.DOCUMENT_EMAIL @staticmethod - def is_document_frame(obj, role=None): + def is_document_frame(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the document frame role""" if role is None: @@ -507,7 +815,7 @@ class AXUtilitiesRole: return role == Atspi.Role.DOCUMENT_FRAME @staticmethod - def is_document_presentation(obj, role=None): + def is_document_presentation(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the document presentation role""" if role is None: @@ -515,7 +823,7 @@ class AXUtilitiesRole: return role == Atspi.Role.DOCUMENT_PRESENTATION @staticmethod - def is_document_spreadsheet(obj, role=None): + def is_document_spreadsheet(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the document spreadsheet role""" if role is None: @@ -523,7 +831,7 @@ class AXUtilitiesRole: return role == Atspi.Role.DOCUMENT_SPREADSHEET @staticmethod - def is_document_text(obj, role=None): + def is_document_text(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the document text role""" if role is None: @@ -531,7 +839,7 @@ class AXUtilitiesRole: return role == Atspi.Role.DOCUMENT_TEXT @staticmethod - def is_document_web(obj, role=None): + def is_document_web(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the document web role""" if role is None: @@ -539,7 +847,231 @@ class AXUtilitiesRole: return role == Atspi.Role.DOCUMENT_WEB @staticmethod - def is_drawing_area(obj, role=None): + def is_dpub(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has a DPub role.""" + + roles = AXUtilitiesRole._get_xml_roles(obj) + rv = bool(list(filter(lambda x: x.startswith("doc-"), roles))) + return rv + + @staticmethod + def is_dpub_abstract(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub abstract role.""" + + return "doc-abstract" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_acknowledgments(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub acknowledgments role.""" + + return "doc-acknowledgments" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_afterword(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub afterword role.""" + + return "doc-afterword" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_appendix(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub appendix role.""" + + return "doc-appendix" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_backlink(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub backlink role.""" + + return "doc-backlink" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_biblioref(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub biblioref role.""" + + return "doc-biblioref" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_bibliography(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub bibliography role.""" + + return "doc-bibliography" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_chapter(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub chapter role.""" + + return "doc-chapter" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_colophon(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub colophon role.""" + + return "doc-colophon" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_conclusion(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub conclusion role.""" + + return "doc-conclusion" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_cover(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub cover role.""" + + return "doc-cover" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_credit(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub credit role.""" + + return "doc-credit" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_credits(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub credits role.""" + + return "doc-credits" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_dedication(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub dedication role.""" + + return "doc-dedication" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_endnote(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub endnote role.""" + + return "doc-endnote" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_endnotes(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub endnotes role.""" + + return "doc-endnotes" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_epigraph(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub epigraph role.""" + + return "doc-epigraph" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_epilogue(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub epilogue role.""" + + return "doc-epilogue" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_errata(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub errata role.""" + + return "doc-errata" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_example(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub example role.""" + + return "doc-example" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_footnote(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub footnote role.""" + + return "doc-footnote" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_foreword(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub foreword role.""" + + return "doc-foreword" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_glossary(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub glossary role.""" + + return "doc-glossary" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_glossref(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub glossref role.""" + + return "doc-glossref" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_index(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub index role.""" + + return "doc-index" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_introduction(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub introduction role.""" + + return "doc-introduction" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_noteref(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub noteref role.""" + + return "doc-noteref" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_pagelist(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub pagelist role.""" + + return "doc-pagelist" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_pagebreak(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub pagebreak role.""" + + return "doc-pagebreak" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_part(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub part role.""" + + return "doc-part" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_preface(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub preface role.""" + + return "doc-preface" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_prologue(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub prologue role.""" + + return "doc-prologue" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_pullquote(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub pullquote role.""" + + return "doc-pullquote" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_qna(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub qna role.""" + + return "doc-qna" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_subtitle(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub subtitle role.""" + + return "doc-subtitle" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_dpub_toc(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the DPub toc role.""" + + return "doc-toc" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_drawing_area(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the drawing area role""" if role is None: @@ -547,7 +1079,19 @@ class AXUtilitiesRole: return role == Atspi.Role.DRAWING_AREA @staticmethod - def is_editbar(obj, role=None): + def is_editable_combo_box(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: + """Returns True if obj is an editable combobox""" + + if role is None: + role = AXObject.get_role(obj) + if role != Atspi.Role.COMBO_BOX: + return False + if AXUtilitiesState.is_editable(obj): + return True + return bool(AXObject.find_descendant(obj, AXUtilitiesRole.is_text_input)) + + @staticmethod + def is_editbar(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the editbar role""" if role is None: @@ -555,7 +1099,7 @@ class AXUtilitiesRole: return role == Atspi.Role.EDITBAR @staticmethod - def is_embedded(obj, role=None): + def is_embedded(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the embedded role""" if role is None: @@ -563,7 +1107,7 @@ class AXUtilitiesRole: return role == Atspi.Role.EMBEDDED @staticmethod - def is_entry(obj, role=None): + def is_entry(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the entry role""" if role is None: @@ -571,7 +1115,7 @@ class AXUtilitiesRole: return role == Atspi.Role.ENTRY @staticmethod - def is_extended(obj, role=None): + def is_extended(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the extended role""" if role is None: @@ -579,7 +1123,29 @@ class AXUtilitiesRole: return role == Atspi.Role.EXTENDED @staticmethod - def is_file_chooser(obj, role=None): + def is_feed(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the feed role""" + + return "feed" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_feed_article(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the article role and descends from a feed.""" + + if not AXUtilitiesRole.is_article(obj, role): + return False + + return AXObject.find_ancestor(obj, AXUtilitiesRole.is_feed) is not None + + @staticmethod + def is_figure(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the figure role or tag.""" + + return "figure" in AXUtilitiesRole._get_xml_roles(obj) \ + or AXUtilitiesRole._get_tag(obj) == "figure" + + @staticmethod + def is_file_chooser(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the file chooser role""" if role is None: @@ -587,7 +1153,7 @@ class AXUtilitiesRole: return role == Atspi.Role.FILE_CHOOSER @staticmethod - def is_filler(obj, role=None): + def is_filler(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the filler role""" if role is None: @@ -595,7 +1161,7 @@ class AXUtilitiesRole: return role == Atspi.Role.FILLER @staticmethod - def is_focus_traversable(obj, role=None): + def is_focus_traversable(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the focus traversable role""" if role is None: @@ -603,7 +1169,7 @@ class AXUtilitiesRole: return role == Atspi.Role.FOCUS_TRAVERSABLE @staticmethod - def is_font_chooser(obj, role=None): + def is_font_chooser(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the font chooser role""" if role is None: @@ -611,7 +1177,7 @@ class AXUtilitiesRole: return role == Atspi.Role.FONT_CHOOSER @staticmethod - def is_footer(obj, role=None): + def is_footer(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the footer role""" if role is None: @@ -619,7 +1185,7 @@ class AXUtilitiesRole: return role == Atspi.Role.FOOTER @staticmethod - def is_footnote(obj, role=None): + def is_footnote(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the footnote role""" if role is None: @@ -627,7 +1193,7 @@ class AXUtilitiesRole: return role == Atspi.Role.FOOTNOTE @staticmethod - def is_form(obj, role=None): + def is_form(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the form role""" if role is None: @@ -635,7 +1201,7 @@ class AXUtilitiesRole: return role == Atspi.Role.FORM @staticmethod - def is_frame(obj, role=None): + def is_frame(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the frame role""" if role is None: @@ -643,7 +1209,7 @@ class AXUtilitiesRole: return role == Atspi.Role.FRAME @staticmethod - def is_glass_pane(obj, role=None): + def is_glass_pane(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the glass pane role""" if role is None: @@ -651,7 +1217,45 @@ class AXUtilitiesRole: return role == Atspi.Role.GLASS_PANE @staticmethod - def is_grouping(obj, role=None): + def is_gui_list(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the list role but contains UI rather than static text.""" + + if not AXUtilitiesRole.is_list(obj, role): + return False + + return AXObject.get_toolkit_name(obj) == "gtk" + + @staticmethod + def is_grid(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the grid role.""" + + if not AXUtilitiesRole.is_table(obj, role): + return False + + return "grid" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_grid_cell(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the gridcell role or the cell role and is in a grid.""" + + if not AXUtilitiesRole.is_table_cell(obj, role): + return False + + roles = AXUtilitiesRole._get_xml_roles(obj) + if "gridcell" in roles: + return True + if "cell" in roles: + return AXObject.find_ancestor(obj, AXUtilitiesRole.is_grid) is not None + return False + + @staticmethod + def is_group(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj is an ARIA group.""" + + return "group" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_grouping(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the grouping role""" if role is None: @@ -659,7 +1263,7 @@ class AXUtilitiesRole: return role == Atspi.Role.GROUPING @staticmethod - def is_header(obj, role=None): + def is_header(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the header role""" if role is None: @@ -667,7 +1271,7 @@ class AXUtilitiesRole: return role == Atspi.Role.HEADER @staticmethod - def is_heading(obj, role=None): + def is_heading(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the heading role""" if role is None: @@ -675,7 +1279,7 @@ class AXUtilitiesRole: return role == Atspi.Role.HEADING @staticmethod - def is_html_container(obj, role=None): + def is_html_container(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the html container role""" if role is None: @@ -683,28 +1287,28 @@ class AXUtilitiesRole: return role == Atspi.Role.HTML_CONTAINER @staticmethod - def is_horizontal_scrollbar(obj, role=None): + def is_horizontal_scrollbar(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj is a horizontal scrollbar""" return AXUtilitiesRole.is_scroll_bar(obj, role) \ and AXObject.has_state(obj, Atspi.StateType.HORIZONTAL) @staticmethod - def is_horizontal_separator(obj, role=None): + def is_horizontal_separator(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj is a horizontal separator""" return AXUtilitiesRole.is_separator(obj, role) \ and AXObject.has_state(obj, Atspi.StateType.HORIZONTAL) @staticmethod - def is_horizontal_slider(obj, role=None): + def is_horizontal_slider(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj is a horizontal slider""" return AXUtilitiesRole.is_slider(obj, role) \ and AXObject.has_state(obj, Atspi.StateType.HORIZONTAL) @staticmethod - def is_icon(obj, role=None): + def is_icon(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the icon role""" if role is None: @@ -712,7 +1316,7 @@ class AXUtilitiesRole: return role == Atspi.Role.ICON @staticmethod - def is_icon_or_canvas(obj, role=None): + def is_icon_or_canvas(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the icon or canvas role""" if role is None: @@ -720,7 +1324,7 @@ class AXUtilitiesRole: return role in [Atspi.Role.ICON, Atspi.Role.CANVAS] @staticmethod - def is_image(obj, role=None): + def is_image(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the image role""" if role is None: @@ -728,7 +1332,7 @@ class AXUtilitiesRole: return role == Atspi.Role.IMAGE @staticmethod - def is_image_or_canvas(obj, role=None): + def is_image_or_canvas(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the image or canvas role""" if role is None: @@ -736,7 +1340,7 @@ class AXUtilitiesRole: return role in [Atspi.Role.IMAGE, Atspi.Role.CANVAS] @staticmethod - def is_image_map(obj, role=None): + def is_image_map(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the image map role""" if role is None: @@ -744,7 +1348,7 @@ class AXUtilitiesRole: return role == Atspi.Role.IMAGE_MAP @staticmethod - def is_info_bar(obj, role=None): + def is_info_bar(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the info bar role""" if role is None: @@ -752,7 +1356,34 @@ class AXUtilitiesRole: return role == Atspi.Role.INFO_BAR @staticmethod - def is_input_method_window(obj, role=None): + def is_inline_internal_frame(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the internal frame role and is inline.""" + + if not AXUtilitiesRole.is_internal_frame(obj, role): + return False + + return "inline" in AXUtilitiesRole._get_display_style(obj) + + @staticmethod + def is_inline_list_item(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the list item role and is inline.""" + + if not AXUtilitiesRole.is_list_item(obj, role): + return False + + return "inline" in AXUtilitiesRole._get_display_style(obj) + + @staticmethod + def is_inline_suggestion(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the suggestion role and is inline.""" + + if not AXUtilitiesRole.is_suggestion(obj, role): + return False + + return "inline" in AXUtilitiesRole._get_display_style(obj) + + @staticmethod + def is_input_method_window(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the input method window role""" if role is None: @@ -760,15 +1391,15 @@ class AXUtilitiesRole: return role == Atspi.Role.INPUT_METHOD_WINDOW @staticmethod - def is_internal_frame(obj, role=None): + def is_internal_frame(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the internal frame role""" if role is None: role = AXObject.get_role(obj) - return role == Atspi.Role.INTERNAL_FRAME + return role == Atspi.Role.INTERNAL_FRAME or AXUtilitiesRole._get_tag(obj) == "iframe" @staticmethod - def is_invalid_role(obj, role=None): + def is_invalid_role(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the invalid role""" if role is None: @@ -776,7 +1407,7 @@ class AXUtilitiesRole: return role == Atspi.Role.INVALID @staticmethod - def is_label(obj, role=None): + def is_label(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the label role""" if role is None: @@ -784,7 +1415,7 @@ class AXUtilitiesRole: return role == Atspi.Role.LABEL @staticmethod - def is_label_or_caption(obj, role=None): + def is_label_or_caption(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the label or caption role""" if role is None: @@ -792,7 +1423,7 @@ class AXUtilitiesRole: return role in [Atspi.Role.LABEL, Atspi.Role.CAPTION] @staticmethod - def is_landmark(obj, role=None): + def is_landmark(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the landmark role""" if role is None: @@ -800,7 +1431,76 @@ class AXUtilitiesRole: return role == Atspi.Role.LANDMARK @staticmethod - def is_layered_pane(obj, role=None): + def is_landmark_banner(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the banner landmark role""" + + return "banner" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_landmark_complementary( + obj: Atspi.Accessible, _role: Atspi.Role | None = None + ) -> bool: + """Returns True if obj has the complementary landmark role""" + + return "complementary" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_landmark_contentinfo(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the contentinfo landmark role""" + + return "contentinfo" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_landmark_form(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the form landmark role""" + + return "form" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_landmark_main(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the main landmark role""" + + return "main" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_landmark_navigation(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the navigation landmark role""" + + return "navigation" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_landmark_region(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the region landmark role""" + + return "region" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_landmark_search(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the search landmark role""" + + return "search" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_landmark_without_type(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the landmark role but no type""" + + if not AXUtilitiesRole.is_landmark(obj, role): + return False + + roles = AXUtilitiesRole._get_xml_roles(obj) + return not roles + + @staticmethod + def is_large_container(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: + """Returns True if obj has a large container role""" + + if role is None: + role = AXObject.get_role(obj) + + return role in AXUtilitiesRole.get_large_container_roles() + + @staticmethod + def is_layered_pane(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the layered pane role""" if role is None: @@ -808,7 +1508,7 @@ class AXUtilitiesRole: return role == Atspi.Role.LAYERED_PANE @staticmethod - def is_level_bar(obj, role=None): + def is_level_bar(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the level bar role""" if role is None: @@ -816,7 +1516,7 @@ class AXUtilitiesRole: return role == Atspi.Role.LEVEL_BAR @staticmethod - def is_link(obj, role=None): + def is_link(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the link role""" if role is None: @@ -824,7 +1524,7 @@ class AXUtilitiesRole: return role == Atspi.Role.LINK @staticmethod - def is_list(obj, role=None): + def is_list(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the list role""" if role is None: @@ -832,7 +1532,7 @@ class AXUtilitiesRole: return role == Atspi.Role.LIST @staticmethod - def is_list_box(obj, role=None): + def is_list_box(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the list box role""" if role is None: @@ -840,7 +1540,15 @@ class AXUtilitiesRole: return role == Atspi.Role.LIST_BOX @staticmethod - def is_list_item(obj, role=None): + def is_list_box_item(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: + """Returns True if obj is an item in a list box""" + + if not AXUtilitiesRole.is_list_item(obj, role): + return False + return AXObject.find_ancestor(obj, AXUtilitiesRole.is_list_box) is not None + + @staticmethod + def is_list_item(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the list item role""" if role is None: @@ -848,7 +1556,7 @@ class AXUtilitiesRole: return role == Atspi.Role.LIST_ITEM @staticmethod - def is_log(obj, role=None): + def is_log(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the log role""" if role is None: @@ -856,15 +1564,24 @@ class AXUtilitiesRole: return role == Atspi.Role.LOG @staticmethod - def is_mark(obj, role=None): + def is_live_region(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj is a live region.""" + + attrs = AXObject.get_attributes_dict(obj) + return "container-live" in attrs and attrs.get("container-live") in ["polite", "assertive"] + + @staticmethod + def is_mark(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the mark role""" if role is None: role = AXObject.get_role(obj) - return role == Atspi.Role.MARK + return role == Atspi.Role.MARK \ + or "mark" in AXUtilitiesRole._get_xml_roles(obj) \ + or "mark" == AXUtilitiesRole._get_tag(obj) @staticmethod - def is_marquee(obj, role=None): + def is_marquee(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the marquee role""" if role is None: @@ -872,7 +1589,7 @@ class AXUtilitiesRole: return role == Atspi.Role.MARQUEE @staticmethod - def is_math(obj, role=None): + def is_math(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the math role""" if role is None: @@ -880,7 +1597,19 @@ class AXUtilitiesRole: return role == Atspi.Role.MATH @staticmethod - def is_math_fraction(obj, role=None): + def is_math_enclose(obj: Atspi.Accessible) -> bool: + """Returns True if obj has the math enclose role/tag""" + + return AXUtilitiesRole._get_tag(obj) == "menclose" + + @staticmethod + def is_math_fenced(obj: Atspi.Accessible) -> bool: + """Returns True if obj has the math fenced role/tag""" + + return AXUtilitiesRole._get_tag(obj) == "mfenced" + + @staticmethod + def is_math_fraction(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the math fraction role""" if role is None: @@ -888,7 +1617,88 @@ class AXUtilitiesRole: return role == Atspi.Role.MATH_FRACTION @staticmethod - def is_math_root(obj, role=None): + def is_math_fraction_without_bar( + obj: Atspi.Accessible, role: Atspi.Role | None = None + ) -> bool: + """Returns True if obj has the math fraction role and lacks the fraction bar""" + + if not AXUtilitiesRole.is_math_fraction(obj, role): + return False + + line_thickness = AXObject.get_attribute(obj, "linethickness") + if not line_thickness: + return False + + for char in line_thickness: + if char.isnumeric() and char != "0": + return False + + return True + + @staticmethod + def is_math_layout_only(obj: Atspi.Accessible) -> bool: + """Returns True if obj has a layout-only math role""" + + return AXUtilitiesRole._get_tag(obj) \ + in ["mrow", "mstyle", "merror", "mpadded", "none", "semantics"] + + @staticmethod + def is_math_multi_script(obj: Atspi.Accessible) -> bool: + """Returns True if obj has the math multi-scripts role/tag""" + + return AXUtilitiesRole._get_tag(obj) == "mmultiscripts" + + @staticmethod + def is_math_related(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: + """Returns True if obj has a math-related role""" + + if role is None: + role = AXObject.get_role(obj) + if role in [Atspi.Role.MATH, Atspi.Role.MATH_FRACTION, Atspi.Role.MATH_ROOT]: + return True + return AXUtilitiesRole._get_tag(obj) in ["math", + "maction", + "maligngroup", + "malignmark", + "menclose", + "merror", + "mfenced", + "mfrac", + "mglyph", + "mi", + "mlabeledtr", + "mlongdiv", + "mmultiscripts", + "mn", + "mo", + "mover", + "mpadded", + "mphantom", + "mprescripts", + "mroot", + "mrow", + "ms", + "mscarries", + "mscarry", + "msgroup", + "msline", + "mspace", + "msqrt", + "msrow", + "mstack", + "mstyle", + "msub", + "msup", + "msubsup", + "mtable", + "mtd", + "mtext", + "mtr", + "munder", + "munderover"] + + @staticmethod + def is_math_root(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the math root role""" if role is None: @@ -896,7 +1706,43 @@ class AXUtilitiesRole: return role == Atspi.Role.MATH_ROOT @staticmethod - def is_menu(obj, role=None): + def is_math_square_root(obj: Atspi.Accessible) -> bool: + """Returns True if obj has the math root role/tag""" + + return AXUtilitiesRole._get_tag(obj) == "msqrt" + + @staticmethod + def is_math_sub_or_super_script(obj: Atspi.Accessible) -> bool: + """Returns True if obj has the math subscript or superscript role/tag""" + + return AXUtilitiesRole._get_tag(obj) in ["msub", "msup", "msubsup"] + + @staticmethod + def is_math_table(obj: Atspi.Accessible) -> bool: + """Returns True if obj has the math table role/tag""" + + return AXUtilitiesRole._get_tag(obj) == "mtable" + + @staticmethod + def is_math_table_row(obj: Atspi.Accessible) -> bool: + """Returns True if obj has the math table row role/tag""" + + return AXUtilitiesRole._get_tag(obj) in ["mtr", "mlabeledtr"] + + @staticmethod + def is_math_token(obj: Atspi.Accessible) -> bool: + """Returns True if obj has a math token role/tag""" + + return AXUtilitiesRole._get_tag(obj) in ["mi", "mn", "mo", "mtext", "ms", "mspace"] + + @staticmethod + def is_math_under_or_over_script(obj: Atspi.Accessible) -> bool: + """Returns True if obj has the math under-script or over-script role/tag""" + + return AXUtilitiesRole._get_tag(obj) in ["mover", "munder", "munderover"] + + @staticmethod + def is_menu(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the menu role""" if role is None: @@ -904,7 +1750,7 @@ class AXUtilitiesRole: return role == Atspi.Role.MENU @staticmethod - def is_menu_bar(obj, role=None): + def is_menu_bar(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the menubar role""" if role is None: @@ -912,7 +1758,7 @@ class AXUtilitiesRole: return role == Atspi.Role.MENU_BAR @staticmethod - def is_menu_item(obj, role=None): + def is_menu_item(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the menu item role""" if role is None: @@ -920,7 +1766,7 @@ class AXUtilitiesRole: return role == Atspi.Role.MENU_ITEM @staticmethod - def is_menu_item_of_any_kind(obj, role=None): + def is_menu_item_of_any_kind(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has any menu item role""" roles = AXUtilitiesRole.get_menu_item_roles() @@ -929,7 +1775,7 @@ class AXUtilitiesRole: return role in roles @staticmethod - def is_menu_related(obj, role=None): + def is_menu_related(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has any menu-related role""" roles = AXUtilitiesRole.get_menu_related_roles() @@ -938,21 +1784,21 @@ class AXUtilitiesRole: return role in roles @staticmethod - def is_modal_dialog(obj, role=None): + def is_modal_dialog(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the alert or dialog role and modal state""" return AXUtilitiesRole.is_dialog_or_alert(obj, role) \ and AXObject.has_state(obj, Atspi.StateType.MODAL) @staticmethod - def is_multi_line_entry(obj, role=None): + def is_multi_line_entry(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the entry role and multiline state""" return AXUtilitiesRole.is_entry(obj, role) \ and AXObject.has_state(obj, Atspi.StateType.MULTI_LINE) @staticmethod - def is_notification(obj, role=None): + def is_notification(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the notification role""" if role is None: @@ -960,7 +1806,7 @@ class AXUtilitiesRole: return role == Atspi.Role.NOTIFICATION @staticmethod - def is_option_pane(obj, role=None): + def is_option_pane(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the option pane role""" if role is None: @@ -968,7 +1814,7 @@ class AXUtilitiesRole: return role == Atspi.Role.OPTION_PANE @staticmethod - def is_page(obj, role=None): + def is_page(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the page role""" if role is None: @@ -976,7 +1822,7 @@ class AXUtilitiesRole: return role == Atspi.Role.PAGE @staticmethod - def is_page_tab(obj, role=None): + def is_page_tab(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the page tab role""" if role is None: @@ -984,7 +1830,7 @@ class AXUtilitiesRole: return role == Atspi.Role.PAGE_TAB @staticmethod - def is_page_tab_list(obj, role=None): + def is_page_tab_list(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the page tab list role""" if role is None: @@ -992,7 +1838,7 @@ class AXUtilitiesRole: return role == Atspi.Role.PAGE_TAB_LIST @staticmethod - def is_page_tab_list_related(obj, role=None): + def is_page_tab_list_related(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the page tab or page tab list role""" roles = [Atspi.Role.PAGE_TAB_LIST, Atspi.Role.PAGE_TAB] @@ -1001,7 +1847,7 @@ class AXUtilitiesRole: return role in roles @staticmethod - def is_panel(obj, role=None): + def is_panel(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the panel role""" if role is None: @@ -1009,7 +1855,7 @@ class AXUtilitiesRole: return role == Atspi.Role.PANEL @staticmethod - def is_paragraph(obj, role=None): + def is_paragraph(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the paragraph role""" if role is None: @@ -1017,7 +1863,7 @@ class AXUtilitiesRole: return role == Atspi.Role.PARAGRAPH @staticmethod - def is_password_text(obj, role=None): + def is_password_text(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the password text role""" if role is None: @@ -1025,7 +1871,7 @@ class AXUtilitiesRole: return role == Atspi.Role.PASSWORD_TEXT @staticmethod - def is_popup_menu(obj, role=None): + def is_popup_menu(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the popup menu role""" if role is None: @@ -1033,7 +1879,7 @@ class AXUtilitiesRole: return role == Atspi.Role.POPUP_MENU @staticmethod - def is_progress_bar(obj, role=None): + def is_progress_bar(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the progress bar role""" if role is None: @@ -1041,15 +1887,15 @@ class AXUtilitiesRole: return role == Atspi.Role.PROGRESS_BAR @staticmethod - def is_push_button(obj, role=None): + def is_push_button(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the push button role""" if role is None: role = AXObject.get_role(obj) - return role == Atspi.Role.PUSH_BUTTON + return role == Atspi.Role.BUTTON @staticmethod - def is_push_button_menu(obj, role=None): + def is_push_button_menu(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the push button menu role""" if role is None: @@ -1057,7 +1903,7 @@ class AXUtilitiesRole: return role == Atspi.Role.PUSH_BUTTON_MENU @staticmethod - def is_radio_button(obj, role=None): + def is_radio_button(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the radio button role""" if role is None: @@ -1065,7 +1911,7 @@ class AXUtilitiesRole: return role == Atspi.Role.RADIO_BUTTON @staticmethod - def is_radio_menu_item(obj, role=None): + def is_radio_menu_item(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the radio menu item role""" if role is None: @@ -1073,7 +1919,7 @@ class AXUtilitiesRole: return role == Atspi.Role.RADIO_MENU_ITEM @staticmethod - def is_rating(obj, role=None): + def is_rating(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the rating role""" if role is None: @@ -1081,7 +1927,7 @@ class AXUtilitiesRole: return role == Atspi.Role.RATING @staticmethod - def is_redundant_object(obj, role=None): + def is_redundant_object_role(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the redundant object role""" if role is None: @@ -1089,7 +1935,7 @@ class AXUtilitiesRole: return role == Atspi.Role.REDUNDANT_OBJECT @staticmethod - def is_root_pane(obj, role=None): + def is_root_pane(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the root pane role""" if role is None: @@ -1097,7 +1943,7 @@ class AXUtilitiesRole: return role == Atspi.Role.ROOT_PANE @staticmethod - def is_row_header(obj, role=None): + def is_row_header(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the row header role""" if role is None: @@ -1105,7 +1951,7 @@ class AXUtilitiesRole: return role == Atspi.Role.ROW_HEADER @staticmethod - def is_ruler(obj, role=None): + def is_ruler(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the ruler role""" if role is None: @@ -1113,7 +1959,7 @@ class AXUtilitiesRole: return role == Atspi.Role.RULER @staticmethod - def is_scroll_bar(obj, role=None): + def is_scroll_bar(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the scrollbar role""" if role is None: @@ -1121,7 +1967,7 @@ class AXUtilitiesRole: return role == Atspi.Role.SCROLL_BAR @staticmethod - def is_scroll_pane(obj, role=None): + def is_scroll_pane(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the scroll pane role""" if role is None: @@ -1129,7 +1975,7 @@ class AXUtilitiesRole: return role == Atspi.Role.SCROLL_PANE @staticmethod - def is_section(obj, role=None): + def is_section(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the section role""" if role is None: @@ -1137,7 +1983,7 @@ class AXUtilitiesRole: return role == Atspi.Role.SECTION @staticmethod - def is_separator(obj, role=None): + def is_separator(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the separator role""" if role is None: @@ -1145,14 +1991,30 @@ class AXUtilitiesRole: return role == Atspi.Role.SEPARATOR @staticmethod - def is_single_line_entry(obj, role=None): - """Returns True if obj has the entry role and multiline state""" + def is_single_line_autocomplete_entry( + obj: Atspi.Accessible, role: Atspi.Role | None = None + ) -> bool: + """Returns True if obj has the entry role and single-line state""" - return AXUtilitiesRole.is_entry(obj, role) \ - and AXObject.has_state(obj, Atspi.StateType.SINGLE_LINE) + if not AXUtilitiesRole.is_single_line_entry(obj, role): + return False + + return AXUtilitiesState.supports_autocompletion(obj) @staticmethod - def is_slider(obj, role=None): + def is_single_line_entry(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the entry role and the single-line state""" + + if not AXUtilitiesState.is_single_line(obj): + return False + if AXUtilitiesRole.is_entry(obj, role): + return True + if AXUtilitiesRole.is_text(obj, role): + return AXUtilitiesState.is_editable(obj) + return False + + @staticmethod + def is_slider(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the slider role""" if role is None: @@ -1160,7 +2022,7 @@ class AXUtilitiesRole: return role == Atspi.Role.SLIDER @staticmethod - def is_spin_button(obj, role=None): + def is_spin_button(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the spin button role""" if role is None: @@ -1168,7 +2030,7 @@ class AXUtilitiesRole: return role == Atspi.Role.SPIN_BUTTON @staticmethod - def is_split_pane(obj, role=None): + def is_split_pane(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the split pane role""" if role is None: @@ -1176,7 +2038,7 @@ class AXUtilitiesRole: return role == Atspi.Role.SPLIT_PANE @staticmethod - def is_static(obj, role=None): + def is_static(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the static role""" if role is None: @@ -1184,7 +2046,7 @@ class AXUtilitiesRole: return role == Atspi.Role.STATIC @staticmethod - def is_status_bar(obj, role=None): + def is_status_bar(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the statusbar role""" if role is None: @@ -1192,7 +2054,7 @@ class AXUtilitiesRole: return role == Atspi.Role.STATUS_BAR @staticmethod - def is_subscript(obj, role=None): + def is_subscript(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the subscript role""" if role is None: @@ -1200,7 +2062,9 @@ class AXUtilitiesRole: return role == Atspi.Role.SUBSCRIPT @staticmethod - def is_subscript_or_superscript(obj, role=None): + def is_subscript_or_superscript( + obj: Atspi.Accessible, role: Atspi.Role | None = None + ) -> bool: """Returns True if obj has the subscript or superscript role""" if role is None: @@ -1208,15 +2072,26 @@ class AXUtilitiesRole: return role in [Atspi.Role.SUBSCRIPT, Atspi.Role.SUPERSCRIPT] @staticmethod - def is_suggestion(obj, role=None): + def is_subscript_or_superscript_text( + obj: Atspi.Accessible, role: Atspi.Role | None = None + ) -> bool: + """Returns True if obj has the subscript or superscript role and is not math-related""" + + if AXUtilitiesRole.is_math_related(obj, role): + return False + return AXUtilitiesRole.is_subscript_or_superscript(obj, role) + + @staticmethod + def is_suggestion(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the suggestion role""" if role is None: role = AXObject.get_role(obj) - return role == Atspi.Role.SUGGESTION + return role == Atspi.Role.SUGGESTION \ + or "suggestion" in AXUtilitiesRole._get_xml_roles(obj) @staticmethod - def is_superscript(obj, role=None): + def is_superscript(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the superscript role""" if role is None: @@ -1224,7 +2099,25 @@ class AXUtilitiesRole: return role == Atspi.Role.SUPERSCRIPT @staticmethod - def is_table(obj, role=None): + def is_svg(obj: Atspi.Accessible) -> bool: + """Returns True if obj is an svg.""" + + return AXUtilitiesRole._get_tag(obj) == "svg" + + @staticmethod + def is_switch(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the switch role.""" + + if role is None: + role = AXObject.get_role(obj) + + if role == Atspi.Role.SWITCH: + return True + + return "switch" in AXUtilitiesRole._get_xml_roles(obj) + + @staticmethod + def is_table(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the table role""" if role is None: @@ -1232,7 +2125,7 @@ class AXUtilitiesRole: return role == Atspi.Role.TABLE @staticmethod - def is_table_cell(obj, role=None): + def is_table_cell(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the table cell role""" if role is None: @@ -1240,7 +2133,7 @@ class AXUtilitiesRole: return role == Atspi.Role.TABLE_CELL @staticmethod - def is_table_cell_or_header(obj, role=None): + def is_table_cell_or_header(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the table cell or a header-related role""" roles = AXUtilitiesRole.get_table_cell_roles() @@ -1249,7 +2142,7 @@ class AXUtilitiesRole: return role in roles @staticmethod - def is_table_column_header(obj, role=None): + def is_table_column_header(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the table column header role""" if role is None: @@ -1257,7 +2150,7 @@ class AXUtilitiesRole: return role == Atspi.Role.TABLE_COLUMN_HEADER @staticmethod - def is_table_header(obj, role=None): + def is_table_header(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has a table header related role""" roles = AXUtilitiesRole.get_table_header_roles() @@ -1266,7 +2159,11 @@ class AXUtilitiesRole: return role in roles @staticmethod - def is_table_related(obj, role=None, include_caption=False): + def is_table_related( + obj: Atspi.Accessible, + role: Atspi.Role | None = None, + include_caption: bool = False + ) -> bool: """Returns True if obj has a table-related role""" roles = AXUtilitiesRole.get_table_related_roles(include_caption) @@ -1275,7 +2172,7 @@ class AXUtilitiesRole: return role in roles @staticmethod - def is_table_row(obj, role=None): + def is_table_row(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the table row role""" if role is None: @@ -1283,7 +2180,7 @@ class AXUtilitiesRole: return role == Atspi.Role.TABLE_ROW @staticmethod - def is_table_row_header(obj, role=None): + def is_table_row_header(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the table row header role""" if role is None: @@ -1291,7 +2188,7 @@ class AXUtilitiesRole: return role == Atspi.Role.TABLE_ROW_HEADER @staticmethod - def is_tearoff_menu_item(obj, role=None): + def is_tearoff_menu_item(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the tearoff menu item role""" if role is None: @@ -1299,14 +2196,14 @@ class AXUtilitiesRole: return role == Atspi.Role.TEAROFF_MENU_ITEM @staticmethod - def is_terminal(obj, role=None): + def is_terminal(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the terminal role""" if role is None: role = AXObject.get_role(obj) return role == Atspi.Role.TERMINAL @staticmethod - def is_text(obj, role=None): + def is_text(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the text role""" if role is None: @@ -1314,16 +2211,127 @@ class AXUtilitiesRole: return role == Atspi.Role.TEXT @staticmethod - def is_text_input(obj, role=None): + def is_text_input(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has any role associated with textual input""" roles = [Atspi.Role.ENTRY, Atspi.Role.PASSWORD_TEXT, Atspi.Role.SPIN_BUTTON] if role is None: role = AXObject.get_role(obj) - return role in roles + if role in roles: + return True + if role == Atspi.Role.TEXT: + return AXUtilitiesState.is_editable(obj) and AXUtilitiesState.is_single_line(obj) + if AXUtilitiesRole.is_editable_combo_box(obj): + return True + return False @staticmethod - def is_timer(obj, role=None): + def is_text_input_date(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: + """Returns True if obj is a date text input""" + + if not AXUtilitiesRole.is_text_input(obj, role): + return False + + attrs = AXObject.get_attributes_dict(obj) + return attrs.get("text-input-type") == "date" + + @staticmethod + def is_text_input_email(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: + """Returns True if obj is an email text input""" + + if not AXUtilitiesRole.is_text_input(obj, role): + return False + + attrs = AXObject.get_attributes_dict(obj) + return attrs.get("text-input-type") == "email" + + @staticmethod + def is_text_input_number(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: + """Returns True if obj is a numeric text input""" + + if not AXUtilitiesRole.is_text_input(obj, role): + return False + + attrs = AXObject.get_attributes_dict(obj) + return attrs.get("text-input-type") == "number" + + @staticmethod + def is_text_input_search(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: + """Returns True if obj is a telephone text input""" + + if not AXUtilitiesRole.is_text_input(obj, role): + return False + + attrs = AXObject.get_attributes_dict(obj) + if attrs.get("text-input-type") == "search": + return True + if "searchbox" in AXUtilitiesRole._get_xml_roles(obj): + return True + + ax_id = AXObject.get_accessible_id(obj) or "" + if ax_id: + return "search" in ax_id.lower() or "find" in ax_id.lower() + + child = AXObject.get_child(obj, 0) + if AXUtilitiesRole.is_icon(child) or AXUtilitiesRole.is_image(child): + child_id = AXObject.get_accessible_id(child) or "" + if "search" in child_id.lower() or "find" in child_id.lower(): + return True + # Some toolkits don't localize the symbolic icon names, so it's worth a try. + child_name = AXObject.get_name(child).lower() + return "search" in child_name or "find" in child_name + + return False + + @staticmethod + def is_text_input_telephone(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: + """Returns True if obj is a telephone text input""" + + if not AXUtilitiesRole.is_text_input(obj, role): + return False + + attrs = AXObject.get_attributes_dict(obj) + return attrs.get("text-input-type") == "telephone" + + @staticmethod + def is_text_input_time(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: + """Returns True if obj is a time text input""" + + if not AXUtilitiesRole.is_text_input(obj, role): + return False + + attrs = AXObject.get_attributes_dict(obj) + return attrs.get("text-input-type") == "time" + + @staticmethod + def is_text_input_url(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: + """Returns True if obj is a url text input""" + + if not AXUtilitiesRole.is_text_input(obj, role): + return False + + attrs = AXObject.get_attributes_dict(obj) + return attrs.get("text-input-type") == "url" + + @staticmethod + def is_text_input_week(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: + """Returns True if obj is a week text input""" + + if not AXUtilitiesRole.is_text_input(obj, role): + return False + + attrs = AXObject.get_attributes_dict(obj) + return attrs.get("text-input-type") == "week" + + @staticmethod + def is_time(obj: Atspi.Accessible, _role: Atspi.Role | None = None) -> bool: + """Returns True if obj has the time role""" + + return "time" in AXUtilitiesRole._get_xml_roles(obj) \ + or "time" == AXUtilitiesRole._get_tag(obj) + + @staticmethod + def is_timer(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the timer role""" if role is None: @@ -1331,7 +2339,7 @@ class AXUtilitiesRole: return role == Atspi.Role.TIMER @staticmethod - def is_title_bar(obj, role=None): + def is_title_bar(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the titlebar role""" if role is None: @@ -1339,7 +2347,7 @@ class AXUtilitiesRole: return role == Atspi.Role.TITLE_BAR @staticmethod - def is_toggle_button(obj, role=None): + def is_toggle_button(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the toggle button role""" if role is None: @@ -1347,7 +2355,7 @@ class AXUtilitiesRole: return role == Atspi.Role.TOGGLE_BUTTON @staticmethod - def is_tool_bar(obj, role=None): + def is_tool_bar(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the toolbar role""" if role is None: @@ -1355,7 +2363,7 @@ class AXUtilitiesRole: return role == Atspi.Role.TOOL_BAR @staticmethod - def is_tool_tip(obj, role=None): + def is_tool_tip(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the tooltip role""" if role is None: @@ -1363,7 +2371,7 @@ class AXUtilitiesRole: return role == Atspi.Role.TOOL_TIP @staticmethod - def is_tree(obj, role=None): + def is_tree(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the tree role""" if role is None: @@ -1371,7 +2379,7 @@ class AXUtilitiesRole: return role == Atspi.Role.TREE @staticmethod - def is_tree_or_tree_table(obj, role=None): + def is_tree_or_tree_table(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the tree or tree table role""" if role is None: @@ -1379,7 +2387,7 @@ class AXUtilitiesRole: return role in [Atspi.Role.TREE, Atspi.Role.TREE_TABLE] @staticmethod - def is_tree_related(obj, role=None): + def is_tree_related(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has a tree-related role""" roles = [Atspi.Role.TREE, @@ -1390,7 +2398,7 @@ class AXUtilitiesRole: return role in roles @staticmethod - def is_tree_item(obj, role=None): + def is_tree_item(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the tree item role""" if role is None: @@ -1398,7 +2406,7 @@ class AXUtilitiesRole: return role == Atspi.Role.TREE_ITEM @staticmethod - def is_tree_table(obj, role=None): + def is_tree_table(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the tree table role""" if role is None: @@ -1406,7 +2414,7 @@ class AXUtilitiesRole: return role == Atspi.Role.TREE_TABLE @staticmethod - def is_unknown(obj, role=None): + def is_unknown(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the unknown role""" if role is None: @@ -1414,7 +2422,7 @@ class AXUtilitiesRole: return role == Atspi.Role.UNKNOWN @staticmethod - def is_unknown_or_redundant(obj, role=None): + def is_unknown_or_redundant(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the unknown or redundant object role""" if role is None: @@ -1422,28 +2430,28 @@ class AXUtilitiesRole: return role in [Atspi.Role.UNKNOWN, Atspi.Role.REDUNDANT_OBJECT] @staticmethod - def is_vertical_scrollbar(obj, role=None): + def is_vertical_scrollbar(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj is a vertical scrollbar""" return AXUtilitiesRole.is_scroll_bar(obj, role) \ and AXObject.has_state(obj, Atspi.StateType.VERTICAL) @staticmethod - def is_vertical_separator(obj, role=None): + def is_vertical_separator(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj is a vertical separator""" return AXUtilitiesRole.is_separator(obj, role) \ and AXObject.has_state(obj, Atspi.StateType.VERTICAL) @staticmethod - def is_vertical_slider(obj, role=None): + def is_vertical_slider(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj is a vertical slider""" return AXUtilitiesRole.is_slider(obj, role) \ and AXObject.has_state(obj, Atspi.StateType.VERTICAL) @staticmethod - def is_video(obj, role=None): + def is_video(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the video role""" if role is None: @@ -1451,7 +2459,7 @@ class AXUtilitiesRole: return role == Atspi.Role.VIDEO @staticmethod - def is_viewport(obj, role=None): + def is_viewport(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the viewport role""" if role is None: @@ -1459,7 +2467,63 @@ class AXUtilitiesRole: return role == Atspi.Role.VIEWPORT @staticmethod - def is_window(obj, role=None): + def is_web_element(obj: Atspi.Accessible, exclude_pseudo_elements: bool = True) -> bool: + """Returns True if obj is a web element""" + + tag = AXUtilitiesRole._get_tag(obj) + if not tag: + return False + if not exclude_pseudo_elements: + return True + exclude = ["::before", "::after", "::marker"] + return tag not in exclude + + @staticmethod + def is_web_element_custom(obj: Atspi.Accessible) -> bool: + """Returns True if obj is a custom web element""" + + tag = AXUtilitiesRole._get_tag(obj) + return tag is not None and "-" in tag + + @staticmethod + def is_widget(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: + """Returns True if obj has a widget role""" + + if role is None: + role = AXObject.get_role(obj) + return role in AXUtilitiesRole.get_widget_roles() + + @staticmethod + def is_widget_controlled_by_line_navigation( + obj: Atspi.Accessible, role: Atspi.Role | None = None + ) -> bool: + """Returns True if obj is a widget controlled by line navigation""" + + if role is None: + role = AXObject.get_role(obj) + + roles = [Atspi.Role.COMBO_BOX, + Atspi.Role.LIST_BOX, + Atspi.Role.MENU, + Atspi.Role.SPIN_BUTTON, + Atspi.Role.TREE, + Atspi.Role.TREE_TABLE] + if role in roles: + return True + + if AXUtilitiesState.is_editable(obj) or AXUtilitiesState.is_selectable(obj): + return AXObject.find_ancestor(obj, lambda x: AXObject.get_role(x) in roles) is not None + + if not AXUtilitiesState.is_vertical(obj): + return False + + return role in [Atspi.Role.SCROLL_BAR, + Atspi.Role.SEPARATOR, + Atspi.Role.SLIDER, + Atspi.Role.SPLIT_PANE] + + @staticmethod + def is_window(obj: Atspi.Accessible, role: Atspi.Role | None = None) -> bool: """Returns True if obj has the window role""" if role is None: diff --git a/src/cthulhu/ax_utilities_state.py b/src/cthulhu/ax_utilities_state.py index 23d8e28..b146261 100644 --- a/src/cthulhu/ax_utilities_state.py +++ b/src/cthulhu/ax_utilities_state.py @@ -1,9 +1,7 @@ -#!/usr/bin/env python3 +# Utilities for obtaining state-related information. # -# Copyright (c) 2024 Stormux -# Copyright (c) 2010-2012 The Orca Team -# Copyright (c) 2012 Igalia, S.L. -# Copyright (c) 2005-2010 Sun Microsystems Inc. +# Copyright 2023 Igalia, S.L. +# Author: Joanmarie Diggs # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -19,19 +17,12 @@ # License along with this library; if not, write to the # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. -# -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca -""" -Utilities for obtaining state-related information. -These utilities are app-type- and toolkit-agnostic. Utilities that might have -different implementations or results depending on the type of app (e.g. terminal, -chat, web) or toolkit (e.g. Qt, Gtk) should be in script_utilities.py file(s). +# pylint: disable=wrong-import-position +# pylint: disable=too-many-public-methods +# pylint: disable=too-many-return-statements -N.B. There are currently utilities that should never have custom implementations -that live in script_utilities.py files. These will be moved over time. -""" +"""Utilities for obtaining state-related information.""" __id__ = "$Id$" __version__ = "$Revision$" @@ -44,6 +35,7 @@ gi.require_version("Atspi", "2.0") from gi.repository import Atspi from . import debug +from . import messages from .ax_object import AXObject @@ -51,49 +43,71 @@ class AXUtilitiesState: """Utilities for obtaining state-related information.""" @staticmethod - def has_no_state(obj): + def get_current_item_status_string(obj: Atspi.Accessible) -> str: + """Returns the current item status string of obj.""" + + if not AXUtilitiesState.is_active(obj): + return "" + + result = AXObject.get_attribute(obj, "current") + if not result: + return "" + if result == "date": + return messages.CURRENT_DATE + if result == "time": + return messages.CURRENT_TIME + if result == "location": + return messages.CURRENT_LOCATION + if result == "page": + return messages.CURRENT_PAGE + if result == "step": + return messages.CURRENT_STEP + return messages.CURRENT_ITEM + + @staticmethod + def has_no_state(obj: Atspi.Accessible) -> bool: """Returns true if obj has an empty state set""" return AXObject.get_state_set(obj).is_empty() @staticmethod - def has_popup(obj): + def has_popup(obj: Atspi.Accessible) -> bool: """Returns true if obj has the has-popup state""" return AXObject.has_state(obj, Atspi.StateType.HAS_POPUP) @staticmethod - def has_tooltip(obj): + def has_tooltip(obj: Atspi.Accessible) -> bool: """Returns true if obj has the has-tooltip state""" return AXObject.has_state(obj, Atspi.StateType.HAS_TOOLTIP) @staticmethod - def is_active(obj): + def is_active(obj: Atspi.Accessible) -> bool: """Returns true if obj has the active state""" return AXObject.has_state(obj, Atspi.StateType.ACTIVE) @staticmethod - def is_animated(obj): + def is_animated(obj: Atspi.Accessible) -> bool: """Returns true if obj has the animated state""" return AXObject.has_state(obj, Atspi.StateType.ANIMATED) @staticmethod - def is_armed(obj): + def is_armed(obj: Atspi.Accessible) -> bool: """Returns true if obj has the armed state""" return AXObject.has_state(obj, Atspi.StateType.ARMED) @staticmethod - def is_busy(obj): + def is_busy(obj: Atspi.Accessible) -> bool: """Returns true if obj has the busy state""" return AXObject.has_state(obj, Atspi.StateType.BUSY) @staticmethod - def is_checkable(obj): + def is_checkable(obj: Atspi.Accessible) -> bool: """Returns true if obj has the checkable state""" if AXObject.has_state(obj, Atspi.StateType.CHECKABLE): @@ -101,13 +115,13 @@ class AXUtilitiesState: if AXObject.has_state(obj, Atspi.StateType.CHECKED): tokens = ["AXUtilitiesState:", obj, "is checked but lacks state checkable"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return True return False @staticmethod - def is_checked(obj): + def is_checked(obj: Atspi.Accessible) -> bool: """Returns true if obj has the checked state""" if not AXObject.has_state(obj, Atspi.StateType.CHECKED): @@ -115,42 +129,42 @@ class AXUtilitiesState: if not AXObject.has_state(obj, Atspi.StateType.CHECKABLE): tokens = ["AXUtilitiesState:", obj, "is checked but lacks state checkable"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return True @staticmethod - def is_collapsed(obj): + def is_collapsed(obj: Atspi.Accessible) -> bool: """Returns true if obj has the collapsed state""" return AXObject.has_state(obj, Atspi.StateType.COLLAPSED) @staticmethod - def is_default(obj): + def is_default(obj: Atspi.Accessible) -> bool: """Returns true if obj has the is-default state""" return AXObject.has_state(obj, Atspi.StateType.IS_DEFAULT) @staticmethod - def is_defunct(obj): + def is_defunct(obj: Atspi.Accessible) -> bool: """Returns true if obj has the defunct state""" return AXObject.has_state(obj, Atspi.StateType.DEFUNCT) @staticmethod - def is_editable(obj): + def is_editable(obj: Atspi.Accessible) -> bool: """Returns true if obj has the editable state""" return AXObject.has_state(obj, Atspi.StateType.EDITABLE) @staticmethod - def is_enabled(obj): + def is_enabled(obj: Atspi.Accessible) -> bool: """Returns true if obj has the enabled state""" return AXObject.has_state(obj, Atspi.StateType.ENABLED) @staticmethod - def is_expandable(obj): + def is_expandable(obj: Atspi.Accessible) -> bool: """Returns true if obj has the expandable state""" if AXObject.has_state(obj, Atspi.StateType.EXPANDABLE): @@ -158,13 +172,13 @@ class AXUtilitiesState: if AXObject.has_state(obj, Atspi.StateType.EXPANDED): tokens = ["AXUtilitiesState:", obj, "is expanded but lacks state expandable"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return True return False @staticmethod - def is_expanded(obj): + def is_expanded(obj: Atspi.Accessible) -> bool: """Returns true if obj has the expanded state""" if not AXObject.has_state(obj, Atspi.StateType.EXPANDED): @@ -172,12 +186,12 @@ class AXUtilitiesState: if not AXObject.has_state(obj, Atspi.StateType.EXPANDABLE): tokens = ["AXUtilitiesState:", obj, "is expanded but lacks state expandable"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return True @staticmethod - def is_focusable(obj): + def is_focusable(obj: Atspi.Accessible) -> bool: """Returns true if obj has the focusable state""" if AXObject.has_state(obj, Atspi.StateType.FOCUSABLE): @@ -185,13 +199,13 @@ class AXUtilitiesState: if AXObject.has_state(obj, Atspi.StateType.FOCUSED): tokens = ["AXUtilitiesState:", obj, "is focused but lacks state focusable"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return True return False @staticmethod - def is_focused(obj): + def is_focused(obj: Atspi.Accessible) -> bool: """Returns true if obj has the focused state""" if not AXObject.has_state(obj, Atspi.StateType.FOCUSED): @@ -199,168 +213,180 @@ class AXUtilitiesState: if not AXObject.has_state(obj, Atspi.StateType.FOCUSABLE): tokens = ["AXUtilitiesState:", obj, "is focused but lacks state focusable"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return True @staticmethod - def is_horizontal(obj): + def is_hidden(obj: Atspi.Accessible) -> bool: + """Returns true if obj reports being hidden""" + + return AXObject.get_attribute(obj, "hidden", False) == "true" + + @staticmethod + def is_horizontal(obj: Atspi.Accessible) -> bool: """Returns true if obj has the horizontal state""" return AXObject.has_state(obj, Atspi.StateType.HORIZONTAL) @staticmethod - def is_iconified(obj): + def is_iconified(obj: Atspi.Accessible) -> bool: """Returns true if obj has the iconified state""" return AXObject.has_state(obj, Atspi.StateType.ICONIFIED) @staticmethod - def is_indeterminate(obj): + def is_indeterminate(obj: Atspi.Accessible) -> bool: """Returns true if obj has the indeterminate state""" return AXObject.has_state(obj, Atspi.StateType.INDETERMINATE) @staticmethod - def is_invalid_state(obj): + def is_invalid_state(obj: Atspi.Accessible) -> bool: """Returns true if obj has the invalid_state state""" return AXObject.has_state(obj, Atspi.StateType.INVALID) @staticmethod - def is_invalid_entry(obj): + def is_invalid_entry(obj: Atspi.Accessible) -> bool: """Returns true if obj has the invalid_entry state""" return AXObject.has_state(obj, Atspi.StateType.INVALID_ENTRY) @staticmethod - def is_modal(obj): + def is_modal(obj: Atspi.Accessible) -> bool: """Returns true if obj has the modal state""" return AXObject.has_state(obj, Atspi.StateType.MODAL) @staticmethod - def is_multi_line(obj): + def is_multi_line(obj: Atspi.Accessible) -> bool: """Returns true if obj has the multi_line state""" return AXObject.has_state(obj, Atspi.StateType.MULTI_LINE) @staticmethod - def is_multiselectable(obj): + def is_multiselectable(obj: Atspi.Accessible) -> bool: """Returns true if obj has the multiselectable state""" return AXObject.has_state(obj, Atspi.StateType.MULTISELECTABLE) @staticmethod - def is_opaque(obj): + def is_opaque(obj: Atspi.Accessible) -> bool: """Returns true if obj has the opaque state""" return AXObject.has_state(obj, Atspi.StateType.OPAQUE) @staticmethod - def is_pressed(obj): + def is_pressed(obj: Atspi.Accessible) -> bool: """Returns true if obj has the pressed state""" return AXObject.has_state(obj, Atspi.StateType.PRESSED) @staticmethod - def is_read_only(obj): + def is_read_only(obj: Atspi.Accessible) -> bool: """Returns true if obj has the read-only state""" - return AXObject.has_state(obj, Atspi.StateType.READ_ONLY) + if AXObject.has_state(obj, Atspi.StateType.READ_ONLY): + return True + if AXUtilitiesState.is_editable(obj): + return False + + # We cannot count on GTK to set the read-only state on text objects. + return AXObject.get_role(obj) == Atspi.Role.TEXT @staticmethod - def is_required(obj): + def is_required(obj: Atspi.Accessible) -> bool: """Returns true if obj has the required state""" return AXObject.has_state(obj, Atspi.StateType.REQUIRED) @staticmethod - def is_resizable(obj): + def is_resizable(obj: Atspi.Accessible) -> bool: """Returns true if obj has the resizable state""" return AXObject.has_state(obj, Atspi.StateType.RESIZABLE) @staticmethod - def is_selectable(obj): + def is_selectable(obj: Atspi.Accessible) -> bool: """Returns true if obj has the selectable state""" return AXObject.has_state(obj, Atspi.StateType.SELECTABLE) @staticmethod - def is_selectable_text(obj): + def is_selectable_text(obj: Atspi.Accessible) -> bool: """Returns true if obj has the selectable-text state""" return AXObject.has_state(obj, Atspi.StateType.SELECTABLE_TEXT) @staticmethod - def is_selected(obj): + def is_selected(obj: Atspi.Accessible) -> bool: """Returns true if obj has the selected state""" return AXObject.has_state(obj, Atspi.StateType.SELECTED) @staticmethod - def is_sensitive(obj): + def is_sensitive(obj: Atspi.Accessible) -> bool: """Returns true if obj has the sensitive state""" return AXObject.has_state(obj, Atspi.StateType.SENSITIVE) @staticmethod - def is_showing(obj): + def is_showing(obj: Atspi.Accessible) -> bool: """Returns true if obj has the showing state""" return AXObject.has_state(obj, Atspi.StateType.SHOWING) @staticmethod - def is_single_line(obj): + def is_single_line(obj: Atspi.Accessible) -> bool: """Returns true if obj has the single-line state""" return AXObject.has_state(obj, Atspi.StateType.SINGLE_LINE) @staticmethod - def is_stale(obj): + def is_stale(obj: Atspi.Accessible) -> bool: """Returns true if obj has the stale state""" return AXObject.has_state(obj, Atspi.StateType.STALE) @staticmethod - def is_transient(obj): + def is_transient(obj: Atspi.Accessible) -> bool: """Returns true if obj has the transient state""" return AXObject.has_state(obj, Atspi.StateType.TRANSIENT) @staticmethod - def is_truncated(obj): + def is_truncated(obj: Atspi.Accessible) -> bool: """Returns true if obj has the truncated state""" return AXObject.has_state(obj, Atspi.StateType.TRUNCATED) @staticmethod - def is_vertical(obj): + def is_vertical(obj: Atspi.Accessible) -> bool: """Returns true if obj has the vertical state""" return AXObject.has_state(obj, Atspi.StateType.VERTICAL) @staticmethod - def is_visible(obj): + def is_visible(obj: Atspi.Accessible) -> bool: """Returns true if obj has the visible state""" return AXObject.has_state(obj, Atspi.StateType.VISIBLE) @staticmethod - def is_visited(obj): + def is_visited(obj: Atspi.Accessible) -> bool: """Returns true if obj has the visited state""" return AXObject.has_state(obj, Atspi.StateType.VISITED) @staticmethod - def manages_descendants(obj): + def manages_descendants(obj: Atspi.Accessible) -> bool: """Returns true if obj has the manages-descendants state""" return AXObject.has_state(obj, Atspi.StateType.MANAGES_DESCENDANTS) @staticmethod - def supports_autocompletion(obj): + def supports_autocompletion(obj: Atspi.Accessible) -> bool: """Returns true if obj has the supports-autocompletion state""" return AXObject.has_state(obj, Atspi.StateType.SUPPORTS_AUTOCOMPLETION) diff --git a/src/cthulhu/ax_value.py b/src/cthulhu/ax_value.py new file mode 100644 index 0000000..fdce81d --- /dev/null +++ b/src/cthulhu/ax_value.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 Stormux +# Copyright 2024 Igalia, S.L. +# Copyright 2024 GNOME Foundation Inc. +# Author: Joanmarie Diggs +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., Franklin Street, Fifth Floor, +# Boston MA 02110-1301 USA. +# +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu + +# pylint: disable=wrong-import-position + +"""Utilities for obtaining value-related information about accessible objects.""" + +__id__ = "$Id$" +__version__ = "$Revision$" +__date__ = "$Date$" +__copyright__ = "Copyright (c) 2024 Igalia, S.L." \ + "Copyright (c) 2024 GNOME Foundation Inc." +__license__ = "LGPL" + +import threading +import time + +import gi +gi.require_version("Atspi", "2.0") +from gi.repository import Atspi +from gi.repository import GLib + +from . import debug +from .ax_object import AXObject +from .ax_utilities import AXUtilities + +class AXValue: + """Utilities for obtaining value-related information about accessible objects.""" + + LAST_KNOWN_VALUE: dict[int, float] = {} + _lock = threading.Lock() + + @staticmethod + def _clear_stored_data() -> None: + """Clears any data we have cached for objects""" + + while True: + time.sleep(60) + msg = "AXValue: Clearing local cache." + debug.print_message(debug.LEVEL_INFO, msg, True) + AXValue.LAST_KNOWN_VALUE.clear() + + @staticmethod + def start_cache_clearing_thread() -> None: + """Starts thread to periodically clear cached details.""" + + thread = threading.Thread(target=AXValue._clear_stored_data) + thread.daemon = True + thread.start() + + @staticmethod + def did_value_change(obj: Atspi.Accessible) -> bool: + """Returns True if the current value changed.""" + + if not AXObject.supports_value(obj): + return False + + old_value = AXValue.LAST_KNOWN_VALUE.get(hash(obj)) + result = old_value != AXValue._get_current_value(obj) + if result: + tokens = ["AXValue: Previous value of", obj, f"was {old_value}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + return result + + @staticmethod + def _get_current_value(obj: Atspi.Accessible) -> float: + """Returns the current value of obj.""" + + if not AXObject.supports_value(obj): + return 0.0 + + try: + value = Atspi.Value.get_current_value(obj) + except GLib.GError as error: + msg = f"AXValue: Exception in _get_current_value: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return 0.0 + + tokens = ["AXValue: Current value of", obj, f"is {value}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return value + + @staticmethod + def get_current_value(obj: Atspi.Accessible) -> float: + """Returns the current value of obj.""" + + if not AXObject.supports_value(obj): + return 0.0 + + value = AXValue._get_current_value(obj) + AXValue.LAST_KNOWN_VALUE[hash(obj)] = value + return value + + @staticmethod + def get_current_value_text(obj: Atspi.Accessible) -> str: + """Returns the app-provided text-alternative for the current value of obj.""" + + text = AXObject.get_attribute(obj, "valuetext", False) or "" + if text: + tokens = ["AXValue: valuetext attribute for", obj, f"is '{text}'"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return text + + if not AXObject.supports_value(obj): + return "" + + try: + value = Atspi.Value.get_text(obj) + except GLib.GError as error: + msg = f"AXValue: Exception in get_current_value_text: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + value = "" + + tokens = ["AXValue: Value text of", obj, f"is '{value}'"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + if value: + return value + + current = AXValue.get_current_value(obj) + if abs(current) < 1 and current != 0: + str_current = str(current) + decimal_places = len(str_current.split('.')[1]) + else: + decimal_places = 0 + + return f"{current:.{decimal_places}f}" + + @staticmethod + def get_value_as_percent(obj: Atspi.Accessible) -> int | None: + """Returns the current value as a percent, or None if that is not applicable.""" + + if not AXObject.supports_value(obj): + return None + + value = AXValue.get_current_value(obj) + if AXUtilities.is_indeterminate(obj) and value <= 0: + tokens = ["AXValue:", obj, "has state indeterminate"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return None + + minimum = AXValue.get_minimum_value(obj) + maximum = AXValue.get_maximum_value(obj) + if minimum == maximum: + return None + + result = int((value / (maximum - minimum)) * 100) + tokens = ["AXValue: Current value of", obj, f"as percent is is {result}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + @staticmethod + def get_minimum_value(obj: Atspi.Accessible) -> float: + """Returns the minimum value of obj.""" + + if not AXObject.supports_value(obj): + return 0.0 + + try: + value = Atspi.Value.get_minimum_value(obj) + except GLib.GError as error: + msg = f"AXValue: Exception in get_minimum_value: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return 0.0 + + tokens = ["AXValue: Minimum value of", obj, f"is {value}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return value + + @staticmethod + def get_maximum_value(obj: Atspi.Accessible) -> float: + """Returns the maximum value of obj.""" + + if not AXObject.supports_value(obj): + return 0.0 + + try: + value = Atspi.Value.get_maximum_value(obj) + except GLib.GError as error: + msg = f"AXValue: Exception in get_maximum_value: {error}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return 0.0 + + tokens = ["AXValue: Maximum value of", obj, f"is {value}"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return value + +AXValue.start_cache_clearing_thread() diff --git a/src/cthulhu/backends/__init__.py b/src/cthulhu/backends/__init__.py index 782103c..301e5ea 100644 --- a/src/cthulhu/backends/__init__.py +++ b/src/cthulhu/backends/__init__.py @@ -20,6 +20,6 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu diff --git a/src/cthulhu/backends/json_backend.py b/src/cthulhu/backends/json_backend.py index b98a3b3..c887d11 100644 --- a/src/cthulhu/backends/json_backend.py +++ b/src/cthulhu/backends/json_backend.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """JSON backend for Cthulhu settings""" diff --git a/src/cthulhu/bookmarks.py b/src/cthulhu/bookmarks.py index a2c069e..68481d8 100644 --- a/src/cthulhu/bookmarks.py +++ b/src/cthulhu/bookmarks.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Provides the default implementation for bookmarks in Cthulhu.""" diff --git a/src/cthulhu/braille.py b/src/cthulhu/braille.py index aa86f6b..cbaf5d3 100644 --- a/src/cthulhu/braille.py +++ b/src/cthulhu/braille.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """A very experimental approach to the refreshable Braille display. This module treats each line of the display as a sequential set of regions, where @@ -42,6 +42,9 @@ import signal import os import re +import gi +gi.require_version("Atspi", "2.0") +from gi.repository import Atspi from gi.repository import GLib from . import brltablenames @@ -54,6 +57,7 @@ from . import settings_manager from .ax_event_synthesizer import AXEventSynthesizer from .ax_object import AXObject +from .ax_hypertext import AXHypertext from .cthulhu_platform import tablesdir _logger = logger.getLogger() @@ -79,6 +83,11 @@ else: tokens = ["BRAILLE: brlapi imported", brlapi] debug.printTokens(debug.LEVEL_INFO, tokens, True) +BRLAPI_PRIORITY_IDLE = 0 +BRLAPI_PRIORITY_DEFAULT = 50 +BRLAPI_PRIORITY_HIGH = 70 +brlapi_priority = BRLAPI_PRIORITY_DEFAULT + try: msg = "BRAILLE: About to import louis." debug.printMessage(debug.LEVEL_INFO, msg, True) @@ -532,10 +541,11 @@ class Component(Region): if cthulhu_state.activeScript and cthulhu_state.activeScript.utilities.\ grabFocusBeforeRouting(self.accessible, offset): - try: - self.accessible.queryComponent().grabFocus() - except Exception: - pass + if AXObject.supports_component(self.accessible): + try: + Atspi.Component.grab_focus(self.accessible) + except Exception: + pass if AXObject.do_action(self.accessible, 0): return @@ -723,9 +733,7 @@ class Text(Region): unreasonable amount of time (AKA Gecko). """ - try: - self.accessible.queryText() - except NotImplementedError: + if not AXObject.supports_text(self.accessible): return '' # Start with an empty mask. @@ -744,22 +752,18 @@ class Text(Region): return "" if getLinkMask and linkIndicator != settings.BRAILLE_UNDERLINE_NONE: - try: - hyperText = self.accessible.queryHypertext() - nLinks = hyperText.getNLinks() - except Exception: - nLinks = 0 - - n = 0 - while n < nLinks: - link = hyperText.getLink(n) - if self.lineOffset <= link.startIndex: - for i in range(link.startIndex, link.endIndex): - try: - regionMask[i] |= linkIndicator - except Exception: - pass - n += 1 + if AXObject.supports_hypertext(self.accessible): + for link in AXHypertext.get_all_links(self.accessible): + start = AXHypertext.get_link_start_offset(link) + end = AXHypertext.get_link_end_offset(link) + if start < 0 or end < 0: + continue + if self.lineOffset <= start: + for i in range(start, end): + try: + regionMask[i] |= linkIndicator + except Exception: + pass if attrIndicator: keys, enabledAttributes = script.utilities.stringToKeysAndDict( @@ -1157,7 +1161,7 @@ def _idleBraille(): try: msg = "BRAILLE: Attempting to idle braille." debug.printMessage(debug.LEVEL_INFO, msg, True) - _brlAPI.setParameter(brlapi.PARAM_CLIENT_PRIORITY, 0, False, 0) + _brlAPI.setParameter(brlapi.PARAM_CLIENT_PRIORITY, 0, False, BRLAPI_PRIORITY_IDLE) idle = True except Exception: msg = "BRAILLE: Idling braille failled. This requires BrlAPI >= 0.8." @@ -1206,7 +1210,7 @@ def _enableBraille(): # Restore default priority msg = "BRAILLE: Attempting to de-idle braille." debug.printMessage(debug.LEVEL_INFO, msg, True) - _brlAPI.setParameter(brlapi.PARAM_CLIENT_PRIORITY, 0, False, 50) + _brlAPI.setParameter(brlapi.PARAM_CLIENT_PRIORITY, 0, False, BRLAPI_PRIORITY_DEFAULT) idle = False except Exception: msg = "BRAILLE: could not restore priority" @@ -1874,6 +1878,36 @@ def setupKeyRanges(keys): msg = "BRAILLE: Key ranges set up." debug.printMessage(debug.LEVEL_INFO, msg, True) +def setBrlapiPriority(level=BRLAPI_PRIORITY_DEFAULT): + """Set BRLAPI priority. + + Arguments: + - level: the priority level to apply. + """ + + global idle, brlapi_priority + + if not _brlAPIAvailable or not _brlAPIRunning or not settings.enableBraille: + return + + if idle: + msg = "BRAILLE: Braille is idle, don't change BRLAPI priority." + debug.printMessage(debug.LEVEL_INFO, msg, True) + brlapi_priority = level + return + + try: + tokens = ["BRAILLE: Setting priority to:", level] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + _brlAPI.setParameter(brlapi.PARAM_CLIENT_PRIORITY, 0, False, level) + except Exception as error: + msg = f"BRAILLE: Cannot set priority: {error}" + debug.printMessage(debug.LEVEL_WARNING, msg, True) + else: + msg = "BRAILLE: Priority set." + debug.printMessage(debug.LEVEL_INFO, msg, True) + brlapi_priority = level + def init(callback=None): """Initializes the braille module, connecting to the BrlTTY driver. diff --git a/src/cthulhu/braille_generator.py b/src/cthulhu/braille_generator.py index 3306c89..47d4177 100644 --- a/src/cthulhu/braille_generator.py +++ b/src/cthulhu/braille_generator.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Utilities for obtaining braille presentations for objects.""" @@ -44,7 +44,9 @@ from . import cthulhu_state from . import settings from . import settings_manager from .ax_object import AXObject +from .ax_text import AXText from .ax_utilities import AXUtilities +from .ax_utilities_relation import AXUtilitiesRelation from .braille_rolenames import shortRoleNames _settingsManager = settings_manager.getManager() @@ -471,22 +473,16 @@ class BrailleGenerator(generator.Generator): include = _settingsManager.getSetting('enableBrailleContext') if not include: return include - try: - text = obj.queryText() - except NotImplementedError: - text = None - if text and (self._script.utilities.isTextArea(obj) or AXUtilities.is_label(obj)): - try: - [lineString, startOffset, endOffset] = text.getTextAtOffset( - text.caretOffset, Atspi.TextBoundaryType.LINE_START) - except Exception: - return include + if AXObject.supports_text(obj) \ + and (self._script.utilities.isTextArea(obj) or AXUtilities.is_label(obj)): + caretOffset = AXText.get_caret_offset(obj) + lineString, startOffset, endOffset = AXText.get_line_at_offset(obj, caretOffset) include = startOffset == 0 if include: - relation = AXObject.get_relation(obj, Atspi.RelationType.FLOWS_FROM) - if relation: - include = not self._script.utilities.isTextArea(relation.get_target(0)) + flowsFrom = AXUtilitiesRelation.get_flows_from(obj) + if flowsFrom: + include = not self._script.utilities.isTextArea(flowsFrom[0]) return include ##################################################################### diff --git a/src/cthulhu/braille_rolenames.py b/src/cthulhu/braille_rolenames.py index ab1836a..e0b5865 100644 --- a/src/cthulhu/braille_rolenames.py +++ b/src/cthulhu/braille_rolenames.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Dictionary of abbreviated rolenames for use with braille.""" diff --git a/src/cthulhu/brlmon.py b/src/cthulhu/brlmon.py index 6948b04..f1efbb0 100644 --- a/src/cthulhu/brlmon.py +++ b/src/cthulhu/brlmon.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Provides a graphical braille display, mainly for development tasks.""" diff --git a/src/cthulhu/brltablenames.py b/src/cthulhu/brltablenames.py index c46306d..65119c6 100644 --- a/src/cthulhu/brltablenames.py +++ b/src/cthulhu/brltablenames.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Braille translation table names. These have been put in their own module so that we can present them in the correct language when users change the diff --git a/src/cthulhu/caret_navigation.py b/src/cthulhu/caret_navigation.py index 53dc0a3..e84533a 100644 --- a/src/cthulhu/caret_navigation.py +++ b/src/cthulhu/caret_navigation.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Provides an Cthulhu-controlled caret for text content.""" @@ -37,6 +37,7 @@ from . import input_event from . import keybindings from . import messages from . import settings_manager +from .ax_text import AXText class CaretNavigation: @@ -443,7 +444,7 @@ class CaretNavigation: offset = 0 text = script.utilities.queryNonEmptyText(obj) if text: - offset = text.characterCount - 1 + offset = AXText.get_character_count(obj) - 1 while obj: lastobj, lastoffset = script.utilities.nextContext(obj, offset) diff --git a/src/cthulhu/chat.py b/src/cthulhu/chat.py index 52c01ce..1f595a0 100644 --- a/src/cthulhu/chat.py +++ b/src/cthulhu/chat.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Implements generic chat support.""" diff --git a/src/cthulhu/chnames.py b/src/cthulhu/chnames.py index 3da83d5..c191340 100644 --- a/src/cthulhu/chnames.py +++ b/src/cthulhu/chnames.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Provides getCharacterName that maps punctuation marks and other individual characters into localized words.""" diff --git a/src/cthulhu/cmdnames.py b/src/cthulhu/cmdnames.py index 34ee347..7501304 100644 --- a/src/cthulhu/cmdnames.py +++ b/src/cthulhu/cmdnames.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Command names which Cthulhu presents in speech and/or braille. These have been put in their own module so that we can present them in diff --git a/src/cthulhu/colornames.py b/src/cthulhu/colornames.py index c483a85..3a33267 100644 --- a/src/cthulhu/colornames.py +++ b/src/cthulhu/colornames.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" diff --git a/src/cthulhu/common_keyboardmap.py b/src/cthulhu/common_keyboardmap.py index cfc054e..cf79ba5 100644 --- a/src/cthulhu/common_keyboardmap.py +++ b/src/cthulhu/common_keyboardmap.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """ A list of common keybindings and unbound keys pulled out from default.py: getKeyBindings() diff --git a/src/cthulhu/cthulhu-setup.ui b/src/cthulhu/cthulhu-setup.ui index bac0ecf..337935f 100644 --- a/src/cthulhu/cthulhu-setup.ui +++ b/src/cthulhu/cthulhu-setup.ui @@ -1018,6 +1018,105 @@ 2 + + + True + False + 0 + none + + + True + False + 12 + + + True + False + vertical + 6 + + + Play sounds when switching between _browse and focus modes + True + True + False + True + 0 + True + + + + False + True + 0 + + + + + True + False + 12 + + + True + False + 0 + Sound _theme: + True + soundThemeCombo + + + + + + False + True + 0 + + + + + True + False + + + + + + + False + True + 1 + + + + + False + True + 1 + + + + + + + + + True + False + Sound Theme + + + + + + + + 1 + 4 + + diff --git a/src/cthulhu/cthulhu.py b/src/cthulhu/cthulhu.py index aced4eb..e923900 100644 --- a/src/cthulhu/cthulhu.py +++ b/src/cthulhu/cthulhu.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """The main module for the Cthulhu screen reader.""" @@ -211,6 +211,7 @@ from . import learn_mode_presenter from . import logger from . import messages from . import notification_presenter +from . import focus_manager from . import cthulhu_state from . import cthulhu_platform from . import script_manager @@ -250,7 +251,7 @@ def _ensureManagers(): if _eventManager is None: _eventManager = event_manager.getManager() if _scriptManager is None: - _scriptManager = script_manager.getManager() + _scriptManager = script_manager.get_manager() if _settingsManager is None: _settingsManager = settings_manager.getManager() if _logger is None: @@ -300,67 +301,19 @@ OBJECT_NAVIGATOR = "object-navigator" SAY_ALL = "say-all" def getActiveModeAndObjectOfInterest(): - tokens = ["CTHULHU: Active mode:", cthulhu_state.activeMode, - "Object of interest:", cthulhu_state.objOfInterest] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return cthulhu_state.activeMode, cthulhu_state.objOfInterest + return focus_manager.get_manager().get_active_mode_and_object_of_interest() def emitRegionChanged(obj, startOffset=None, endOffset=None, mode=None): """Notifies interested clients that the current region of interest has changed.""" - - if startOffset is None: - startOffset = 0 - if endOffset is None: - endOffset = startOffset - if mode is None: - mode = FOCUS_TRACKING - - try: - obj.emit("mode-changed::" + mode, 1, "") - except Exception: - msg = "CTHULHU: Exception emitting mode-changed notification" - debug.printMessage(debug.LEVEL_INFO, msg, True) - - if mode != cthulhu_state.activeMode: - tokens = ["CTHULHU: Switching active mode from", cthulhu_state.activeMode, "to", mode] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - cthulhu_state.activeMode = mode - - try: - tokens = ["CTHULHU: Region of interest:", obj, "(", startOffset, ")", endOffset] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - obj.emit("region-changed", startOffset, endOffset) - except Exception: - msg = "CTHULHU: Exception emitting region-changed notification" - debug.printMessage(debug.LEVEL_INFO, msg, True) - - if obj != cthulhu_state.objOfInterest: - tokens = ["CTHULHU: Switching object of interest from", cthulhu_state.objOfInterest, "to", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - cthulhu_state.objOfInterest = obj + focus_manager.get_manager().emit_region_changed(obj, startOffset, endOffset, mode) def setActiveWindow(frame, app=None, alsoSetLocusOfFocus=False, notifyScript=False): - tokens = ["CTHULHU: Request to set active window to", frame] - if app is not None: - tokens.extend(["in", app]) - debug.printTokens(debug.LEVEL_INFO, tokens, True) - - if frame == cthulhu_state.activeWindow: - msg = "CTHULHU: Setting activeWindow to existing activeWindow" - debug.printMessage(debug.LEVEL_INFO, msg, True) - elif frame is None: - cthulhu_state.activeWindow = None - else: + real_app = app + real_frame = frame + if frame is not None and hasattr(AXObject, "find_real_app_and_window_for"): real_app, real_frame = AXObject.find_real_app_and_window_for(frame, app) - if real_frame != frame: - tokens = ["CTHULHU: Correcting active window to", real_frame, "in", real_app] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - cthulhu_state.activeWindow = real_frame - else: - cthulhu_state.activeWindow = frame - - if alsoSetLocusOfFocus: - setLocusOfFocus(None, cthulhu_state.activeWindow, notifyScript=notifyScript) + focus_manager.get_manager().set_active_window( + real_frame, real_app, set_window_as_focus=alsoSetLocusOfFocus, notify_script=notifyScript) def setLocusOfFocus(event, obj, notifyScript=True, force=False): """Sets the locus of focus (i.e., the object with visual focus) and @@ -375,51 +328,8 @@ def setLocusOfFocus(event, obj, notifyScript=True, force=False): current locusOfFocus """ - if not force and obj == cthulhu_state.locusOfFocus: - msg = "CTHULHU: Setting locusOfFocus to existing locusOfFocus" - debug.printMessage(debug.LEVEL_INFO, msg, True) - return - - if event and (cthulhu_state.activeScript and not cthulhu_state.activeScript.app): - app = AXObject.get_application(event.source) - script = _scriptManager.getScript(app, event.source) - _scriptManager.setActiveScript(script, "Setting locusOfFocus") - - oldFocus = cthulhu_state.locusOfFocus - if AXObject.is_dead(oldFocus): - oldFocus = None - - if obj is None: - msg = "CTHULHU: New locusOfFocus is null (being cleared)" - debug.printMessage(debug.LEVEL_INFO, msg, True) - cthulhu_state.locusOfFocus = None - return - - if cthulhu_state.activeScript: - tokens = ["CTHULHU: Active script is:", cthulhu_state.activeScript] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - if cthulhu_state.activeScript.utilities.isZombie(obj): - tokens = ["ERROR: New locusOfFocus (", obj, ") is zombie. Not updating."] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return - if AXObject.is_dead(obj): - tokens = ["ERROR: New locusOfFocus (", obj, ") is dead. Not updating."] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return - - tokens = ["CTHULHU: Changing locusOfFocus from", oldFocus, "to", obj, ". Notify:", notifyScript] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - cthulhu_state.locusOfFocus = obj - - if not notifyScript: - return - - if not cthulhu_state.activeScript: - msg = "CTHULHU: Cannot notify active script because there isn't one" - debug.printMessage(debug.LEVEL_INFO, msg, True) - return - - cthulhu_state.activeScript.locusOfFocusChanged(event, oldFocus, cthulhu_state.locusOfFocus) + focus_manager.get_manager().set_locus_of_focus( + event, obj, notify_script=notifyScript, force=force) ######################################################################## # # @@ -477,7 +387,7 @@ def updateKeyMap(keyboardEvent): """Unsupported convenience method to call sad hacks which should go away.""" global _restoreCthulhuKeys - if keyboardEvent.isPressedKey(): + if keyboardEvent.is_pressed_key(): return if keyboardEvent.event_string in settings.cthulhuModifierKeys \ @@ -638,7 +548,7 @@ def loadUserSettings(script=None, inputEvent=None, skipReloadMessage=False): debug.printException(debug.LEVEL_SEVERE) if not script: - script = _scriptManager.getDefaultScript() + script = _scriptManager.get_default_script() _settingsManager.loadAppSettings(script) @@ -754,7 +664,7 @@ def showPreferencesGUI(script=None, inputEvent=None): _ensureManagers() # Initialize managers if not already done prefs = _settingsManager.getGeneralSettings(_settingsManager.profile) - script = _scriptManager.getDefaultScript() + script = _scriptManager.get_default_script() _showPreferencesUI(script, prefs) return True @@ -1094,17 +1004,17 @@ def main(): # setActiveWindow does some corrective work needed thanks to # mutter-x11-frames. So retrieve the window just in case. window = cthulhu_state.activeWindow - script = _scriptManager.getScript(app, window) - _scriptManager.setActiveScript(script, "Launching.") + script = _scriptManager.get_script(app, window) + _scriptManager.set_active_script(script, "Launching.") focusedObject = AXUtilities.get_focused_object(window) tokens = ["CTHULHU: Focused object is:", focusedObject] debug.printTokens(debug.LEVEL_INFO, tokens, True) if focusedObject: setLocusOfFocus(None, focusedObject) - script = _scriptManager.getScript( + script = _scriptManager.get_script( AXObject.get_application(focusedObject), focusedObject) - _scriptManager.setActiveScript(script, "Found focused object.") + _scriptManager.set_active_script(script, "Found focused object.") try: msg = "CTHULHU: Starting ATSPI registry." @@ -1155,7 +1065,7 @@ class Cthulhu(GObject.Object): return self.eventManager def getSettingsManager(self): return self.settingsManager - def getScriptManager(self): + def get_scriptManager(self): return self.scriptManager def getDebugManager(self): return self.debugManager diff --git a/src/cthulhu/cthulhuVersion.py b/src/cthulhu/cthulhuVersion.py index 52b51af..4e83ffc 100644 --- a/src/cthulhu/cthulhuVersion.py +++ b/src/cthulhu/cthulhuVersion.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu -version = "2025.12.12" +version = "2025.12.28" codeName = "master" diff --git a/src/cthulhu/cthulhu_bin.py.in b/src/cthulhu/cthulhu_bin.py.in index 9925fcf..316c8a6 100644 --- a/src/cthulhu/cthulhu_bin.py.in +++ b/src/cthulhu/cthulhu_bin.py.in @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu import argparse import gi diff --git a/src/cthulhu/cthulhu_gtkbuilder.py b/src/cthulhu/cthulhu_gtkbuilder.py index 53c80f8..d676bb7 100644 --- a/src/cthulhu/cthulhu_gtkbuilder.py +++ b/src/cthulhu/cthulhu_gtkbuilder.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Displays a GUI for the user to quit Cthulhu.""" diff --git a/src/cthulhu/cthulhu_gui_find.py b/src/cthulhu/cthulhu_gui_find.py index 2fccdf4..af99110 100644 --- a/src/cthulhu/cthulhu_gui_find.py +++ b/src/cthulhu/cthulhu_gui_find.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Displays a GUI for the Cthulhu Find window""" diff --git a/src/cthulhu/cthulhu_gui_navlist.py b/src/cthulhu/cthulhu_gui_navlist.py index d277573..ac82fff 100644 --- a/src/cthulhu/cthulhu_gui_navlist.py +++ b/src/cthulhu/cthulhu_gui_navlist.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Displays a GUI for Cthulhu navigation list dialogs""" diff --git a/src/cthulhu/cthulhu_gui_prefs.py b/src/cthulhu/cthulhu_gui_prefs.py index 8c7ecf4..be79be9 100644 --- a/src/cthulhu/cthulhu_gui_prefs.py +++ b/src/cthulhu/cthulhu_gui_prefs.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Displays a GUI for the user to set Cthulhu preferences.""" @@ -63,6 +63,7 @@ from . import braille from . import speech from . import speechserver from . import text_attribute_names +from . import sound_theme_manager from .ax_object import AXObject _settingsManager = settings_manager.getManager() @@ -368,6 +369,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): self.get_widget("notebook").append_page(appPage, label) self._initGUIState() + self._initSoundThemeState() def _getACSSForVoiceType(self, voiceType): """Return the ACSS value for the given voice type. @@ -1991,6 +1993,61 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): copyToClipboard = prefs.get("ocrCopyToClipboard", settings.ocrCopyToClipboard) self.ocrCopyToClipboardCheckButton.set_active(copyToClipboard) + def _initSoundThemeState(self): + """Initialize Sound Theme widgets with current settings.""" + prefs = self.prefsDict + + # Get widget references + self.enableModeChangeSoundCheckButton = self.get_widget( + "enableModeChangeSoundCheckButton") + self.soundThemeCombo = self.get_widget("soundThemeCombo") + + # Set enable mode change sound checkbox + enabled = prefs.get("enableModeChangeSound", settings.enableModeChangeSound) + self.enableModeChangeSoundCheckButton.set_active(enabled) + + # Populate sound theme combo box + themeManager = sound_theme_manager.getManager() + availableThemes = themeManager.getAvailableThemes() + + # Clear and populate combo - add "None" as first option + self.soundThemeCombo.remove_all() + self.soundThemeCombo.append_text(sound_theme_manager.THEME_NONE) + for theme in availableThemes: + self.soundThemeCombo.append_text(theme) + + # Build the full list for index lookup + allThemes = [sound_theme_manager.THEME_NONE] + availableThemes + + # Set active theme + currentTheme = prefs.get("soundTheme", settings.soundTheme) + if currentTheme in allThemes: + self.soundThemeCombo.set_active(allThemes.index(currentTheme)) + elif len(allThemes) > 1: + # Default to first actual theme (skip "none") + self.soundThemeCombo.set_active(1) + else: + self.soundThemeCombo.set_active(0) + + # Update sensitivity based on checkbox + self._updateSoundThemeWidgetSensitivity() + + def _updateSoundThemeWidgetSensitivity(self): + """Update sound theme combo sensitivity based on enable checkbox.""" + enabled = self.enableModeChangeSoundCheckButton.get_active() + self.soundThemeCombo.set_sensitive(enabled) + + def enableModeChangeSoundCheckButtonToggled(self, widget): + """Signal handler for the enable mode change sound checkbox.""" + self.prefsDict["enableModeChangeSound"] = widget.get_active() + self._updateSoundThemeWidgetSensitivity() + + def soundThemeComboChanged(self, widget): + """Signal handler for the sound theme combo box.""" + activeText = widget.get_active_text() + if activeText: + self.prefsDict["soundTheme"] = activeText + def _updateCthulhuModifier(self): combobox = self.get_widget("cthulhuModifierComboBox") keystring = ", ".join(self.prefsDict["cthulhuModifierKeys"]) @@ -2361,7 +2418,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper): svKeyBindings = self.script.getSpeechAndVerbosityManager().get_bindings() dtKeyBindings = self.script.getDateAndTimePresenter().get_bindings() bmKeyBindings = self.script.getBookmarks().get_bindings() - onKeyBindings = self.script.getObjectNavigator().get_bindings() + onKeyBindings = self.script.get_objectNavigator().get_bindings() lmKeyBindings = self.script.getLearnModePresenter().get_bindings() mrKeyBindings = self.script.getMouseReviewer().get_bindings() acKeyBindings = self.script.getActionPresenter().get_bindings() diff --git a/src/cthulhu/cthulhu_gui_profile.py b/src/cthulhu/cthulhu_gui_profile.py index 2355bb0..b0edb9c 100644 --- a/src/cthulhu/cthulhu_gui_profile.py +++ b/src/cthulhu/cthulhu_gui_profile.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Displays the Save Profile As dialog.""" diff --git a/src/cthulhu/cthulhu_state.py b/src/cthulhu/cthulhu_state.py index a4b547e..e2dee10 100644 --- a/src/cthulhu/cthulhu_state.py +++ b/src/cthulhu/cthulhu_state.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Holds state that is shared among many modules. """ diff --git a/src/cthulhu/date_and_time_presenter.py b/src/cthulhu/date_and_time_presenter.py index b8df145..a4a2bf9 100644 --- a/src/cthulhu/date_and_time_presenter.py +++ b/src/cthulhu/date_and_time_presenter.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Module for date and time presentation""" diff --git a/src/cthulhu/dbus_service.py b/src/cthulhu/dbus_service.py index a32dedc..daf51b3 100644 --- a/src/cthulhu/dbus_service.py +++ b/src/cthulhu/dbus_service.py @@ -48,11 +48,15 @@ from . import cthulhu_platform # pylint: disable=no-name-in-module from . import script_manager from . import cthulhu_state -# Lazy import to avoid circular dependency +# Lazy imports to avoid circular dependency def _get_input_event(): from . import input_event return input_event +def _get_input_event_manager(): + from . import input_event_manager + return input_event_manager + class HandlerType(enum.Enum): """Enumeration of handler types for D-Bus methods.""" @@ -451,8 +455,8 @@ if _dasbus_available: msg = "DBUS SERVICE: ShowPreferences called." debug.printMessage(debug.LEVEL_INFO, msg, True) - manager = script_manager.getManager() - script = cthulhu_state.activeScript or manager.getDefaultScript() + manager = script_manager.get_manager() + script = cthulhu_state.activeScript or manager.get_default_script() if script is None: msg = "DBUS SERVICE: No script available" debug.printMessage(debug.LEVEL_WARNING, msg, True) @@ -467,8 +471,8 @@ if _dasbus_available: msg = f"DBUS SERVICE: PresentMessage called with: '{message}'" debug.printMessage(debug.LEVEL_INFO, msg, True) - manager = script_manager.getManager() - script = cthulhu_state.activeScript or script_manager.getManager().getDefaultScript() + manager = script_manager.get_manager() + script = cthulhu_state.activeScript or script_manager.get_manager().get_default_script() if script is None: msg = "DBUS SERVICE: No script available" debug.printMessage(debug.LEVEL_WARNING, msg, True) @@ -670,7 +674,12 @@ class CthulhuRemoteController: def _wrapper(notify_user): event = _get_input_event().RemoteControllerEvent() script = cthulhu_state.activeScript - return method(script=script, event=event, notify_user=notify_user) + if script is None: + manager = script_manager.get_manager() + script = manager.get_default_script() + rv = method(script=script, event=event, notify_user=notify_user) + _get_input_event_manager().get_manager().process_remote_controller_event(event) + return rv return _wrapper handler_info = _HandlerInfo( python_function_name=attr_name, @@ -690,9 +699,11 @@ class CthulhuRemoteController: event = _get_input_event().RemoteControllerEvent() script = cthulhu_state.activeScript if script is None: - manager = script_manager.getManager() - script = manager.getDefaultScript() - return method(script=script, event=event, **kwargs) + manager = script_manager.get_manager() + script = manager.get_default_script() + rv = method(script=script, event=event, **kwargs) + _get_input_event_manager().get_manager().process_remote_controller_event(event) + return rv return _wrapper handler_info = _HandlerInfo( python_function_name=attr_name, @@ -859,4 +870,4 @@ _remote_controller: CthulhuRemoteController = CthulhuRemoteController() def get_remote_controller() -> CthulhuRemoteController: """Returns the CthulhuRemoteController singleton.""" - return _remote_controller \ No newline at end of file + return _remote_controller diff --git a/src/cthulhu/debug.py b/src/cthulhu/debug.py index 47a2566..e58d789 100644 --- a/src/cthulhu/debug.py +++ b/src/cthulhu/debug.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Provides debug utilities for Cthulhu. Debugging is managed by a debug level, which is held in the debugLevel field. All other methods take @@ -48,8 +48,14 @@ import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi -from .ax_object import AXObject -from .ax_utilities import AXUtilities +AXObject = None + +def _get_ax_object(): + global AXObject + if AXObject is None: + from .ax_object import AXObject as ax_object + AXObject = ax_object + return AXObject # Used to turn off all debugging. # @@ -210,6 +216,7 @@ def printStack(level): println(level) def _asString(obj): + AXObject = _get_ax_object() if isinstance(obj, Atspi.Accessible): result = AXObject.get_role_name(obj) name = AXObject.get_name(obj) @@ -257,12 +264,18 @@ def printTokens(level, tokens, timestamp=False, stack=False): text = re.sub(r" (?=[,.:)])(?![\n])", "", text) println(level, text, timestamp, stack) +def print_tokens(level, tokens, timestamp=False, stack=False): + return printTokens(level, tokens, timestamp, stack) + def printMessage(level, text, timestamp=False, stack=False): if level < debugLevel: return println(level, text, timestamp, stack) +def print_message(level, text, timestamp=False, stack=False): + return printMessage(level, text, timestamp, stack) + def _stackAsString(max_frames=4): callers = [] current_module = inspect.getmodule(inspect.currentframe()) @@ -412,30 +425,8 @@ def getAccessibleDetails(level, acc, indent="", includeApp=True): if level < debugLevel: return "" - if includeApp: - string = indent + f"app='{AXObject.application_as_string(acc)}' " - else: - string = indent - - if AXObject.is_dead(acc): - string += "(exception fetching data)" - return string - - name_string = "name='%s'".replace("\n", "\\n") % AXObject.get_name(acc) - desc_string = "%sdescription='%s'".replace("\n", "\\n") % \ - (indent, AXObject.get_description(acc)) - role_string = f"role='{AXObject.get_role_name(acc)}'" - path_string = f"{indent}path={AXObject.get_path(acc)}" - state_string = f"{indent}states='{AXObject.state_set_as_string(acc)}'" - rel_string = f"{indent}relations='{AXObject.relations_as_string(acc)}'" - actions_string = f"{indent}actions='{AXObject.actions_as_string(acc)}'" - iface_string = f"{indent}interfaces='{AXObject.supported_interfaces_as_string(acc)}'" - attr_string = f"{indent}attributes='{AXObject.attributes_as_string(acc)}'" - - string += "%s %s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n" \ - % (name_string, role_string, desc_string, state_string, rel_string, - actions_string, iface_string, attr_string, path_string) - return string + from .ax_utilities_debugging import AXUtilitiesDebugging + return AXUtilitiesDebugging.object_details_as_string(acc, indent, includeApp) # The following code originated from the following URL: # @@ -458,6 +449,7 @@ def _getFileAndModule(frame): return filename, module def _shouldTraceIt(): + AXObject = _get_ax_object() if not objEvent: return not TRACE_ONLY_PROCESSING_EVENTS @@ -555,6 +547,8 @@ def pidOf(procName): return [int(p) for p in pids.split()] def examineProcesses(force=False): + AXObject = _get_ax_object() + from .ax_utilities import AXUtilities if force: level = LEVEL_OFF else: diff --git a/src/cthulhu/desktop_keyboardmap.py b/src/cthulhu/desktop_keyboardmap.py index f447dde..1ce694c 100644 --- a/src/cthulhu/desktop_keyboardmap.py +++ b/src/cthulhu/desktop_keyboardmap.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """ A list of common keybindings and unbound keys pulled out from default.py: __getDesktopBindings() diff --git a/src/cthulhu/dynamic_api_manager.py b/src/cthulhu/dynamic_api_manager.py index e377f75..e9a7bb3 100644 --- a/src/cthulhu/dynamic_api_manager.py +++ b/src/cthulhu/dynamic_api_manager.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu import gi from gi.repository import GObject diff --git a/src/cthulhu/event_manager.py b/src/cthulhu/event_manager.py index 8f2db5d..35fc16a 100644 --- a/src/cthulhu/event_manager.py +++ b/src/cthulhu/event_manager.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" @@ -30,10 +30,9 @@ __copyright__ = "Copyright (c) 2011. Cthulhu Team." __license__ = "LGPL" import gi -gi.require_version('Atspi', '2.0') +gi.require_version('Atspi', '2.0') from gi.repository import Atspi from gi.repository import GLib -import pyatspi import queue import threading import time @@ -47,7 +46,7 @@ from . import settings from .ax_object import AXObject from .ax_utilities import AXUtilities -_scriptManager = script_manager.getManager() +_scriptManager = script_manager.get_manager() class EventManager: @@ -87,9 +86,8 @@ class EventManager: self._parentsOfDefunctDescendants = [] cthulhu_state.device = None - self.newKeyHandlingActive = False - self.legacyKeyHandlingActive = False - self.forceLegacyKeyHandling = False + self._keyHandlingActive = False + self._inputEventManager = None debug.printMessage(debug.LEVEL_INFO, 'Event manager initialized', True) @@ -97,49 +95,46 @@ class EventManager: """Called when this event manager is activated.""" debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Activating', True) - self.setKeyHandling(True) # Enable new InputEventManager for global keyboard capture - + self._activateKeyHandling() self._active = True debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Activated', True) - def activateNewKeyHandling(self): - if not self.newKeyHandlingActive: - try: - debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Attempting to activate new keyboard handling', True) - # Use the new InputEventManager instead of direct Atspi.Device - self._inputEventManager = input_event_manager.getManager() - debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: InputEventManager obtained', True) - self._inputEventManager.start_key_watcher() - debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Key watcher started', True) - cthulhu_state.device = self._inputEventManager._device # For compatibility - except Exception as e: - debug.printMessage(debug.LEVEL_INFO, f'EVENT MANAGER: New keyboard handling failed: {e}', True) - self.forceLegacyKeyHandling = True - self.activateLegacyKeyHandling() - return - - self.newKeyHandlingActive = True - debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: New keyboard handling activated with InputEventManager', True) - - # Notify plugin system that device is now available for keybinding registration - from . import cthulhu - if hasattr(cthulhu, 'cthulhuApp') and cthulhu.cthulhuApp: - plugin_manager = cthulhu.cthulhuApp.getPluginSystemManager() - if plugin_manager: - pass # plugin_manager.register_plugin_keybindings_with_active_script() + def _activateKeyHandling(self): + """Activates keyboard handling using InputEventManager with Atspi.Device.""" - def activateLegacyKeyHandling(self): - if not self.legacyKeyHandlingActive: - self.registerKeystrokeListener(self._processKeyboardEvent) - self.legacyKeyHandlingActive = True + if self._keyHandlingActive: + return - def setKeyHandling(self, new): - if new and not self.forceLegacyKeyHandling: - self.deactivateLegacyKeyHandling() - self.activateNewKeyHandling() + debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Activating keyboard handling', True) + self._inputEventManager = input_event_manager.get_manager() + self._inputEventManager.start_key_watcher() + cthulhu_state.device = self._inputEventManager._device + self._keyHandlingActive = True + debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Keyboard handling activated', True) + + def _deactivateKeyHandling(self): + """Deactivates keyboard handling.""" + + if not self._keyHandlingActive: + return + + debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Deactivating keyboard handling', True) + if self._inputEventManager: + self._inputEventManager.stop_key_watcher() + cthulhu_state.device = None + self._keyHandlingActive = False + debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Keyboard handling deactivated', True) + + def setKeyHandling(self, enable): + """Enables or disables keyboard handling. + + Arguments: + - enable: if True, activate keyboard handling; if False, deactivate. + """ + if enable: + self._activateKeyHandling() else: - self.deactivateNewKeyHandling() - self.activateLegacyKeyHandling() + self._deactivateKeyHandling() def deactivate(self): """Called when this event manager is deactivated.""" @@ -148,22 +143,9 @@ class EventManager: self._active = False self._eventQueue = queue.Queue(0) self._scriptListenerCounts = {} - self.deactivateLegacyKeyHandling() + self._deactivateKeyHandling() debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Deactivated', True) - def deactivateNewKeyHandling(self): - if self.newKeyHandlingActive: - if hasattr(self, '_inputEventManager'): - self._inputEventManager.stop_key_watcher() - self._inputEventManager = None - cthulhu_state.device = None - self.newKeyHandlingActive = False - - def deactivateLegacyKeyHandling(self): - if self.legacyKeyHandlingActive: - self.deregisterKeystrokeListener(self._processKeyboardEvent) - self.legacyKeyHandlingActive = False - def ignoreEventTypes(self, eventTypeList): for eventType in eventTypeList: if eventType not in self._ignoredEvents: @@ -524,7 +506,7 @@ class EventManager: def _shouldSuspendEventsFor(self, event): if AXUtilities.is_frame(event.source) \ or (AXUtilities.is_window(event.source) \ - and AXObject.get_application_toolkit_name(event.source) == "clutter"): + and AXUtilities.get_application_toolkit_name(event.source) == "clutter"): if event.type.startswith("window"): msg = "EVENT MANAGER: Should suspend events for window event." debug.printMessage(debug.LEVEL_INFO, msg, True) @@ -603,7 +585,7 @@ class EventManager: if isObjectEvent: if isinstance(e, input_event.MouseButtonEvent): asyncMode = True - elif AXObject.get_application_toolkit_name(e.source) in self._synchronousToolkits: + elif AXUtilities.get_application_toolkit_name(e.source) in self._synchronousToolkits: asyncMode = False elif e.type.startswith("object:children-changed"): asyncMode = AXUtilities.is_table(e.source) @@ -611,7 +593,7 @@ class EventManager: # To decrease the likelihood that the popup will be destroyed before we # have its contents. asyncMode = False - script = _scriptManager.getScript(AXObject.get_application(e.source), e.source) + script = _scriptManager.get_script(AXObject.get_application(e.source), e.source) script.eventCache[e.type] = (e, time.time()) self._addToQueue(e, asyncMode) @@ -633,8 +615,8 @@ class EventManager: if not self._isNoFocus(): return False - defaultScript = _scriptManager.getDefaultScript() - _scriptManager.setActiveScript(defaultScript, 'No focus') + defaultScript = _scriptManager.get_default_script() + _scriptManager.set_active_script(defaultScript, 'No focus') defaultScript.idleMessage() return False @@ -764,34 +746,6 @@ class EventManager: for eventType in script.listeners.keys(): self.deregisterListener(eventType) - def registerKeystrokeListener(self, function, mask=None, kind=None): - """Register the keystroke listener on behalf of the caller.""" - - tokens = ["EVENT MANAGER: Registering keystroke listener function:", function] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - - if mask is None: - mask = list(range(256)) - - if kind is None: - kind = (Atspi.EventType.KEY_PRESSED_EVENT, Atspi.EventType.KEY_RELEASED_EVENT) - - pyatspi.Registry.registerKeystrokeListener(function, mask=mask, kind=kind) - - def deregisterKeystrokeListener(self, function, mask=None, kind=None): - """Deregister the keystroke listener on behalf of the caller.""" - - tokens = ["EVENT MANAGER: De-registering keystroke listener function:", function] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - - if mask is None: - mask = list(range(256)) - - if kind is None: - kind = (Atspi.EventType.KEY_PRESSED_EVENT, Atspi.EventType.KEY_RELEASED_EVENT) - - pyatspi.Registry.deregisterKeystrokeListener(function, mask=mask, kind=kind) - def _processInputEvent(self, event): """Processes the given input event based on the keybinding from the currently-active script. @@ -826,11 +780,11 @@ class EventManager: debug.printMessage(debug.eventDebugLevel, msg, False) @staticmethod - def _getScriptForEvent(event): + def _get_scriptForEvent(event): """Returns the script associated with event.""" if event.type.startswith("mouse:"): - return _scriptManager.getScriptForMouseButtonEvent(event) + return _scriptManager.get_script_for_mouse_button_event(event) script = None app = AXObject.get_application(event.source) @@ -859,7 +813,7 @@ class EventManager: tokens = ["EVENT MANAGER: Getting script for", app, "check:", check] debug.printTokens(debug.LEVEL_INFO, tokens, True) - script = _scriptManager.getScript(app, event.source, sanityCheck=check) + script = _scriptManager.get_script(app, event.source, sanity_check=check) tokens = ["EVENT MANAGER: Script is ", script] debug.printTokens(debug.LEVEL_INFO, tokens, True) return script @@ -877,7 +831,7 @@ class EventManager: return False, "event.source? What event.source??" if not script: - script = self._getScriptForEvent(event) + script = self._get_scriptForEvent(event) if not script: return False, "There is no script for this event." @@ -1065,14 +1019,14 @@ class EventManager: if eType.startswith("object:children-changed:remove") \ and event.source == AXUtilities.get_desktop(): - _scriptManager.reclaimScripts() + _scriptManager.reclaim_scripts() return if eType.startswith("window:") and not eType.endswith("create"): - _scriptManager.reclaimScripts() + _scriptManager.reclaim_scripts() elif eType.startswith("object:state-changed:active") \ and AXUtilities.is_frame(event.source): - _scriptManager.reclaimScripts() + _scriptManager.reclaim_scripts() if AXObject.is_dead(event.source) or AXUtilities.is_defunct(event.source): tokens = ["EVENT MANAGER: Ignoring defunct object:", event.source] @@ -1084,7 +1038,7 @@ class EventManager: debug.printMessage(debug.LEVEL_INFO, msg, True) cthulhu_state.locusOfFocus = None cthulhu_state.activeWindow = None - _scriptManager.setActiveScript(None, "Active window is dead or defunct") + _scriptManager.set_active_script(None, "Active window is dead or defunct") return if AXUtilities.is_iconified(event.source): @@ -1116,7 +1070,7 @@ class EventManager: debug.printMessage(debug.LEVEL_INFO, f"{indent}ANY DATA:") debug.printDetails(debug.LEVEL_INFO, indent, event.any_data, includeApp=False) - script = self._getScriptForEvent(event) + script = self._get_scriptForEvent(event) if not script: msg = "ERROR: Could not get script for event" debug.printMessage(debug.LEVEL_INFO, msg, True) @@ -1128,7 +1082,7 @@ class EventManager: if setNewActiveScript: try: - _scriptManager.setActiveScript(script, reason) + _scriptManager.set_active_script(script, reason) except Exception as error: tokens = ["EVENT MANAGER: Exception setting active script for", event.source, ":", error] @@ -1152,63 +1106,6 @@ class EventManager: msg = f"EVENT MANAGER: {key}: {value}" debug.printMessage(debug.LEVEL_INFO, msg, True) - def _processNewKeyboardEvent(self, device, pressed, keycode, keysym, state, text): - """Process keyboard event using new direct KeyboardEvent creation.""" - - if not pressed and text == "Num_Lock" and "KP_Insert" in settings.cthulhuModifierKeys \ - and cthulhu_state.activeScript is not None: - cthulhu_state.activeScript.refreshKeyGrabs() - - if pressed: - cthulhu_state.openingDialog = (text == "space" \ - and (state & ~(1 << Atspi.ModifierType.NUMLOCK))) - - # Create KeyboardEvent directly with new constructor - keyboardEvent = input_event.KeyboardEvent(pressed, keycode, keysym, state, text or "") - - # Set context information - keyboardEvent.setWindow(cthulhu_state.activeWindow) - keyboardEvent.setObject(cthulhu_state.locusOfFocus) - keyboardEvent.setScript(cthulhu_state.activeScript) - - # Finalize initialization now that context is set - keyboardEvent._finalize_initialization() - - if not keyboardEvent.is_duplicate: - debug.printMessage(debug.LEVEL_INFO, f"\n{keyboardEvent}") - - rv = keyboardEvent.process() - - # Do any needed xmodmap crap. Hopefully this can die soon. - from cthulhu import cthulhu - cthulhu.updateKeyMap(keyboardEvent) - - return rv - - def _processKeyboardEvent(self, event): - # Convert AT-SPI event to new KeyboardEvent format - pressed = event.type == Atspi.EventType.KEY_PRESSED_EVENT - keyboardEvent = input_event.KeyboardEvent(pressed, event.hw_code, event.id, event.modifiers, event.event_string or "") - - # Set context information - keyboardEvent.setWindow(cthulhu_state.activeWindow) - keyboardEvent.setObject(cthulhu_state.locusOfFocus) - keyboardEvent.setScript(cthulhu_state.activeScript) - - # Finalize initialization - keyboardEvent._finalize_initialization() - - if not keyboardEvent.is_duplicate: - debug.printMessage(debug.LEVEL_INFO, f"\n{keyboardEvent}") - - rv = keyboardEvent.process() - - # Do any needed xmodmap crap. Hopefully this can die soon. - from cthulhu import cthulhu - cthulhu.updateKeyMap(keyboardEvent) - - return rv - def processBrailleEvent(self, brailleEvent): """Called whenever a cursor key is pressed on the Braille display. diff --git a/src/cthulhu/find.py b/src/cthulhu/find.py index 5e567a1..7ad7e09 100644 --- a/src/cthulhu/find.py +++ b/src/cthulhu/find.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Provides support for a flat review find.""" diff --git a/src/cthulhu/flat_review.py b/src/cthulhu/flat_review.py index aecc0ce..effc082 100644 --- a/src/cthulhu/flat_review.py +++ b/src/cthulhu/flat_review.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Provides the default implementation for flat review for Cthulhu.""" @@ -43,7 +43,9 @@ from . import cthulhu from . import cthulhu_state from . import settings from .ax_event_synthesizer import AXEventSynthesizer +from .ax_component import AXComponent from .ax_object import AXObject +from .ax_text import AXText from .ax_utilities import AXUtilities EMBEDDED_OBJECT_CHARACTER = '\ufffc' @@ -113,17 +115,14 @@ class Word: # The main goal is to improve reviewability. extents = self.x, self.y, self.width, self.height - try: - text = self.zone.accessible.queryText() - except Exception: - text = None - chars = [] for i, char in enumerate(self.string): start = i + self.startOffset - if text: + if AXObject.supports_text(self.zone.accessible): try: - extents = text.getRangeExtents(start, start+1, Atspi.CoordType.SCREEN) + rect = Atspi.Text.get_range_extents( + self.zone.accessible, start, start + 1, Atspi.CoordType.SCREEN) + extents = rect.x, rect.y, rect.width, rect.height except Exception as error: tokens = ["FLAT REVIEW: Exception in getRangeExtents:", error] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -281,7 +280,7 @@ class TextZone(Zone): self.startOffset = startOffset self.endOffset = self.startOffset + len(string) - self._itext = self.accessible.queryText() + self._itext = None def __getattribute__(self, attr): """To ensure we update the content.""" @@ -289,11 +288,15 @@ class TextZone(Zone): if attr not in ["words", "string"]: return super().__getattribute__(attr) - string = self._itext.getText(self.startOffset, self.endOffset) + string = AXText.get_substring(self.accessible, self.startOffset, self.endOffset) words = [] for i, word in enumerate(re.finditer(self.WORDS_RE, string)): start, end = map(lambda x: x + self.startOffset, word.span()) - extents = self._itext.getRangeExtents(start, end, Atspi.CoordType.SCREEN) + try: + rect = Atspi.Text.get_range_extents(self.accessible, start, end, Atspi.CoordType.SCREEN) + extents = rect.x, rect.y, rect.width, rect.height + except Exception: + extents = (self.x, self.y, self.width, self.height) words.append(Word(self, i, start, word.group(), *extents)) self._string = string @@ -303,11 +306,11 @@ class TextZone(Zone): def hasCaret(self): """Returns True if this Zone contains the caret.""" - offset = self._itext.caretOffset + offset = AXText.get_caret_offset(self.accessible) if self.startOffset <= offset < self.endOffset: return True - return self.endOffset == self._itext.characterCount + return self.endOffset == AXText.get_character_count(self.accessible) def wordWithCaret(self): """Returns the Word and relative offset with the caret.""" @@ -315,7 +318,7 @@ class TextZone(Zone): if not self.hasCaret(): return None, -1 - return self.getWordAtOffset(self._itext.caretOffset) + return self.getWordAtOffset(AXText.get_caret_offset(self.accessible)) class StateZone(Zone): @@ -497,7 +500,7 @@ class Context: self.container = None self.focusObj = cthulhu.getActiveModeAndObjectOfInterest()[1] or cthulhu_state.locusOfFocus self.topLevel = None - self.bounds = 0, 0, 0, 0 + self.bounds = Atspi.Rect() frame, dialog = script.utilities.frameAndDialog(self.focusObj) if root is not None: @@ -510,8 +513,7 @@ class Context: debug.printTokens(debug.LEVEL_INFO, tokens, True) try: - component = self.topLevel.queryComponent() - self.bounds = component.getExtents(Atspi.CoordType.SCREEN) + self.bounds = AXComponent.get_rect(self.topLevel) except Exception: tokens = ["ERROR: Exception getting extents of", self.topLevel] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -563,11 +565,16 @@ class Context: Returns a list of Zones for the visible text. """ + cliprect = self._ensureRect(cliprect) zones = [] substrings = [(*m.span(), m.group(0)) for m in re.finditer(r"[^\ufffc]+", string)] substrings = list(map(lambda x: (x[0] + startOffset, x[1] + startOffset, x[2]), substrings)) for (start, end, substring) in substrings: - extents = accessible.queryText().getRangeExtents(start, end, Atspi.CoordType.SCREEN) + try: + rect = Atspi.Text.get_range_extents(accessible, start, end, Atspi.CoordType.SCREEN) + extents = rect.x, rect.y, rect.width, rect.height + except Exception: + extents = (0, 0, 0, 0) if self.script.utilities.containsRegion(extents, cliprect): clipping = self.script.utilities.intersection(extents, cliprect) zones.append(TextZone(accessible, start, substring, *clipping)) @@ -577,18 +584,17 @@ class Context: def _getLines(self, accessible, startOffset, endOffset): # TODO - JD: Move this into the script utilities so we can better handle # app and toolkit quirks and also reuse this (e.g. for SayAll). - try: - text = accessible.queryText() - except NotImplementedError: + if not AXObject.supports_text(accessible): return [] lines = [] offset = startOffset - while offset < min(endOffset, text.characterCount): - result = text.getTextAtOffset(offset, Atspi.TextBoundaryType.LINE_START) - if result[0] and result not in lines: - lines.append(result) - offset = max(result[2], offset + 1) + maxOffset = min(endOffset, AXText.get_character_count(accessible)) + while offset < maxOffset: + line, start, end = AXText.get_line_at_offset(accessible, offset) + if line and (line, start, end) not in lines: + lines.append((line, start, end)) + offset = max(end, offset + 1) return lines @@ -603,20 +609,21 @@ class Context: Returns a list of Zones. """ + cliprect = self._ensureRect(cliprect) if not self.script.utilities.hasPresentableText(accessible): return [] zones = [] - text = accessible.queryText() # TODO - JD: This is here temporarily whilst I sort out the rest # of the text-related mess. if AXObject.supports_editable_text(accessible) \ and AXUtilities.is_single_line(accessible): - extents = accessible.queryComponent().getExtents(0) - return [TextZone(accessible, 0, text.getText(0, -1), *extents)] + rect = AXComponent.get_rect(accessible) + return [TextZone(accessible, 0, AXText.get_substring(accessible, 0, -1), + rect.x, rect.y, rect.width, rect.height)] - upperMax = lowerMax = text.characterCount + upperMax = lowerMax = AXText.get_character_count(accessible) upperMid = lowerMid = int(upperMax / 2) upperMin = lowerMin = 0 oldMid = 0 @@ -624,9 +631,9 @@ class Context: # performing binary search to locate first line inside clipped area while oldMid != upperMid: oldMid = upperMid - [x, y, width, height] = text.getRangeExtents(upperMid, - upperMid+1, - 0) + rect = Atspi.Text.get_range_extents(accessible, upperMid, upperMid + 1, + Atspi.CoordType.SCREEN) + x, y, width, height = rect.x, rect.y, rect.width, rect.height if y > cliprect.y: upperMax = upperMid else: @@ -638,9 +645,9 @@ class Context: limit = cliprect.y+cliprect.height while oldMid != lowerMid: oldMid = lowerMid - [x, y, width, height] = text.getRangeExtents(lowerMid, - lowerMid+1, - 0) + rect = Atspi.Text.get_range_extents(accessible, lowerMid, lowerMid + 1, + Atspi.CoordType.SCREEN) + x, y, width, height = rect.x, rect.y, rect.width, rect.height if y > limit: lowerMax = lowerMid else: @@ -667,6 +674,7 @@ class Context: # TODO - JD: This whole thing is pretty hacky. Either do it # right or nuke it. + extents = self._ensureRect(extents) indicatorExtents = [extents.x, extents.y, 1, extents.height] role = AXObject.get_role(accessible) if role == Atspi.Role.TOGGLE_BUTTON: @@ -709,9 +717,13 @@ class Context: def getZonesFromAccessible(self, accessible, cliprect): """Returns a list of Zones for the given accessible.""" + cliprect = self._ensureRect(cliprect) try: - component = accessible.queryComponent() - extents = component.getExtents(Atspi.CoordType.SCREEN) + if AXObject.supports_component(accessible): + rect = Atspi.Component.get_extents(accessible, Atspi.CoordType.SCREEN) + extents = rect.x, rect.y, rect.width, rect.height + else: + return [] except Exception: return [] @@ -750,6 +762,25 @@ class Context: return AXObject.find_ancestor(child, lambda x: x == parent) + @staticmethod + def _ensureRect(rect): + if rect is None: + return Atspi.Rect() + if hasattr(rect, "x") and hasattr(rect, "y") \ + and hasattr(rect, "width") and hasattr(rect, "height"): + return rect + + try: + x, y, width, height = rect + except Exception: + return Atspi.Rect() + + newRect = Atspi.Rect() + newRect.x = x + newRect.y = y + newRect.width = width + newRect.height = height + return newRect def setCurrentToZoneWithObject(self, obj): """Attempts to set the current zone to obj, if obj is in the current context.""" @@ -812,6 +843,7 @@ class Context: if boundingbox is None: boundingbox = self.bounds + boundingbox = self._ensureRect(boundingbox) objs = self.script.utilities.getOnScreenObjects(root, boundingbox) tokens = ["FLAT REVIEW:", len(objs), "on-screen objects found for", root] diff --git a/src/cthulhu/flat_review_presenter.py b/src/cthulhu/flat_review_presenter.py index aff7c45..ea29ac1 100644 --- a/src/cthulhu/flat_review_presenter.py +++ b/src/cthulhu/flat_review_presenter.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Module for flat-review commands""" diff --git a/src/cthulhu/focus_manager.py b/src/cthulhu/focus_manager.py new file mode 100644 index 0000000..600eaa1 --- /dev/null +++ b/src/cthulhu/focus_manager.py @@ -0,0 +1,469 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 Stormux +# Copyright 2005-2008 Sun Microsystems Inc. +# Copyright 2016-2023 Igalia, S.L. +# +# Author: Joanmarie Diggs +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., Franklin Street, Fifth Floor, +# Boston MA 02110-1301 USA. +# +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu + +# pylint: disable=wrong-import-position +# pylint: disable=too-many-instance-attributes +# pylint: disable=too-many-public-methods + +"""Module to manage the focused object, window, etc.""" + +# This has to be the first non-docstring line in the module to make linters happy. +from __future__ import annotations + +__id__ = "$Id$" +__version__ = "$Revision$" +__date__ = "$Date$" +__copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." \ + "Copyright (c) 2016-2023 Igalia, S.L." +__license__ = "LGPL" + +from typing import TYPE_CHECKING + +import gi +gi.require_version("Atspi", "2.0") +from gi.repository import Atspi + +from . import braille +from . import cthulhu_state +from . import dbus_service +from . import debug +from . import script_manager +from .ax_object import AXObject +from .ax_table import AXTable +from .ax_text import AXText +def _get_ax_utilities(): + # Avoid circular import with ax_utilities -> ax_utilities_event -> focus_manager. + from .ax_utilities import AXUtilities + return AXUtilities + +if TYPE_CHECKING: + from .input_event import InputEvent + from .scripts import default + +CARET_TRACKING = "caret-tracking" +FOCUS_TRACKING = "focus-tracking" +FLAT_REVIEW = "flat-review" +MOUSE_REVIEW = "mouse-review" +OBJECT_NAVIGATOR = "object-navigator" +SAY_ALL = "say-all" + + +class FocusManager: + """Manages the focused object, window, etc.""" + + def __init__(self) -> None: + self._window: Atspi.Accessible | None = cthulhu_state.activeWindow + self._focus: Atspi.Accessible | None = cthulhu_state.locusOfFocus + self._object_of_interest: Atspi.Accessible | None = cthulhu_state.objOfInterest + self._active_mode: str | None = cthulhu_state.activeMode + self._last_cell_coordinates: tuple[int, int] = (-1, -1) + self._last_cursor_position: tuple[Atspi.Accessible | None, int] = (None, -1) + self._penultimate_cursor_position: tuple[Atspi.Accessible | None, int] = (None, -1) + + msg = "FOCUS MANAGER: Registering D-Bus commands." + debug.print_message(debug.LEVEL_INFO, msg, True) + controller = dbus_service.get_remote_controller() + controller.register_decorated_module("FocusManager", self) + + def clear_state(self, reason: str = "") -> None: + """Clears everything we're tracking.""" + + msg = "FOCUS MANAGER: Clearing all state" + if reason: + msg += f": {reason}" + debug.print_message(debug.LEVEL_INFO, msg, True) + self._focus = None + self._window = None + self._object_of_interest = None + self._active_mode = None + cthulhu_state.locusOfFocus = None + cthulhu_state.activeWindow = None + cthulhu_state.objOfInterest = None + cthulhu_state.activeMode = None + + def find_focused_object(self) -> Atspi.Accessible | None: + """Returns the focused object in the active window.""" + + result = _get_ax_utilities().get_focused_object(self._window) + tokens = ["FOCUS MANAGER: Focused object in", self._window, "is", result] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return result + + def focus_and_window_are_unknown(self) -> bool: + """Returns True if we have no knowledge about what is focused.""" + + result = self._focus is None and self._window is None + if result: + msg = "FOCUS MANAGER: Focus and window are unknown" + debug.print_message(debug.LEVEL_INFO, msg, True) + + return result + + def focus_is_dead(self) -> bool: + """Returns True if the locus of focus is dead.""" + + if not AXObject.is_dead(self._focus): + return False + + msg = "FOCUS MANAGER: Focus is dead" + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + + def focus_is_active_window(self) -> bool: + """Returns True if the locus of focus is the active window.""" + + if self._focus is None: + return False + + return self._focus == self._window + + def focus_is_in_active_window(self) -> bool: + """Returns True if the locus of focus is inside the current window.""" + + return self._focus is not None and AXObject.is_ancestor(self._focus, self._window) + + def emit_region_changed( + self, obj: Atspi.Accessible, + start_offset: int | None = None, + end_offset: int | None = None, + mode: str | None = None + ) -> None: + """Notifies interested clients that the current region of interest has changed.""" + + if start_offset is None: + start_offset = 0 + if end_offset is None: + end_offset = start_offset + if mode is None: + mode = FOCUS_TRACKING + + if obj is not None: + obj.emit("mode-changed::" + mode, 1, "") + + if mode != self._active_mode: + tokens = ["FOCUS MANAGER: Switching mode from", self._active_mode, "to", mode] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + self._active_mode = mode + cthulhu_state.activeMode = mode + if mode == FLAT_REVIEW: + braille.setBrlapiPriority(braille.BRLAPI_PRIORITY_HIGH) + else: + braille.setBrlapiPriority() + + tokens = ["FOCUS MANAGER: Region of interest:", obj, f"({start_offset}, {end_offset})"] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + if obj is not None: + obj.emit("region-changed", start_offset, end_offset) + + if obj != self._object_of_interest: + tokens = ["FOCUS MANAGER: Switching object of interest from", + self._object_of_interest, "to", obj] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + self._object_of_interest = obj + cthulhu_state.objOfInterest = obj + + def in_say_all(self) -> bool: + """Returns True if we are in say-all mode.""" + + return self._active_mode == SAY_ALL + + def get_active_mode_and_object_of_interest( + self + ) -> tuple[str | None, Atspi.Accessible | None]: + """Returns the current mode and associated object of interest""" + + tokens = ["FOCUS MANAGER: Active mode:", self._active_mode, + "Object of interest:", self._object_of_interest] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return self._active_mode, self._object_of_interest + + def get_penultimate_cursor_position(self) -> tuple[Atspi.Accessible | None, int]: + """Returns the penultimate cursor position as a tuple of (object, offset).""" + + obj, offset = self._penultimate_cursor_position + tokens = ["FOCUS MANAGER: Penultimate cursor position:", obj, offset] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return obj, offset + + def get_last_cursor_position(self) -> tuple[Atspi.Accessible | None, int]: + """Returns the last cursor position as a tuple of (object, offset).""" + + obj, offset = self._last_cursor_position + tokens = ["FOCUS MANAGER: Last cursor position:", obj, offset] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return obj, offset + + def set_last_cursor_position(self, obj: Atspi.Accessible | None, offset: int) -> None: + """Sets the last cursor position as a tuple of (object, offset).""" + + tokens = ["FOCUS MANAGER: Setting last cursor position to", obj, offset] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + self._penultimate_cursor_position = self._last_cursor_position + self._last_cursor_position = obj, offset + + def get_last_cell_coordinates(self) -> tuple[int, int]: + """Returns the last known cell coordinates as a tuple of (row, column).""" + + row, column = self._last_cell_coordinates + msg = f"FOCUS MANAGER: Last known cell coordinates: row={row}, column={column}" + debug.print_message(debug.LEVEL_INFO, msg, True) + return row, column + + def set_last_cell_coordinates(self, row: int, column: int) -> None: + """Sets the last known cell coordinates as a tuple of (row, column).""" + + msg = f"FOCUS MANAGER: Setting last cell coordinates to row={row}, column={column}" + debug.print_message(debug.LEVEL_INFO, msg, True) + self._last_cell_coordinates = row, column + + def get_locus_of_focus(self) -> Atspi.Accessible | None: + """Returns the current locus of focus (i.e. the object with visual focus).""" + + tokens = ["FOCUS MANAGER: Locus of focus is", self._focus] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return self._focus + + def set_locus_of_focus( + self, + event: Atspi.Event | None, + obj: Atspi.Accessible | None, + notify_script: bool = True, + force: bool = False + ) -> None: + """Sets the locus of focus (i.e., the object with visual focus).""" + + tokens = ["FOCUS MANAGER: Request to set locus of focus to", obj] + debug.print_tokens(debug.LEVEL_INFO, tokens, True, True) + + # We clear the cache on the locus of focus because too many apps and toolkits fail + # to emit the correct accessibility events. We do so recursively on table cells + # to handle bugs like https://gitlab.gnome.org/GNOME/nautilus/-/issues/3253. + recursive = _get_ax_utilities().is_table_cell(obj) + AXObject.clear_cache(obj, recursive, "Setting locus of focus.") + if not force and obj == self._focus: + msg = "FOCUS MANAGER: Setting locus of focus to existing locus of focus" + debug.print_message(debug.LEVEL_INFO, msg, True) + return + + # We save the current row and column of a newly focused or selected table cell so that on + # subsequent cell focus/selection we only present the changed location. + row, column = AXTable.get_cell_coordinates(obj, find_cell=True) + self.set_last_cell_coordinates(row, column) + + # We save the offset for text objects because some apps and toolkits emit caret-moved events + # immediately after a text object gains focus, even though the caret has not actually moved. + # TODO - JD: We should consider making this part of `save_object_info_for_events()` for the + # motivation described above. However, we need to audit callers that set/get the position + # before doing so. + self.set_last_cursor_position(obj, AXText.get_caret_offset(obj)) + AXText.update_cached_selected_text(obj) + + # We save additional information about the object for events that were received at the same + # time as the prioritized focus-change event so we don't double-present aspects about obj. + _get_ax_utilities().save_object_info_for_events(obj) + + # TODO - JD: Consider always updating the active script here. + script = script_manager.get_manager().get_active_script() + if event and (script and not script.app): + app = _get_ax_utilities().get_application(event.source) + script = script_manager.get_manager().get_script(app, event.source) + script_manager.get_manager().set_active_script(script, "Setting locus of focus") + + old_focus = self._focus + if AXObject.is_dead(old_focus): + old_focus = None + + if obj is None: + msg = "FOCUS MANAGER: New locus of focus is null (being cleared)" + debug.print_message(debug.LEVEL_INFO, msg, True) + self._focus = None + cthulhu_state.locusOfFocus = None + return + + if AXObject.is_dead(obj): + tokens = ["FOCUS MANAGER: New locus of focus (", obj, ") is dead. Not updating."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return + + if script is not None: + if not AXObject.is_valid(obj): + tokens = ["FOCUS MANAGER: New locus of focus (", obj, ") is invalid. Not updating."] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return + + tokens = ["FOCUS MANAGER: Changing locus of focus from", old_focus, + "to", obj, ". Notify:", notify_script] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + self._focus = obj + cthulhu_state.locusOfFocus = obj + + if not notify_script: + return + + if script is None: + msg = "FOCUS MANAGER: Cannot notify active script because there isn't one" + debug.print_message(debug.LEVEL_INFO, msg, True) + return + + self.emit_region_changed(obj, mode=FOCUS_TRACKING) + script.locus_of_focus_changed(event, old_focus, self._focus) + + def active_window_is_active(self) -> bool: + """Returns True if the window we think is currently active is actually active.""" + + AXObject.clear_cache(self._window, False, "Ensuring the active window is really active.") + is_active = _get_ax_utilities().is_active(self._window) + tokens = ["FOCUS MANAGER:", self._window, "is active:", is_active] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return is_active + + def get_active_window(self) -> Atspi.Accessible | None: + """Returns the currently-active window (i.e. without searching or verifying).""" + + tokens = ["FOCUS MANAGER: Active window is", self._window] + debug.print_tokens(debug.LEVEL_INFO, tokens, True, True) + return self._window + + def set_active_window( + self, + frame: Atspi.Accessible | None, + app: Atspi.Accessible | None = None, + set_window_as_focus: bool = False, + notify_script: bool = False + ) -> None: + """Sets the active window.""" + + tokens = ["FOCUS MANAGER: Request to set active window to", frame] + if app is not None: + tokens.extend(["in", app]) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + + if frame == self._window: + msg = "FOCUS MANAGER: Setting active window to existing active window" + debug.print_message(debug.LEVEL_INFO, msg, True) + elif frame is None: + self._window = None + cthulhu_state.activeWindow = None + else: + self._window = frame + cthulhu_state.activeWindow = frame + + if set_window_as_focus: + self.set_locus_of_focus(None, self._window, notify_script) + elif not (self.focus_is_active_window() or self.focus_is_in_active_window()): + tokens = ["FOCUS MANAGER: Focus", self._focus, "is not in", self._window] + debug.print_tokens(debug.LEVEL_INFO, tokens, True, True) + + # Don't update the focus to the active window if we can't get to the active window + # from the focused object. https://bugreports.qt.io/browse/QTBUG-130116 + if not AXObject.has_broken_ancestry(self._focus): + self.set_locus_of_focus(None, self._window, notify_script=True) + + app = _get_ax_utilities().get_application(self._focus) + script = script_manager.get_manager().get_script(app, self._focus) + script_manager.get_manager().set_active_script(script, "Setting active window") + + @dbus_service.command + def toggle_presentation_mode( + self, + script: default.Script, + event: InputEvent | None = None, + notify_user: bool = True + ) -> bool: + """Switches between browse mode and focus mode (web content only).""" + + return script.toggle_presentation_mode(event, document=None, notify_user=notify_user) + + @dbus_service.command + def toggle_layout_mode( + self, + script: default.Script, + event: InputEvent | None = None, + notify_user: bool = True + ) -> bool: + """Switches between object mode and layout mode for line presentation (web content only).""" + + return script.toggle_layout_mode(event, notify_user=notify_user) + + @dbus_service.command + def enable_sticky_browse_mode( + self, + script: default.Script, + event: InputEvent | None = None, + notify_user: bool = True + ) -> bool: + """Enables sticky browse mode (web content only).""" + + return script.enable_sticky_browse_mode(event, force_message=notify_user) + + @dbus_service.command + def enable_sticky_focus_mode( + self, + script: default.Script, + event: InputEvent | None = None, + notify_user: bool = True + ) -> bool: + """Enables sticky focus mode (web content only).""" + + return script.enable_sticky_focus_mode(event, force_message=notify_user) + + @dbus_service.getter + def get_in_layout_mode(self) -> bool: + """Returns True if layout mode (as opposed to object mode) is active (web content only).""" + + if script := script_manager.get_manager().get_active_script(): + return script.in_layout_mode() + return False + + @dbus_service.getter + def get_in_focus_mode(self) -> bool: + """Returns True if focus mode is active (web content only).""" + + if script := script_manager.get_manager().get_active_script(): + return script.in_focus_mode() + return False + + @dbus_service.getter + def get_focus_mode_is_sticky(self) -> bool: + """Returns True if focus mode is active and 'sticky' (web content only).""" + + if script := script_manager.get_manager().get_active_script(): + return script.focus_mode_is_sticky() + return False + + @dbus_service.getter + def get_browse_mode_is_sticky(self) -> bool: + """Returns True if browse mode is active and 'sticky' (web content only).""" + + if script := script_manager.get_manager().get_active_script(): + return script.browse_mode_is_sticky() + return False + +_manager: FocusManager = FocusManager() + +def get_manager() -> FocusManager: + """Returns the focus manager singleton.""" + return _manager diff --git a/src/cthulhu/formatting.py b/src/cthulhu/formatting.py index ff0fdf6..0d42196 100644 --- a/src/cthulhu/formatting.py +++ b/src/cthulhu/formatting.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Manages the formatting settings for Cthulhu.""" diff --git a/src/cthulhu/generator.py b/src/cthulhu/generator.py index fe86343..f774c1b 100644 --- a/src/cthulhu/generator.py +++ b/src/cthulhu/generator.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Superclass of classes used to generate presentations for objects.""" @@ -51,7 +51,9 @@ from . import object_properties from . import settings from . import settings_manager from .ax_object import AXObject +from .ax_text import AXText from .ax_utilities import AXUtilities +from .ax_utilities_relation import AXUtilitiesRelation # Python 3.10 compatibility: try: @@ -525,14 +527,9 @@ class Generator: exists. Otherwise, an empty array is returned. """ result = [] - try: - image = obj.queryImage() - except NotImplementedError: - pass - else: - description = image.imageDescription - if description and len(description): - result.append(description) + description = AXObject.get_image_description(obj) + if description and len(description): + result.append(description) return result ##################################################################### @@ -1166,9 +1163,9 @@ class Generator: return [] radioGroupLabel = None - relation = AXObject.get_relation(obj, Atspi.RelationType.LABELLED_BY) - if relation: - radioGroupLabel = relation.get_target(0) + labelledBy = AXUtilitiesRelation.get_is_labelled_by(obj) + if labelledBy: + radioGroupLabel = labelledBy[0] if radioGroupLabel: return [self._script.utilities.displayedText(radioGroupLabel)] @@ -1193,7 +1190,10 @@ class Generator: if not (AXUtilities.is_table_cell(rad) and AXObject.get_child_count(rad)): return self._generateDisplayedText(rad, **args) - content = set([self._script.utilities.displayedText(x).strip() for x in rad]) + content = { + (AXObject.get_name(x) or AXText.get_all_text(x)).strip() + for x in AXObject.iter_children(rad) + } rv = " ".join(filter(lambda x: x, content)) if not rv: return self._generateDisplayedText(rad, **args) diff --git a/src/cthulhu/guilabels.py b/src/cthulhu/guilabels.py index 7cefc9d..730e0fb 100644 --- a/src/cthulhu/guilabels.py +++ b/src/cthulhu/guilabels.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Labels for Cthulhu's GUIs. These have been put in their own module so that we can present them in the correct language when users change the language on the @@ -929,6 +929,20 @@ USE_CARET_NAVIGATION = _("Control caret navigation") # of a checkbox in which users can indicate their default preference. USE_STRUCTURAL_NAVIGATION = _("Enable _structural navigation") +# Translators: This is the label for a checkbox in the preferences dialog. +# When enabled, Cthulhu will play sounds when switching between browse mode +# and focus mode in web content. +ENABLE_MODE_CHANGE_SOUND = _("Play sounds when switching between _browse and focus modes") + +# Translators: This is the label for a combo box in the preferences dialog +# where users can select a sound theme. A sound theme is a collection of +# audio files that Cthulhu plays for various events. +SOUND_THEME = _("Sound _theme:") + +# Translators: This is the title of a frame in the preferences dialog +# containing sound theme options. +SOUND_THEME_TITLE = _("Sound Theme") + # Translators: This refers to the amount of information Cthulhu provides about a # particular object that receives focus. VERBOSITY_LEVEL_BRIEF = _("Brie_f") diff --git a/src/cthulhu/highlighter.py b/src/cthulhu/highlighter.py index cc6d581..961f0b8 100644 --- a/src/cthulhu/highlighter.py +++ b/src/cthulhu/highlighter.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Module for drawing highlights over an area of interest.""" diff --git a/src/cthulhu/input_event.py b/src/cthulhu/input_event.py index 87fd66a..276a429 100644 --- a/src/cthulhu/input_event.py +++ b/src/cthulhu/input_event.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Provides support for handling input events.""" @@ -67,15 +67,18 @@ class InputEvent: self.time = time.time() self._clickCount = 0 - def getClickCount(self): + def get_click_count(self): """Return the count of the number of clicks a user has made.""" return self._clickCount - def setClickCount(self): + def set_click_count(self, count=None): """Updates the count of the number of clicks a user has made.""" - pass + if count is None: + return + + self._clickCount = count def _getXkbStickyKeysState(): from subprocess import check_output @@ -292,8 +295,8 @@ class KeyboardEvent(InputEvent): self.keyType = None self.shouldEcho = False - # Initialize key type - will be refined later in _finalize_initialization - self._finalize_initialization() + # InputEventManager will call _finalize_initialization after setting + # script/object/window to ensure shouldConsume uses correct context. def _finalize_initialization(self): """Finalize initialization after object creation. @@ -331,7 +334,7 @@ class KeyboardEvent(InputEvent): elif self.isActionKey(): self.keyType = KeyboardEvent.TYPE_ACTION self.shouldEcho = _mayEcho and settings.enableActionKeys - elif self.isModifierKey(): + elif self.is_modifier_key(): self.keyType = KeyboardEvent.TYPE_MODIFIER self.shouldEcho = _mayEcho and settings.enableModifierKeys if self.isCthulhuModifier() and not self.is_duplicate: @@ -393,8 +396,8 @@ class KeyboardEvent(InputEvent): if not self.isLockingKey(): self.shouldEcho = self.shouldEcho and settings.enableKeyEcho - if not self.isModifierKey(): - self.setClickCount() + if not self.is_modifier_key(): + self.set_click_count() if cthulhu_state.bypassNextCommand and _isPressed: KeyboardEvent.cthulhuModifierPressed = False @@ -405,7 +408,7 @@ class KeyboardEvent(InputEvent): if KeyboardEvent.stickyKeys: # apply all recorded sticky modifiers self.modifiers |= KeyboardEvent.cthulhuStickyModifiers - if self.isModifierKey(): + if self.is_modifier_key(): # add this modifier to the sticky ones KeyboardEvent.cthulhuStickyModifiers |= self.modifiers else: @@ -425,7 +428,7 @@ class KeyboardEvent(InputEvent): return lastEvent return None - def setClickCount(self, count=None): + def set_click_count(self, count=None): """Updates the count of the number of clicks a user has made. If count is provided, sets the click count to that value. @@ -440,7 +443,7 @@ class KeyboardEvent(InputEvent): self._clickCount = 1 return - self._clickCount = doubleEvent.getClickCount() + self._clickCount = doubleEvent.get_click_count() if self.is_duplicate: return @@ -489,7 +492,7 @@ class KeyboardEvent(InputEvent): if not AXUtilities.is_password_text(self._obj): return False - if not self.isPrintableKey(): + if not self.is_printable_key(): return False if self.modifiers & keybindings.CTRL_MODIFIER_MASK \ @@ -504,7 +507,7 @@ class KeyboardEvent(InputEvent): if not last: return False - if not last.isPressedKey() or self.isPressedKey(): + if not last.is_pressed_key() or self.is_pressed_key(): return False if self.id == last.id and self.hw_code == last.hw_code: @@ -518,7 +521,7 @@ class KeyboardEvent(InputEvent): if not other: return False - if not other.isPressedKey() or self.isPressedKey(): + if not other.is_pressed_key() or self.is_pressed_key(): return False return self.id == other.id \ @@ -592,7 +595,7 @@ class KeyboardEvent(InputEvent): return True - def isModifierKey(self): + def is_modifier_key(self): """Return True if this is a modifier key.""" if self.keyType: @@ -646,7 +649,7 @@ class KeyboardEvent(InputEvent): return self._is_kp_with_numlock - def isPrintableKey(self): + def is_printable_key(self): """Return True if this is a printable key.""" if self.event_string in ["space", " "]: @@ -657,7 +660,7 @@ class KeyboardEvent(InputEvent): return self.event_string.isprintable() - def isPressedKey(self): + def is_pressed_key(self): """Returns True if the key is pressed""" return self.type == Atspi.EventType.KEY_PRESSED_EVENT @@ -694,7 +697,7 @@ class KeyboardEvent(InputEvent): character echo. We do this to not double-echo a given printable character.""" - if not self.isPrintableKey(): + if not self.is_printable_key(): return False script = cthulhu_state.activeScript @@ -737,47 +740,47 @@ class KeyboardEvent(InputEvent): return keynames.getKeyName(self.event_string) - def getObject(self): + def get_object(self): """Returns the object believed to be associated with this key event.""" return self._obj - def setObject(self, obj): + def set_object(self, obj): """Sets the object believed to be associated with this key event.""" self._obj = obj - def getWindow(self): + def get_window(self): """Returns the window associated with this key event.""" return self._window - def setWindow(self, window): + def set_window(self, window): """Sets the window associated with this key event.""" self._window = window - def getScript(self): + def get_script(self): """Returns the script associated with this key event.""" return self._script - def setScript(self, script): + def set_script(self, script): """Sets the script associated with this key event.""" self._script = script if script: self._app = script.app - def getClickCount(self): + def get_click_count(self): """Returns the click count for this event.""" return self._clickCount - def asSingleLineString(self): + def as_single_line_string(self): """Returns a single-line string representation of this event.""" - return f"KeyboardEvent({self.keyval_name}, pressed={self.isPressedKey()}, modifiers={self.modifiers})" + return f"KeyboardEvent({self.keyval_name}, pressed={self.is_pressed_key()}, modifiers={self.modifiers})" def getHandler(self): """Returns the handler associated with this key event.""" @@ -806,12 +809,19 @@ class KeyboardEvent(InputEvent): def shouldConsume(self): """Returns True if this event should be consumed.""" + # Debug logging to understand handler matching + debugMsg = f"shouldConsume: key='{self.event_string}' hw_code={self.hw_code} modifiers={self.modifiers}" + debug.printMessage(debug.LEVEL_INFO, debugMsg, True) + if not self.timestamp: return False, 'No timestamp' if not self._script: + debug.printMessage(debug.LEVEL_INFO, "shouldConsume: No active script", True) return False, 'No active script when received' + debug.printMessage(debug.LEVEL_INFO, f"shouldConsume: Active script={self._script.__class__.__name__}", True) + if self.is_duplicate: return False, 'Is duplicate' @@ -824,10 +834,16 @@ class KeyboardEvent(InputEvent): self._handler = self._getUserHandler() \ or self._script.keyBindings.getInputHandler(self) + if self._handler: + debug.printMessage(debug.LEVEL_INFO, f"shouldConsume: Handler found: {self._handler.description}", True) + else: + debug.printMessage(debug.LEVEL_INFO, "shouldConsume: No handler found", True) + # TODO - JD: Right now we need to always call consumesKeyboardEvent() # because that method is updating state, even in instances where there # is no handler. scriptConsumes = self._script.consumesKeyboardEvent(self) + debug.printMessage(debug.LEVEL_INFO, f"shouldConsume: scriptConsumes={scriptConsumes}", True) if self._isReleaseForLastNonModifierKeyEvent(): return scriptConsumes, 'Is release for last non-modifier keyevent' @@ -836,7 +852,7 @@ class KeyboardEvent(InputEvent): self._consumer = self._script.learnModePresenter.handle_event return True, 'In Learn Mode' - if self.isModifierKey(): + if self.is_modifier_key(): if not self.isCthulhuModifier(): return False, 'Non-Cthulhu modifier not in Learn Mode' return True, 'Cthulhu modifier' @@ -861,7 +877,7 @@ class KeyboardEvent(InputEvent): return method.__func__ == self._handler.function def _present(self, inputEvent=None): - if self.isPressedKey(): + if self.is_pressed_key(): self._script.presentationInterrupt() if self._script.learnModePresenter.is_active(): @@ -929,7 +945,7 @@ class KeyboardEvent(InputEvent): return False, 'Bypassed cthulhu modifier' cthulhu_state.lastInputEvent = self - if not self.isModifierKey(): + if not self.is_modifier_key(): cthulhu_state.lastNonModifierKeyEvent = self if not self._script: @@ -940,7 +956,7 @@ class KeyboardEvent(InputEvent): self._present() - if not self.isPressedKey(): + if not self.is_pressed_key(): return self._should_consume, 'Consumed based on handler' if cthulhu_state.capturingKeys: @@ -950,7 +966,7 @@ class KeyboardEvent(InputEvent): return True, 'Cthulhu modifier' if cthulhu_state.bypassNextCommand: - if not self.isModifierKey(): + if not self.is_modifier_key(): cthulhu_state.bypassNextCommand = False self._script.addKeyGrabs() return False, 'Bypass next command' @@ -1083,9 +1099,13 @@ class MouseButtonEvent(InputEvent): debug.printMessage(debug.LEVEL_INFO, msg, True) self.x, self.y = x, y - def setClickCount(self): + def set_click_count(self, count=None): """Updates the count of the number of clicks a user has made.""" + if count is not None: + self._clickCount = count + return + if not self.pressed: return diff --git a/src/cthulhu/input_event_manager.py b/src/cthulhu/input_event_manager.py index 959a2d8..6d02cce 100644 --- a/src/cthulhu/input_event_manager.py +++ b/src/cthulhu/input_event_manager.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 # # Copyright (c) 2024 Stormux -# Copyright (c) 2024 Igalia, S.L. -# Copyright (c) 2024 GNOME Foundation Inc. +# Copyright 2024 Igalia, S.L. +# Copyright 2024 GNOME Foundation Inc. +# Author: Joanmarie Diggs # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -18,16 +19,23 @@ # License along with this library; if not, write to the # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. +# +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu + +# pylint: disable=wrong-import-position +# pylint: disable=too-many-public-methods +# pylint: disable=too-many-lines """Provides utilities for managing input events.""" +# This has to be the first non-docstring line in the module to make linters happy. from __future__ import annotations __id__ = "$Id$" __version__ = "$Revision$" __date__ = "$Date$" -__copyright__ = "Copyright (c) 2024 Stormux" \ - "Copyright (c) 2024 Igalia, S.L." \ +__copyright__ = "Copyright (c) 2024 Igalia, S.L." \ "Copyright (c) 2024 GNOME Foundation Inc." __license__ = "LGPL" @@ -35,15 +43,13 @@ from typing import TYPE_CHECKING import gi gi.require_version("Atspi", "2.0") -gi.require_version("Gdk", "3.0") from gi.repository import Atspi -from gi.repository import Gdk from . import debug +from . import focus_manager from . import input_event from . import script_manager from . import settings -from . import cthulhu_state from .ax_object import AXObject from .ax_utilities import AXUtilities @@ -66,47 +72,32 @@ class InputEventManager: """Starts the watcher for keyboard input events.""" msg = "INPUT EVENT MANAGER: Starting key watcher." - debug.printMessage(debug.LEVEL_INFO, msg, True) - try: - atspi_version = Atspi.get_version() - debug.printMessage(debug.LEVEL_INFO, f"INPUT EVENT MANAGER: AT-SPI version: {atspi_version}", True) - if atspi_version >= (2, 55, 90): - debug.printMessage(debug.LEVEL_INFO, "INPUT EVENT MANAGER: Using Device.new_full", True) - self._device = Atspi.Device.new_full("org.stormux.Cthulhu") - else: - debug.printMessage(debug.LEVEL_INFO, "INPUT EVENT MANAGER: Using Device.new", True) - self._device = Atspi.Device.new() - debug.printMessage(debug.LEVEL_INFO, f"INPUT EVENT MANAGER: Device created: {self._device}", True) - debug.printMessage(debug.LEVEL_INFO, f"INPUT EVENT MANAGER: About to add key watcher callback", True) - result = self._device.add_key_watcher(self.process_keyboard_event) - debug.printMessage(debug.LEVEL_INFO, f"INPUT EVENT MANAGER: add_key_watcher result: {result}", True) - debug.printMessage(debug.LEVEL_INFO, "INPUT EVENT MANAGER: Key watcher added successfully", True) - except Exception as e: - debug.printMessage(debug.LEVEL_INFO, f"INPUT EVENT MANAGER: Error in start_key_watcher: {e}", True) - raise + debug.print_message(debug.LEVEL_INFO, msg, True) + self._device = Atspi.Device.new_full("org.stormux.Cthulhu") + self._device.add_key_watcher(self.process_keyboard_event) def stop_key_watcher(self) -> None: - """Stops the watcher for keyboard input events.""" + """Starts the watcher for keyboard input events.""" msg = "INPUT EVENT MANAGER: Stopping key watcher." - debug.printMessage(debug.LEVEL_INFO, msg, True) + debug.print_message(debug.LEVEL_INFO, msg, True) self._device = None def pause_key_watcher(self, pause: bool = True, reason: str = "") -> None: """Pauses processing of keyboard input events.""" - msg = f"INPUT EVENT MANAGER: Pause processing: {pause}. {reason}" - debug.printMessage(debug.LEVEL_INFO, msg, True) + msg = f"INPUT EVENT MANAGER: Pause queueing: {pause}. {reason}" + debug.print_message(debug.LEVEL_INFO, msg, True) self._paused = pause def check_grabbed_bindings(self) -> None: """Checks the grabbed key bindings.""" msg = f"INPUT EVENT MANAGER: {len(self._grabbed_bindings)} grabbed key bindings." - debug.printMessage(debug.LEVEL_INFO, msg, True) + debug.print_message(debug.LEVEL_INFO, msg, True) for grab_id, binding in self._grabbed_bindings.items(): msg = f"INPUT EVENT MANAGER: {grab_id} for: {binding}" - debug.printMessage(debug.LEVEL_INFO, msg, True) + debug.print_message(debug.LEVEL_INFO, msg, True) def add_grabs_for_keybinding(self, binding: keybindings.KeyBinding) -> list[int]: """Adds grabs for binding if it is enabled, returns grab IDs.""" @@ -116,17 +107,23 @@ class InputEventManager: if binding.has_grabs(): tokens = ["INPUT EVENT MANAGER:", binding, "already has grabs."] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return [] if self._device is None: tokens = ["INPUT EVENT MANAGER: No device to add grab for", binding] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return [] grab_ids = [] for kd in binding.key_definitions(): grab_id = self._device.add_key_grab(kd, None) + # When we have double/triple-click bindings, the single-click binding will be + # registered first, and subsequent attempts to register what is externally the + # same grab will fail. If we only have a double/triple-click, it succeeds. + # A grab id of 0 indicates failure. + if grab_id == 0: + continue grab_ids.append(grab_id) self._grabbed_bindings[grab_id] = binding @@ -137,13 +134,13 @@ class InputEventManager: if self._device is None: tokens = ["INPUT EVENT MANAGER: No device to remove grab from", binding] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return grab_ids = binding.get_grab_ids() if not grab_ids: tokens = ["INPUT EVENT MANAGER:", binding, "doesn't have grabs to remove."] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + debug.print_tokens(debug.LEVEL_INFO, tokens, True) return for grab_id in grab_ids: @@ -151,25 +148,14 @@ class InputEventManager: removed = self._grabbed_bindings.pop(grab_id, None) if removed is None: msg = f"INPUT EVENT MANAGER: No key binding for grab id {grab_id}" - debug.printMessage(debug.LEVEL_INFO, msg, True) - - def map_keycode_to_modifier(self, keycode: int) -> int: - """Maps keycode as a modifier, returns the newly-mapped modifier.""" - - if self._device is None: - msg = f"INPUT EVENT MANAGER: No device to map keycode {keycode} to modifier" - debug.printMessage(debug.LEVEL_INFO, msg, True) - return 0 - - self._mapped_keycodes.append(keycode) - return self._device.map_modifier(keycode) + debug.print_message(debug.LEVEL_INFO, msg, True) def map_keysym_to_modifier(self, keysym: int) -> int: """Maps keysym as a modifier, returns the newly-mapped modifier.""" if self._device is None: msg = f"INPUT EVENT MANAGER: No device to map keysym {keysym} to modifier" - debug.printMessage(debug.LEVEL_INFO, msg, True) + debug.print_message(debug.LEVEL_INFO, msg, True) return 0 self._mapped_keysyms.append(keysym) @@ -180,7 +166,7 @@ class InputEventManager: if self._device is None: msg = "INPUT EVENT MANAGER: No device to unmap all modifiers from" - debug.printMessage(debug.LEVEL_INFO, msg, True) + debug.print_message(debug.LEVEL_INFO, msg, True) return for keycode in self._mapped_keycodes: @@ -197,7 +183,7 @@ class InputEventManager: if self._device is None: msg = f"INPUT EVENT MANAGER: No device to add grab for {modifier}" - debug.printMessage(debug.LEVEL_INFO, msg, True) + debug.print_message(debug.LEVEL_INFO, msg, True) return -1 kd = Atspi.KeyDefinition() @@ -207,7 +193,7 @@ class InputEventManager: grab_id = self._device.add_key_grab(kd) msg = f"INPUT EVENT MANAGER: Grab id for {modifier}: {grab_id}" - debug.printMessage(debug.LEVEL_INFO, msg, True) + debug.print_message(debug.LEVEL_INFO, msg, True) return grab_id def remove_grab_for_modifier(self, modifier: str, grab_id: int) -> None: @@ -215,12 +201,12 @@ class InputEventManager: if self._device is None: msg = f"INPUT EVENT MANAGER: No device to remove grab from {modifier}" - debug.printMessage(debug.LEVEL_INFO, msg, True) + debug.print_message(debug.LEVEL_INFO, msg, True) return self._device.remove_key_grab(grab_id) msg = f"INPUT EVENT MANAGER: Grab id removed for {modifier}: {grab_id}" - debug.printMessage(debug.LEVEL_INFO, msg, True) + debug.print_message(debug.LEVEL_INFO, msg, True) def grab_keyboard(self, reason: str = "") -> None: """Grabs the keyboard, e.g. when entering learn mode.""" @@ -228,7 +214,7 @@ class InputEventManager: msg = "INPUT EVENT MANAGER: Grabbing keyboard" if reason: msg += f" Reason: {reason}" - debug.printMessage(debug.LEVEL_INFO, msg, True) + debug.print_message(debug.LEVEL_INFO, msg, True) Atspi.Device.grab_keyboard(self._device) def ungrab_keyboard(self, reason: str = "") -> None: @@ -237,7 +223,7 @@ class InputEventManager: msg = "INPUT EVENT MANAGER: Ungrabbing keyboard" if reason: msg += f" Reason: {reason}" - debug.printMessage(debug.LEVEL_INFO, msg, True) + debug.print_message(debug.LEVEL_INFO, msg, True) Atspi.Device.ungrab_keyboard(self._device) def process_braille_event(self, event: Atspi.Event) -> bool: @@ -253,86 +239,84 @@ class InputEventManager: """Processes this Mouse event.""" mouse_event = input_event.MouseButtonEvent(event) - mouse_event.setClickCount(self._determine_mouse_event_click_count(mouse_event)) + mouse_event.set_click_count(self._determine_mouse_event_click_count(mouse_event)) self._last_input_event = mouse_event - def process_keyboard_event(self, _device, pressed, keycode, keysym, modifiers, text): - """Processes this Atspi keyboard event.""" + def process_remote_controller_event(self, event: input_event.RemoteControllerEvent) -> None: + """Processes this RemoteController event.""" - debug.printMessage(debug.LEVEL_INFO, f"INPUT EVENT MANAGER: Received keyboard event: pressed={pressed}, keycode={keycode}, keysym={keysym}, text='{text}'", True) + # TODO - JD: It probably makes sense to process remote controller events here rather + # than just updating state. + self._last_input_event = event + self._last_non_modifier_key_event = None + + # pylint: disable=too-many-arguments + # pylint: disable=too-many-positional-arguments + def process_keyboard_event( + self, + _device: Atspi.Device, + pressed: bool, + keycode: int, + keysym: int, + modifiers: int, + text: str + ) -> bool: + """Processes this Atspi keyboard event.""" if self._paused: msg = "INPUT EVENT MANAGER: Keyboard event processing is paused." - debug.printMessage(debug.LEVEL_INFO, msg, True) + debug.print_message(debug.LEVEL_INFO, msg, True) return False - # Handle Cthulhu-specific logic before creating event - if not pressed and text == "Num_Lock" and "KP_Insert" in settings.cthulhuModifierKeys \ - and cthulhu_state.activeScript is not None: - cthulhu_state.activeScript.refreshKeyGrabs() - - if pressed: - cthulhu_state.openingDialog = (text == "space" \ - and (modifiers & ~(1 << Atspi.ModifierType.NUMLOCK))) - - event = input_event.KeyboardEvent(pressed, keycode, keysym, modifiers, text or "") + event = input_event.KeyboardEvent(pressed, keycode, keysym, modifiers, text) if event in [self._last_input_event, self._last_non_modifier_key_event]: msg = "INPUT EVENT MANAGER: Received duplicate event." - debug.printMessage(debug.LEVEL_INFO, msg, True) + debug.print_message(debug.LEVEL_INFO, msg, True) return False + manager = focus_manager.get_manager() if pressed: - # Get active window and focus - window = cthulhu_state.activeWindow - # Use cthulhu_state.activeScript instead of ScriptManager API - active_script = cthulhu_state.activeScript - if active_script and hasattr(active_script, 'utilities') and hasattr(active_script.utilities, 'canBeActiveWindow'): - if not active_script.utilities.canBeActiveWindow(window): - new_window = active_script.utilities.activeWindow() - if new_window is not None: - window = new_window - tokens = ["INPUT EVENT MANAGER: Updating window and active window to", window] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - cthulhu_state.activeWindow = window - else: - tokens = ["WARNING:", window, "cannot be active window. No alternative found."] - debug.printTokens(debug.LEVEL_WARNING, tokens, True) - - event.setWindow(window) - event.setObject(cthulhu_state.locusOfFocus) - event.setScript(active_script) + window = manager.get_active_window() + if not AXUtilities.can_be_active_window(window): + new_window = AXUtilities.find_active_window() + if new_window is not None: + window = new_window + tokens = ["INPUT EVENT MANAGER: Updating window and active window to", window] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + manager.set_active_window(window) + else: + # One example: Brave's popup menus live in frames which lack the active state. + tokens = ["WARNING:", window, "cannot be active window. No alternative found."] + debug.print_tokens(debug.LEVEL_WARNING, tokens, True) + event.set_window(window) + event.set_object(manager.get_locus_of_focus()) + event.set_script(script_manager.get_manager().get_active_script()) elif self.last_event_was_keyboard(): assert isinstance(self._last_input_event, input_event.KeyboardEvent) - event.setWindow(self._last_input_event.getWindow()) - event.setObject(self._last_input_event.getObject()) - event.setScript(self._last_input_event.getScript()) + event.set_window(self._last_input_event.get_window()) + event.set_object(self._last_input_event.get_object()) + event.set_script(self._last_input_event.get_script()) else: - event.setWindow(cthulhu_state.activeWindow) - event.setObject(cthulhu_state.locusOfFocus) - event.setScript(cthulhu_state.activeScript) + event.set_window(manager.get_active_window()) + event.set_object(manager.get_locus_of_focus()) + event.set_script(script_manager.get_manager().get_active_script()) - # Finalize initialization now that context is set event._finalize_initialization() - - if not event.is_duplicate: - debug.printMessage(debug.LEVEL_INFO, f"\n{event}") + event.set_click_count(self._determine_keyboard_event_click_count(event)) + event.process() - event.setClickCount(self._determine_keyboard_event_click_count(event)) - rv = event.process() - - # Do any needed xmodmap handling - from . import cthulhu - cthulhu.updateKeyMap(event) - - if event.isModifierKey(): + if event.is_modifier_key(): if self.is_release_for(event, self._last_input_event): msg = "INPUT EVENT MANAGER: Clearing last non modifier key event" - debug.printMessage(debug.LEVEL_INFO, msg, True) + debug.print_message(debug.LEVEL_INFO, msg, True) self._last_non_modifier_key_event = None else: self._last_non_modifier_key_event = event self._last_input_event = event - return rv + return True + + # pylint: enable=too-many-arguments + # pylint: enable=too-many-positional-arguments def _determine_keyboard_event_click_count(self, event: input_event.KeyboardEvent) -> int: """Determines the click count of event.""" @@ -340,7 +324,7 @@ class InputEventManager: if not self.last_event_was_keyboard(): return 1 - if event.isModifierKey(): + if event.is_modifier_key(): last_event = self._last_input_event else: last_event = self._last_non_modifier_key_event or self._last_input_event @@ -348,15 +332,15 @@ class InputEventManager: assert isinstance(last_event, input_event.KeyboardEvent) if (event.time - last_event.time > settings.doubleClickTimeout) or \ (event.keyval_name != last_event.keyval_name) or \ - (event.getObject() != last_event.getObject()): + (event.get_object() != last_event.get_object()): return 1 - last_count = last_event.getClickCount() - if not event.isPressedKey(): + last_count = last_event.get_click_count() + if not event.is_pressed_key(): return last_count - if last_event.isPressedKey(): + if last_event.is_pressed_key(): return last_count - if (event.isModifierKey() and last_count == 2) or last_count == 3: + if (event.is_modifier_key() and last_count == 2) or last_count == 3: return 1 return last_count + 1 @@ -368,13 +352,13 @@ class InputEventManager: assert isinstance(self._last_input_event, input_event.MouseButtonEvent) if not event.pressed: - return self._last_input_event.getClickCount() + return self._last_input_event.get_click_count() if self._last_input_event.button != event.button: return 1 if event.time - self._last_input_event.time > settings.doubleClickTimeout: return 1 - return self._last_input_event.getClickCount() + 1 + return self._last_input_event.get_click_count() + 1 def last_event_was_keyboard(self) -> bool: """Returns True if the last event is a keyboard event.""" @@ -396,25 +380,31 @@ class InputEventManager: or not isinstance(event2, input_event.KeyboardEvent): return False - if event1.isPressedKey() or not event2.isPressedKey(): + if event1.is_pressed_key() or not event2.is_pressed_key(): return False result = event1.id == event2.id \ and event1.hw_code == event2.hw_code \ and event1.keyval_name == event2.keyval_name - if result and not event1.isModifierKey(): + if result and not event1.is_modifier_key(): result = event1.modifiers == event2.modifiers msg = ( - f"INPUT EVENT MANAGER: {event1.asSingleLineString()} " - f"is release for {event2.asSingleLineString()}: {result}" + f"INPUT EVENT MANAGER: {event1.as_single_line_string()} " + f"is release for {event2.as_single_line_string()}: {result}" ) - debug.printMessage(debug.LEVEL_INFO, msg, True) + debug.print_message(debug.LEVEL_INFO, msg, True) return result def last_event_equals_or_is_release_for_event(self, event): - """Returns True if the last non-modifier event equals, or is the release for, event.""" + """Returns True if the last event equals the provided event, or is the release for it.""" + + if self._last_input_event is event: + return True + + if not self.last_event_was_keyboard(): + return False if self._last_non_modifier_key_event is None: return False @@ -424,8 +414,600 @@ class InputEventManager: return self.is_release_for(self._last_non_modifier_key_event, event) + def _last_key_and_modifiers(self): + """Returns the last keyval name and modifiers""" + + if self._last_non_modifier_key_event is None: + return "", 0 + + if not self.last_event_was_keyboard(): + return "", 0 + + return self._last_non_modifier_key_event.keyval_name, self._last_input_event.modifiers + + def last_event_was_command(self): + """Returns True if the last event is believed to be a command.""" + + if bool(self._last_key_and_modifiers()[1] & 1 << Atspi.ModifierType.CONTROL): + msg = "INPUT EVENT MANAGER: Last event was command." + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + + return False + + def last_event_was_shortcut_for(self, obj): + """Returns True if the last event is believed to be a shortcut key for obj.""" + + string = self._last_key_and_modifiers()[0] + if not string: + return False + + rv = False + keys = AXObject.get_action_key_binding(obj, 0).split(";") + for key in keys: + if key.endswith(string.upper()): + rv = True + break + + if rv: + tokens = ["INPUT EVENT MANAGER: Last event was shortcut for", obj] + debug.print_tokens(debug.LEVEL_INFO, tokens, True) + return rv + + def last_event_was_printable_key(self): + """Returns True if the last event is believed to be a printable key.""" + + if not self.last_event_was_keyboard(): + return False + + if self._last_input_event.is_printable_key(): + msg = "INPUT EVENT MANAGER: Last event was printable key" + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + + return False + + def last_event_was_caret_navigation(self): + """Returns True if the last event is believed to be caret navigation.""" + + return self.last_event_was_character_navigation() \ + or self.last_event_was_word_navigation() \ + or self.last_event_was_line_navigation() \ + or self.last_event_was_line_boundary_navigation() \ + or self.last_event_was_file_boundary_navigation() \ + or self.last_event_was_page_navigation() + + def last_event_was_caret_selection(self): + """Returns True if the last event is believed to be caret selection.""" + + string, mods = self._last_key_and_modifiers() + if string not in ["Home", "End", "Up", "Down", "Left", "Right"]: + rv = False + else: + rv = bool(mods & 1 << Atspi.ModifierType.SHIFT) + + if rv: + msg = "INPUT EVENT MANAGER: Last event was caret selection" + debug.print_message(debug.LEVEL_INFO, msg, True) + return rv + + def last_event_was_backward_caret_navigation(self): + """Returns True if the last event is believed to be backward caret navigation.""" + + string, mods = self._last_key_and_modifiers() + if string not in ["Up", "Left"]: + rv = False + else: + rv = not mods & 1 << Atspi.ModifierType.SHIFT + + if rv: + msg = "INPUT EVENT MANAGER: Last event was backward caret navigation" + debug.print_message(debug.LEVEL_INFO, msg, True) + return rv + + def last_event_was_forward_caret_navigation(self): + """Returns True if the last event is believed to be forward caret navigation.""" + + string, mods = self._last_key_and_modifiers() + if string not in ["Down", "Right"]: + rv = False + else: + rv = not mods & 1 << Atspi.ModifierType.SHIFT + + if rv: + msg = "INPUT EVENT MANAGER: Last event was forward caret navigation" + debug.print_message(debug.LEVEL_INFO, msg, True) + return rv + + def last_event_was_forward_caret_selection(self): + """Returns True if the last event is believed to be forward caret selection.""" + + string, mods = self._last_key_and_modifiers() + if string not in ["Down", "Right"]: + rv = False + else: + rv = bool(mods & 1 << Atspi.ModifierType.SHIFT) + + if rv: + msg = "INPUT EVENT MANAGER: Last event was forward caret selection" + debug.print_message(debug.LEVEL_INFO, msg, True) + return rv + + def last_event_was_character_navigation(self): + """Returns True if the last event is believed to be character navigation.""" + + string, mods = self._last_key_and_modifiers() + if string not in ["Left", "Right"]: + rv = False + elif mods & 1 << Atspi.ModifierType.CONTROL or mods & 1 << Atspi.ModifierType.ALT: + rv = False + else: + rv = True + + if rv: + msg = "INPUT EVENT MANAGER: Last event was character navigation" + debug.print_message(debug.LEVEL_INFO, msg, True) + return rv + + def last_event_was_word_navigation(self): + """Returns True if the last event is believed to be word navigation.""" + + string, mods = self._last_key_and_modifiers() + if string not in ["Left", "Right"]: + rv = False + else: + rv = bool(mods & 1 << Atspi.ModifierType.CONTROL) + + if rv: + msg = "INPUT EVENT MANAGER: Last event was word navigation" + debug.print_message(debug.LEVEL_INFO, msg, True) + return rv + + def last_event_was_previous_word_navigation(self): + """Returns True if the last event is believed to be previous-word navigation.""" + + string, mods = self._last_key_and_modifiers() + if string != "Left": + rv = False + else: + rv = bool(mods & 1 << Atspi.ModifierType.CONTROL) + + if rv: + msg = "INPUT EVENT MANAGER: Last event was previous-word navigation" + debug.print_message(debug.LEVEL_INFO, msg, True) + return rv + + def last_event_was_next_word_navigation(self): + """Returns True if the last event is believed to be next-word navigation.""" + + string, mods = self._last_key_and_modifiers() + if string != "Right": + rv = False + else: + rv = bool(mods & 1 << Atspi.ModifierType.CONTROL) + + if rv: + msg = "INPUT EVENT MANAGER: Last event was next-word navigation" + debug.print_message(debug.LEVEL_INFO, msg, True) + return rv + + def last_event_was_line_navigation(self): + """Returns True if the last event is believed to be line navigation.""" + + string, mods = self._last_key_and_modifiers() + if string not in ["Up", "Down"]: + rv = False + elif mods & 1 << Atspi.ModifierType.CONTROL: + rv = False + else: + focus = focus_manager.get_manager().get_locus_of_focus() + if AXUtilities.is_single_line(focus): + rv = False + else: + rv = not AXUtilities.is_widget_controlled_by_line_navigation(focus) + + if rv: + msg = "INPUT EVENT MANAGER: Last event was line navigation" + debug.print_message(debug.LEVEL_INFO, msg, True) + return rv + + def last_event_was_paragraph_navigation(self): + """Returns True if the last event is believed to be paragraph navigation.""" + + string, mods = self._last_key_and_modifiers() + if not (string in ["Up", "Down"] and mods & 1 << Atspi.ModifierType.CONTROL): + rv = False + else: + rv = not mods & 1 << Atspi.ModifierType.SHIFT + + if rv: + msg = "INPUT EVENT MANAGER: Last event was paragraph navigation" + debug.print_message(debug.LEVEL_INFO, msg, True) + return rv + + def last_event_was_line_boundary_navigation(self): + """Returns True if the last event is believed to be navigation to start/end of line.""" + + string, mods = self._last_key_and_modifiers() + if string not in ["Home", "End"]: + rv = False + else: + rv = not mods & 1 << Atspi.ModifierType.CONTROL + + if rv: + msg = "INPUT EVENT MANAGER: Last event was line boundary navigation" + debug.print_message(debug.LEVEL_INFO, msg, True) + return rv + + def last_event_was_file_boundary_navigation(self): + """Returns True if the last event is believed to be navigation to top/bottom of file.""" + + string, mods = self._last_key_and_modifiers() + if string not in ["Home", "End"]: + rv = False + else: + rv = bool(mods & 1 << Atspi.ModifierType.CONTROL) + + if rv: + msg = "INPUT EVENT MANAGER: Last event was file boundary navigation" + debug.print_message(debug.LEVEL_INFO, msg, True) + return rv + + def last_event_was_page_navigation(self): + """Returns True if the last event is believed to be page navigation.""" + + string, mods = self._last_key_and_modifiers() + if string not in ["Page_Up", "Page_Down"]: + rv = False + elif mods & 1 << Atspi.ModifierType.CONTROL: + rv = False + else: + focus = focus_manager.get_manager().get_locus_of_focus() + if AXUtilities.is_single_line(focus): + rv = False + else: + rv = not AXUtilities.is_widget_controlled_by_line_navigation(focus) + + if rv: + msg = "INPUT EVENT MANAGER: Last event was page navigation" + debug.print_message(debug.LEVEL_INFO, msg, True) + return rv + + def last_event_was_page_switch(self): + """Returns True if the last event is believed to be a page switch.""" + + string, mods = self._last_key_and_modifiers() + if string.isnumeric(): + rv = bool(mods & 1 << Atspi.ModifierType.ALT) + elif string in ["Page_Up", "Page_Down"]: + rv = bool(mods & 1 << Atspi.ModifierType.CONTROL) + else: + rv = False + + if rv: + msg = "INPUT EVENT MANAGER: Last event was page switch" + debug.print_message(debug.LEVEL_INFO, msg, True) + return rv + + def last_event_was_tab_navigation(self): + """Returns True if the last event is believed to be Tab navigation.""" + + string, mods = self._last_key_and_modifiers() + if string not in ["Tab", "ISO_Left_Tab"]: + rv = False + elif mods & 1 << Atspi.ModifierType.CONTROL or mods & 1 << Atspi.ModifierType.ALT: + rv = False + else: + rv = True + + if rv: + msg = "INPUT EVENT MANAGER: Last event was Tab navigation" + debug.print_message(debug.LEVEL_INFO, msg, True) + return rv + + def last_event_was_table_sort(self): + """Returns True if the last event is believed to be a table sort.""" + + focus = focus_manager.get_manager().get_locus_of_focus() + if not AXUtilities.is_table_header(focus): + rv = False + elif self.last_event_was_mouse_button(): + rv = self.last_event_was_primary_click() + elif self.last_event_was_keyboard(): + rv = self.last_event_was_return_or_space() + else: + rv = False + + if rv: + msg = "INPUT EVENT MANAGER: Last event was table sort" + debug.print_message(debug.LEVEL_INFO, msg, True) + return rv + + def last_event_was_unmodified_arrow(self): + """Returns True if the last event is an unmodified arrow.""" + + string, mods = self._last_key_and_modifiers() + if string not in ["Left", "Right", "Up", "Down"]: + return False + + if mods & 1 << Atspi.ModifierType.CONTROL \ + or mods & 1 << Atspi.ModifierType.SHIFT \ + or mods & 1 << Atspi.ModifierType.ALT: + return False + + # TODO: JD - 8 is the value of keybindings.MODIFIER_ORCA, but we need to + # avoid a circular import. + if mods & 1 << 8: + return False + + return True + + def last_event_was_alt_modified(self): + """Returns True if the last event was alt-modified.""" + + mods = self._last_key_and_modifiers()[-1] + return mods & 1 << Atspi.ModifierType.ALT + + def last_event_was_backspace(self): + """Returns True if the last event is BackSpace.""" + + return self._last_key_and_modifiers()[0] == "BackSpace" + + def last_event_was_down(self): + """Returns True if the last event is Down.""" + + return self._last_key_and_modifiers()[0] == "Down" + + def last_event_was_f1(self): + """Returns True if the last event is F1.""" + + return self._last_key_and_modifiers()[0] == "F1" + + def last_event_was_left(self): + """Returns True if the last event is Left.""" + + return self._last_key_and_modifiers()[0] == "Left" + + def last_event_was_left_or_right(self): + """Returns True if the last event is Left or Right.""" + + return self._last_key_and_modifiers()[0] in ["Left", "Right"] + + def last_event_was_page_up_or_page_down(self): + """Returns True if the last event is Page_Up or Page_Down.""" + + return self._last_key_and_modifiers()[0] in ["Page_Up", "Page_Down"] + + def last_event_was_right(self): + """Returns True if the last event is Right.""" + + return self._last_key_and_modifiers()[0] == "Right" + + def last_event_was_return(self): + """Returns True if the last event is Return.""" + + return self._last_key_and_modifiers()[0] == "Return" + + def last_event_was_return_or_space(self): + """Returns True if the last event is Return or space.""" + + return self._last_key_and_modifiers()[0] in ["Return", "space", " "] + + def last_event_was_return_tab_or_space(self): + """Returns True if the last event is Return, Tab, or space.""" + + return self._last_key_and_modifiers()[0] in ["Return", "Tab", "space", " "] + + def last_event_was_space(self): + """Returns True if the last event is space.""" + + return self._last_key_and_modifiers()[0] in [" ", "space"] + + def last_event_was_tab(self): + """Returns True if the last event is Tab.""" + + return self._last_key_and_modifiers()[0] == "Tab" + + def last_event_was_up(self): + """Returns True if the last event is Up.""" + + return self._last_key_and_modifiers()[0] == "Up" + + def last_event_was_up_or_down(self): + """Returns True if the last event is Up or Down.""" + + return self._last_key_and_modifiers()[0] in ["Up", "Down"] + + def last_event_was_delete(self): + """Returns True if the last event is believed to be delete.""" + + string, mods = self._last_key_and_modifiers() + if string in ["Delete", "KP_Delete"]: + rv = True + elif string.lower() == "d": + rv = bool(mods & 1 << Atspi.ModifierType.CONTROL) + else: + rv = False + + if rv: + msg = "INPUT EVENT MANAGER: Last event was delete" + debug.print_message(debug.LEVEL_INFO, msg, True) + return rv + + def last_event_was_cut(self): + """Returns True if the last event is believed to be the cut command.""" + + string, mods = self._last_key_and_modifiers() + if string.lower() != "x": + return False + + if mods & 1 << Atspi.ModifierType.CONTROL and not mods & 1 << Atspi.ModifierType.SHIFT: + msg = "INPUT EVENT MANAGER: Last event was cut" + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + return False + + def last_event_was_copy(self): + """Returns True if the last event is believed to be the copy command.""" + + string, mods = self._last_key_and_modifiers() + if string.lower() != "c" or not mods & 1 << Atspi.ModifierType.CONTROL: + rv = False + elif AXUtilities.is_terminal(self._last_input_event.get_object()): + rv = mods & 1 << Atspi.ModifierType.SHIFT + else: + rv = not mods & 1 << Atspi.ModifierType.SHIFT + + if rv: + msg = "INPUT EVENT MANAGER: Last event was copy" + debug.print_message(debug.LEVEL_INFO, msg, True) + return rv + + def last_event_was_paste(self): + """Returns True if the last event is believed to be the paste command.""" + + string, mods = self._last_key_and_modifiers() + if string.lower() != "v" or not mods & 1 << Atspi.ModifierType.CONTROL: + rv = False + elif AXUtilities.is_terminal(self._last_input_event.get_object()): + rv = mods & 1 << Atspi.ModifierType.SHIFT + else: + rv = not mods & 1 << Atspi.ModifierType.SHIFT + + if rv: + msg = "INPUT EVENT MANAGER: Last event was paste" + debug.print_message(debug.LEVEL_INFO, msg, True) + return rv + + def last_event_was_undo(self): + """Returns True if the last event is believed to be the undo command.""" + + string, mods = self._last_key_and_modifiers() + if string.lower() != "z": + return False + if mods & 1 << Atspi.ModifierType.CONTROL and not mods & 1 << Atspi.ModifierType.SHIFT: + msg = "INPUT EVENT MANAGER: Last event was undo" + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + return False + + def last_event_was_redo(self): + """Returns True if the last event is believed to be the redo command.""" + + string, mods = self._last_key_and_modifiers() + if string.lower() == "z": + rv = mods & 1 << Atspi.ModifierType.CONTROL and mods & 1 << Atspi.ModifierType.SHIFT + elif string.lower() == "y": + # LibreOffice + rv = mods & 1 << Atspi.ModifierType.CONTROL \ + and not mods & 1 << Atspi.ModifierType.SHIFT + else: + rv = False + + if rv: + msg = "INPUT EVENT MANAGER: Last event was redo" + debug.print_message(debug.LEVEL_INFO, msg, True) + return rv + + def last_event_was_select_all(self): + """Returns True if the last event is believed to be the select all command.""" + + string, mods = self._last_key_and_modifiers() + if string.lower() != "a": + return False + + if (mods & 1 << Atspi.ModifierType.CONTROL and not mods & 1 << Atspi.ModifierType.SHIFT): + msg = "INPUT EVENT MANAGER: Last event was select all" + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + return False + + def last_event_was_primary_click(self): + """Returns True if the last event is a primary mouse click.""" + + if not self.last_event_was_mouse_button(): + return False + + if self._last_input_event.button == "1" and self._last_input_event.pressed: + msg = "INPUT EVENT MANAGER: Last event was primary click" + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + return False + + def last_event_was_primary_release(self): + """Returns True if the last event is a primary mouse release.""" + + if not self.last_event_was_mouse_button(): + return False + + if self._last_input_event.button == "1" and not self._last_input_event.pressed: + msg = "INPUT EVENT MANAGER: Last event was primary release" + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + return False + + def last_event_was_primary_click_or_release(self): + """Returns True if the last event is a primary mouse click or release.""" + + if not self.last_event_was_mouse_button(): + return False + + if self._last_input_event.button == "1": + msg = "INPUT EVENT MANAGER: Last event was primary click or release" + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + return False + + def last_event_was_middle_click(self): + """Returns True if the last event is a middle mouse click.""" + + if not self.last_event_was_mouse_button(): + return False + + if self._last_input_event.button == "2" and self._last_input_event.pressed: + msg = "INPUT EVENT MANAGER: Last event was middle click" + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + return False + + def last_event_was_middle_release(self): + """Returns True if the last event is a middle mouse release.""" + + if not self.last_event_was_mouse_button(): + return False + + if self._last_input_event.button == "2" and not self._last_input_event.pressed: + msg = "INPUT EVENT MANAGER: Last event was middle release" + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + return False + + def last_event_was_secondary_click(self): + """Returns True if the last event is a secondary mouse click.""" + + if not self.last_event_was_mouse_button(): + return False + + if self._last_input_event.button == "3" and self._last_input_event.pressed: + msg = "INPUT EVENT MANAGER: Last event was secondary click" + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + return False + + def last_event_was_secondary_release(self): + """Returns True if the last event is a secondary mouse release.""" + + if not self.last_event_was_mouse_button(): + return False + + if self._last_input_event.button == "3" and not self._last_input_event.pressed: + msg = "INPUT EVENT MANAGER: Last event was secondary release" + debug.print_message(debug.LEVEL_INFO, msg, True) + return True + return False + _manager = InputEventManager() -def getManager(): +def get_manager(): """Returns the Input Event Manager singleton.""" - return _manager \ No newline at end of file + return _manager diff --git a/src/cthulhu/keybindings.py b/src/cthulhu/keybindings.py index c3e682e..dcc0367 100644 --- a/src/cthulhu/keybindings.py +++ b/src/cthulhu/keybindings.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Provides support for defining keybindings and matching them to input events.""" @@ -193,7 +193,7 @@ def getModifierNames(mods): text += _("Shift") + "+" return text -def getClickCountString(count): +def get_click_countString(count): """Returns a human-consumable string representing the number of clicks, such as 'double click' and 'triple click'.""" @@ -286,7 +286,7 @@ class KeyBinding: """Returns a more human-consumable string representing this binding.""" mods = getModifierNames(self.modifiers) - clickCount = getClickCountString(self.click_count) + clickCount = get_click_countString(self.click_count) keysym = self.keysymstring string = f'{mods}{keysym} {clickCount}' @@ -515,7 +515,7 @@ class KeyBindings: matches = [] candidates = [] - clickCount = keyboardEvent.getClickCount() + clickCount = keyboardEvent.get_click_count() for keyBinding in self.keyBindings: if keyBinding.matches(keyboardEvent.hw_code, keyboardEvent.modifiers): if event_str.lower() == 'v': diff --git a/src/cthulhu/keynames.py b/src/cthulhu/keynames.py index 8da6411..642c236 100644 --- a/src/cthulhu/keynames.py +++ b/src/cthulhu/keynames.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Exposes a dictionary, keynames, that maps key events into localized words.""" diff --git a/src/cthulhu/label_inference.py b/src/cthulhu/label_inference.py index 2e614d8..f0384ea 100644 --- a/src/cthulhu/label_inference.py +++ b/src/cthulhu/label_inference.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Heuristic means to infer the functional/displayed label of a widget.""" @@ -31,12 +31,12 @@ __date__ = "$Date$" __copyright__ = "Copyright (C) 2011-2013 Igalia, S.L." __license__ = "LGPL" -import gi -gi.require_version("Atspi", "2.0") -from gi.repository import Atspi - from . import debug +from .ax_component import AXComponent +from .ax_hypertext import AXHypertext from .ax_object import AXObject +from .ax_table import AXTable +from .ax_text import AXText from .ax_utilities import AXUtilities class LabelInference: @@ -78,7 +78,9 @@ class LabelInference: result, objects = self.inferFromTextLeft(obj) debug.printMessage(debug.LEVEL_INFO, f"LABEL INFERENCE: Text Left: '{result}'", True) if not result or self._preferRight(obj): - result, objects = self.inferFromTextRight(obj) or result + rightResult = self.inferFromTextRight(obj) + if rightResult[0] is not None: + result, objects = rightResult debug.printMessage(debug.LEVEL_INFO, f"LABEL INFERENCE: Text Right: '{result}'", True) if not result: result, objects = self.inferFromTable(obj) @@ -151,20 +153,13 @@ class LabelInference: return False def isMatch(x): - return x is not None \ - and not self._script.utilities.isStaticTextLeaf(x) \ - and not AXUtilities.is_link(x) + return AXUtilities.is_web_element(x) and not AXUtilities.is_link(x) - children = [child for child in AXObject.iter_children(obj, isMatch)] + children = list(AXObject.iter_children(obj, isMatch)) if len(children) > 1: return False - try: - text = obj.queryText() - except NotImplementedError: - return True - - string = text.getText(0, -1).strip() + string = AXText.get_all_text(obj).strip() if string.count(self._script.EMBEDDED_OBJECT_CHARACTER) > 1: return False @@ -191,19 +186,7 @@ class LabelInference: if rv is not None: return rv - widgetRoles = [Atspi.Role.CHECK_BOX, - Atspi.Role.RADIO_BUTTON, - Atspi.Role.TOGGLE_BUTTON, - Atspi.Role.COMBO_BOX, - Atspi.Role.LIST, - Atspi.Role.LIST_BOX, - Atspi.Role.MENU, - Atspi.Role.MENU_ITEM, - Atspi.Role.ENTRY, - Atspi.Role.PASSWORD_TEXT, - Atspi.Role.PUSH_BUTTON] - - isWidget = AXObject.get_role(obj) in widgetRoles + isWidget = AXUtilities.is_widget(obj) or AXUtilities.is_menu_related(obj) if not isWidget and AXUtilities.is_editable(obj): isWidget = True @@ -223,30 +206,15 @@ class LabelInference: return rv extents = 0, 0, 0, 0 - text = self._script.utilities.queryNonEmptyText(obj) - if text: - if not AXUtilities.is_text_input(obj): - if endOffset == -1: - try: - endOffset = text.characterCount - except Exception: - tokens = ["LABEL INFERENCE: Exception getting character count for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return extents - - extents = text.getRangeExtents(startOffset, endOffset, 0) + if AXObject.supports_text(obj) and not AXUtilities.is_text_input(obj): + if endOffset == -1: + endOffset = AXText.get_character_count(obj) + rect = AXText.get_range_rect(obj, startOffset, endOffset) + extents = rect.x, rect.y, rect.width, rect.height if not (extents[2] and extents[3]): - try: - ext = obj.queryComponent().getExtents(0) - except NotImplementedError: - tokens = ["LABEL INFERENCE:", obj, "does not implement the component interface"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - except Exception: - tokens = ["LABEL INFERENCE: Exception getting extents for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - else: - extents = ext.x, ext.y, ext.width, ext.height + rect = AXComponent.get_rect(obj) + extents = rect.x, rect.y, rect.width, rect.height self._extentsCache[(hash(obj), startOffset, endOffset)] = extents return extents @@ -260,7 +228,7 @@ class LabelInference: if self._cannotLabel(obj): return None, [] - contents = self._script.utilities.getObjectContentsAtOffset(obj, useCache=False) + contents = self._script.utilities.get_objectContentsAtOffset(obj, useCache=False) objects = [content[0] for content in contents] if list(filter(self._isWidget, objects)): return None, [] @@ -278,10 +246,12 @@ class LabelInference: key = hash(obj) if self._isWidget(obj): - start, end = self._script.utilities.getHyperlinkRange(obj) + start = AXHypertext.get_link_start_offset(obj) obj = AXObject.get_parent(obj) rv = self._script.utilities.getLineContentsAtOffset(obj, start, True, False) + if rv is None: + rv = [] self._lineCache[key] = rv return rv @@ -408,6 +378,10 @@ class LabelInference: if self._cannotLabel(prevObj): return None, [] + if string.endswith("\n"): + string = string[:-1] + end -= 1 + if string.strip(): x, y, width, height = self._getExtents(prevObj, start, end) objX, objY, objWidth, objHeight = self._getExtents(obj) @@ -485,11 +459,12 @@ class LabelInference: if rowindex < 0 or colindex < 0: return None - iface = table.queryTable() - if rowindex >= iface.nRows or colindex >= iface.nColumns: + rows = AXTable.get_row_count(table, prefer_attribute=False) + cols = AXTable.get_column_count(table, prefer_attribute=False) + if rowindex >= rows or colindex >= cols: return None - return table.queryTable().getAccessibleAt(rowindex, colindex) + return AXTable.get_cell_at(table, rowindex, colindex) def _getCellFromRow(self, row, colindex): if 0 <= colindex < AXObject.get_child_count(row): @@ -527,7 +502,7 @@ class LabelInference: cellLeft = cellRight = cellAbove = cellBelow = None gridrow = AXObject.find_ancestor(cell, self._isRow) - rowindex, colindex = self._script.utilities.coordinatesForCell(cell) + rowindex, colindex = AXTable.get_cell_coordinates(cell, prefer_attribute=False) if colindex > -1: cellLeft = self._getCellFromTable(grid, rowindex, colindex - 1) cellRight = self._getCellFromTable(grid, rowindex, colindex + 1) @@ -588,12 +563,12 @@ class LabelInference: # as a functional label. Therefore, see if this table looks like a grid # of widgets with the functional labels in the first row. - try: - table = grid.queryTable() - except NotImplementedError: + rows = AXTable.get_row_count(grid, prefer_attribute=False) + cols = AXTable.get_column_count(grid, prefer_attribute=False) + if rows <= 0 or cols <= 0: return None, [] - firstRow = [table.getAccessibleAt(0, i) for i in range(table.nColumns)] + firstRow = [AXTable.get_cell_at(grid, 0, i) for i in range(cols)] if not firstRow or list(filter(self._isWidget, firstRow)): return None, [] @@ -605,7 +580,7 @@ class LabelInference: return False return not AXUtilities.have_same_role(AXObject.get_child(x, 0), obj) - cells = [table.getAccessibleAt(i, colindex) for i in range(1, table.nRows)] + cells = [AXTable.get_cell_at(grid, i, colindex) for i in range(1, rows)] if list(filter(isMatch, cells)): return None, [] diff --git a/src/cthulhu/laptop_keyboardmap.py b/src/cthulhu/laptop_keyboardmap.py index c4bcfda..fbfa2c8 100644 --- a/src/cthulhu/laptop_keyboardmap.py +++ b/src/cthulhu/laptop_keyboardmap.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """ A list of common keybindings and unbound keys pulled out from default.py: __getLaptopBindings() diff --git a/src/cthulhu/learn_mode_presenter.py b/src/cthulhu/learn_mode_presenter.py index 6ff3fd3..e64c067 100644 --- a/src/cthulhu/learn_mode_presenter.py +++ b/src/cthulhu/learn_mode_presenter.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Module for learn mode""" @@ -164,7 +164,7 @@ class LearnModePresenter: return False cthulhu_state.activeScript.speakKeyEvent(event) - if event.isPrintableKey() and event.getClickCount() == 2 \ + if event.is_printable_key() and event.get_click_count() == 2 \ and event.getHandler() is None: cthulhu_state.activeScript.phoneticSpellCurrentItem(event.event_string) @@ -238,7 +238,7 @@ class LearnModePresenter: bindings[guilabels.KB_GROUP_FLAT_REVIEW] = bound items += len(bound) - bound = script.getObjectNavigator().get_bindings().getBoundBindings() + bound = script.get_objectNavigator().get_bindings().getBoundBindings() bindings[guilabels.KB_GROUP_OBJECT_NAVIGATION] = bound items += len(bound) diff --git a/src/cthulhu/liveregions.py b/src/cthulhu/liveregions.py index 93a9c3a..6caa161 100644 --- a/src/cthulhu/liveregions.py +++ b/src/cthulhu/liveregions.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu import gi gi.require_version("Atspi", "2.0") @@ -42,6 +42,8 @@ from . import cthulhu_state from . import settings_manager from .ax_collection import AXCollection from .ax_object import AXObject +from .ax_text import AXText +from .ax_utilities_relation import AXUtilitiesRelation _settingsManager = settings_manager.getManager() @@ -313,7 +315,7 @@ class LiveRegionManager: return obj = cthulhu_state.locusOfFocus - objectid = self._getObjectId(obj) + objectid = self._get_objectId(obj) uri = self._script.bookmarks.getURIKey() try: @@ -344,7 +346,7 @@ class LiveRegionManager: contents of that object""" if self.lastliveobj: self._script.utilities.setCaretPosition(self.lastliveobj, 0) - self._script.speakContents(self._script.utilities.getObjectContentsAtOffset( + self._script.speakContents(self._script.utilities.get_objectContentsAtOffset( self.lastliveobj, 0)) def reviewLiveAnnouncement(self, script, inputEvent): @@ -391,7 +393,7 @@ class LiveRegionManager: # markup. matches = self.getAllLiveRegions(docframe) for match in matches: - objectid = self._getObjectId(match) + objectid = self._get_objectId(match) self._politenessOverrides[(uri, objectid)] = LIVE_OFF # Toggle our flag @@ -421,26 +423,24 @@ class LiveRegionManager: def generateLiveRegionDescription(self, obj, **args): """Used in conjunction with whereAmI to output description and politeness of the given live region object""" - objectid = self._getObjectId(obj) + objectid = self._get_objectId(obj) uri = self._script.bookmarks.getURIKey() results = [] # get the description if there is one. - relation = AXObject.get_relation(obj, Atspi.RelationType.DESCRIBED_BY) - if relation: - targetobj = relation.getTarget(0) - try: + describedBy = AXUtilitiesRelation.get_is_described_by(obj) + if describedBy: + targetobj = describedBy[0] + if AXObject.supports_text(targetobj): # We will add on descriptions if they don't duplicate # what's already in the object's description. # See http://bugzilla.gnome.org/show_bug.cgi?id=568467 # for more information. # - description = targetobj.queryText().getText(0, -1) + description = AXText.get_all_text(targetobj) if description.strip() != AXObject.get_description(obj).strip(): results.append(description) - except NotImplementedError: - pass # get the politeness level as a string try: @@ -526,7 +526,7 @@ class LiveRegionManager: def _getLiveType(self, obj): """Returns the live politeness setting for a given object. Also, registers LIVE_NONE objects in politeness overrides when monitoring.""" - objectid = self._getObjectId(obj) + objectid = self._get_objectId(obj) uri = self._script.bookmarks.getURIKey() if (uri, objectid) in self._politenessOverrides: # look to see if there is a user politeness override @@ -540,7 +540,7 @@ class LiveRegionManager: self._politenessOverrides[(uri, objectid)] = livetype return livetype - def _getObjectId(self, obj): + def _get_objectId(self, obj): """Returns the HTML 'id' or a path to the object is an HTML id is unavailable""" attrs = self._getAttrDictionary(obj) diff --git a/src/cthulhu/logger.py b/src/cthulhu/logger.py index 74d3286..8de4614 100644 --- a/src/cthulhu/logger.py +++ b/src/cthulhu/logger.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Output logger for regression testing.""" diff --git a/src/cthulhu/mathsymbols.py b/src/cthulhu/mathsymbols.py index edec26b..d3f482c 100644 --- a/src/cthulhu/mathsymbols.py +++ b/src/cthulhu/mathsymbols.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" diff --git a/src/cthulhu/meson.build b/src/cthulhu/meson.build index 52296f4..f7ed6a1 100644 --- a/src/cthulhu/meson.build +++ b/src/cthulhu/meson.build @@ -4,13 +4,23 @@ cthulhu_python_sources = files([ 'acss.py', 'action_presenter.py', 'ax_collection.py', + 'ax_component.py', + 'ax_document.py', 'ax_event_synthesizer.py', + 'ax_hypertext.py', 'ax_object.py', 'ax_selection.py', + 'ax_table.py', + 'ax_text.py', + 'ax_utilities_application.py', 'ax_utilities.py', 'ax_utilities_collection.py', + 'ax_utilities_debugging.py', + 'ax_utilities_event.py', + 'ax_utilities_relation.py', 'ax_utilities_role.py', 'ax_utilities_state.py', + 'ax_value.py', 'bookmarks.py', 'braille.py', 'braille_generator.py', @@ -35,6 +45,7 @@ cthulhu_python_sources = files([ 'flat_review.py', 'flat_review_presenter.py', 'formatting.py', + 'focus_manager.py', 'generator.py', 'guilabels.py', 'highlighter.py', @@ -74,6 +85,7 @@ cthulhu_python_sources = files([ 'sleep_mode_manager.py', 'sound.py', 'sound_generator.py', + 'sound_theme_manager.py', 'speech_and_verbosity_manager.py', 'speech_history.py', 'speech.py', diff --git a/src/cthulhu/messages.py b/src/cthulhu/messages.py index 2187fad..8fd9505 100644 --- a/src/cthulhu/messages.py +++ b/src/cthulhu/messages.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Messages which Cthulhu presents in speech and/or braille. These have been put in their own module so that we can present them in @@ -314,7 +314,7 @@ CLI_GUI_SETUP = _("Set up user preferences (GUI version)") # Translators: This text is the description displayed when Cthulhu is launched # from the command line and the help text is displayed. -CLI_EPILOG = _("Report bugs to cthulhu-list@gnome.org.") +CLI_EPILOG = _("Report bugs to https://groups.io/g/stormux.") # Translators: Cthulhu normal speaks the text which was just deleted from a # document via command. Depending on the circumstances, that might be a diff --git a/src/cthulhu/mouse_review.py b/src/cthulhu/mouse_review.py index b758a81..b2e2031 100644 --- a/src/cthulhu/mouse_review.py +++ b/src/cthulhu/mouse_review.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Mouse review mode.""" @@ -57,9 +57,10 @@ from . import cthulhu_state from . import script_manager from . import settings_manager from .ax_object import AXObject +from .ax_text import AXText from .ax_utilities import AXUtilities -_scriptManager = script_manager.getManager() +_scriptManager = script_manager.get_manager() _settingsManager = settings_manager.getManager() class _StringContext: @@ -222,7 +223,7 @@ class _ItemContext: if not AXObject.supports_text(self._obj): return True - if not self._obj.queryText().characterCount: + if not AXText.get_character_count(self._obj): return True return False @@ -258,7 +259,7 @@ class _ItemContext: return self._string.isSubstringOf(other._string) - def getObject(self): + def get_object(self): """Returns the accessible object associated with this context.""" return self._obj @@ -431,7 +432,7 @@ class MouseReviewer: script = None frame = None if obj: - script = _scriptManager.getScript(AXObject.get_application(obj), obj) + script = _scriptManager.get_script(AXObject.get_application(obj), obj) if script: frame = script.utilities.topLevelObject(obj) self._currentMouseOver = _ItemContext(obj=obj, frame=frame, script=script) @@ -480,7 +481,7 @@ class MouseReviewer: if not self._active: return None - obj = self._currentMouseOver.getObject() + obj = self._currentMouseOver.get_object() if time.time() - self._currentMouseOver.getTime() > 0.1: tokens = ["MOUSE REVIEW: Treating", obj, "as stale"] @@ -545,8 +546,11 @@ class MouseReviewer: if coordType is None: coordType = Atspi.CoordType.SCREEN + if not AXObject.supports_component(obj): + return False + try: - return obj.queryComponent().contains(x, y, coordType) + return Atspi.Component.contains(obj, x, y, coordType) except Exception: return False @@ -556,12 +560,15 @@ class MouseReviewer: if coordType is None: coordType = Atspi.CoordType.SCREEN + if not AXObject.supports_component(obj): + return False + try: - extents = obj.queryComponent().getExtents(coordType) + extents = Atspi.Component.get_extents(obj, coordType) except Exception: return False - return list(extents) == list(bounds) + return [extents.x, extents.y, extents.width, extents.height] == list(bounds) def _accessible_window_at_point(self, pX, pY): """Returns the accessible window at the specified coordinates.""" @@ -614,7 +621,7 @@ class MouseReviewer: if not window: return - script = _scriptManager.getScript(AXObject.get_application(window)) + script = _scriptManager.get_script(AXObject.get_application(window)) if not script: return @@ -636,7 +643,7 @@ class MouseReviewer: tokens = [f"MOUSE REVIEW: Object at ({pX}, {pY}) is", obj] debug.printTokens(debug.LEVEL_INFO, tokens, True) - script = _scriptManager.getScript(AXObject.get_application(window), obj) + script = _scriptManager.get_script(AXObject.get_application(window), obj) if menu and obj and not AXObject.find_ancestor(obj, AXUtilities.is_menu): if script.utilities.intersectingRegion(obj, menu) != (0, 0, 0, 0): tokens = ["MOUSE REVIEW:", obj, "believed to be under", menu] @@ -661,7 +668,7 @@ class MouseReviewer: x, y, width, height = self._currentMouseOver.getBoundingBox() if y <= pY <= y + height and self._currentMouseOver.getString(): boundary = Atspi.TextBoundaryType.WORD_START - elif obj == self._currentMouseOver.getObject(): + elif obj == self._currentMouseOver.get_object(): boundary = Atspi.TextBoundaryType.LINE_START elif AXUtilities.is_selectable(obj): boundary = Atspi.TextBoundaryType.LINE_START diff --git a/src/cthulhu/notification_presenter.py b/src/cthulhu/notification_presenter.py index 71e0a8b..b7ff978 100644 --- a/src/cthulhu/notification_presenter.py +++ b/src/cthulhu/notification_presenter.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Module for notification messages""" diff --git a/src/cthulhu/object_navigator.py b/src/cthulhu/object_navigator.py index bc01f6b..6459160 100644 --- a/src/cthulhu/object_navigator.py +++ b/src/cthulhu/object_navigator.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Provides ability to navigate objects hierarchically.""" diff --git a/src/cthulhu/object_properties.py b/src/cthulhu/object_properties.py index 77e90e7..0c8e87b 100644 --- a/src/cthulhu/object_properties.py +++ b/src/cthulhu/object_properties.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Propeerties of accessible objects. These have been put in their own module so that we can present them in the correct language when users change the diff --git a/src/cthulhu/phonnames.py b/src/cthulhu/phonnames.py index 9032ac7..ad76f0c 100644 --- a/src/cthulhu/phonnames.py +++ b/src/cthulhu/phonnames.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Provides getPhoneticName method that maps each letter of the alphabet into its localized phonetic equivalent.""" diff --git a/src/cthulhu/plugins/AIAssistant/plugin.py b/src/cthulhu/plugins/AIAssistant/plugin.py index b2f3a1c..c2ea69a 100644 --- a/src/cthulhu/plugins/AIAssistant/plugin.py +++ b/src/cthulhu/plugins/AIAssistant/plugin.py @@ -27,6 +27,8 @@ from cthulhu import settings from cthulhu import settings_manager from cthulhu import cthulhu_state from cthulhu import ax_object +from cthulhu.ax_text import AXText +from cthulhu.ax_value import AXValue from cthulhu import ax_utilities from cthulhu.ax_utilities_state import AXUtilitiesState from cthulhu.plugins.AIAssistant.ai_providers import create_provider @@ -742,16 +744,11 @@ class AIAssistant(Plugin): pass # Fallback: try direct AT-SPI text interface - try: - if ax_object.AXObject.supports_text(obj): - text_iface = obj.queryText() - if text_iface: - text = text_iface.getText(0, -1) - if text: - return text.strip() - except: - pass - + if ax_object.AXObject.supports_text(obj): + text = AXText.get_all_text(obj) + if text: + return text.strip() + return "" except Exception as e: @@ -762,12 +759,8 @@ class AIAssistant(Plugin): """Get value from an accessibility object.""" try: if ax_object.AXObject.supports_value(obj): - try: - value_iface = obj.queryValue() - if value_iface: - return str(value_iface.currentValue) or "" - except: - pass + value = AXValue.get_current_value(obj) + return str(value) if value is not None else "" return "" except Exception as e: @@ -822,18 +815,16 @@ class AIAssistant(Plugin): def _get_object_position(self, obj): """Get position and size information from an accessibility object.""" try: - if hasattr(obj, 'queryComponent'): - component = obj.queryComponent() - if component: - extents = component.getExtents(Atspi.CoordType.SCREEN) - return { - 'x': extents.x, - 'y': extents.y, - 'width': extents.width, - 'height': extents.height - } + if ax_object.AXObject.supports_component(obj): + extents = Atspi.Component.get_extents(obj, Atspi.CoordType.SCREEN) + return { + 'x': extents.x, + 'y': extents.y, + 'width': extents.width, + 'height': extents.height + } return None - + except Exception as e: logger.error(f"Error getting object position: {e}") return None @@ -1170,21 +1161,25 @@ class AIAssistant(Plugin): actions = [] # Check for AT-SPI action interface - try: - if hasattr(obj, 'queryAction'): - action_iface = obj.queryAction() - if action_iface: - action_count = action_iface.get_nActions() - for i in range(action_count): - action_name = action_iface.getName(i) - action_desc = action_iface.getDescription(i) - actions.append({ - 'name': action_name or '', - 'description': action_desc or '', - 'index': i - }) - except: - pass + if ax_object.AXObject.supports_action(obj): + try: + action_count = Atspi.Action.get_n_actions(obj) + except Exception: + action_count = 0 + for i in range(action_count): + try: + action_name = Atspi.Action.get_name(obj, i) + except Exception: + action_name = "" + try: + action_desc = Atspi.Action.get_description(obj, i) + except Exception: + action_desc = "" + actions.append({ + 'name': action_name or '', + 'description': action_desc or '', + 'index': i + }) return actions diff --git a/src/cthulhu/plugins/ByeCthulhu/__init__.py b/src/cthulhu/plugins/ByeCthulhu/__init__.py index 782103c..301e5ea 100644 --- a/src/cthulhu/plugins/ByeCthulhu/__init__.py +++ b/src/cthulhu/plugins/ByeCthulhu/__init__.py @@ -20,6 +20,6 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu diff --git a/src/cthulhu/plugins/Clipboard/__init__.py b/src/cthulhu/plugins/Clipboard/__init__.py index 9531613..f4d7c3a 100644 --- a/src/cthulhu/plugins/Clipboard/__init__.py +++ b/src/cthulhu/plugins/Clipboard/__init__.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Clipboard plugin package.""" diff --git a/src/cthulhu/plugins/Clipboard/plugin.py b/src/cthulhu/plugins/Clipboard/plugin.py index 27e2f4e..6054c62 100644 --- a/src/cthulhu/plugins/Clipboard/plugin.py +++ b/src/cthulhu/plugins/Clipboard/plugin.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Clipboard plugin for Cthulhu.""" diff --git a/src/cthulhu/plugins/DisplayVersion/__init__.py b/src/cthulhu/plugins/DisplayVersion/__init__.py index 782103c..301e5ea 100644 --- a/src/cthulhu/plugins/DisplayVersion/__init__.py +++ b/src/cthulhu/plugins/DisplayVersion/__init__.py @@ -20,6 +20,6 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu diff --git a/src/cthulhu/plugins/HelloCthulhu/__init__.py b/src/cthulhu/plugins/HelloCthulhu/__init__.py index 782103c..301e5ea 100644 --- a/src/cthulhu/plugins/HelloCthulhu/__init__.py +++ b/src/cthulhu/plugins/HelloCthulhu/__init__.py @@ -20,6 +20,6 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu diff --git a/src/cthulhu/plugins/IndentationAudio/plugin.py b/src/cthulhu/plugins/IndentationAudio/plugin.py index 84318ad..04252ba 100644 --- a/src/cthulhu/plugins/IndentationAudio/plugin.py +++ b/src/cthulhu/plugins/IndentationAudio/plugin.py @@ -15,6 +15,8 @@ from gi.repository import GLib from cthulhu.plugin import Plugin, cthulhu_hookimpl from cthulhu import debug +from cthulhu.ax_object import AXObject +from cthulhu.ax_text import AXText # Import Cthulhu's sound system try: @@ -200,22 +202,19 @@ class IndentationAudio(Plugin): # Fallback to direct AT-SPI access try: obj = event.source - if obj and hasattr(obj, 'queryText'): - text_iface = obj.queryText() - if text_iface: - # Get all text and find current line - full_text = text_iface.getText(0, -1) - caret_pos = text_iface.caretOffset - - if full_text: - lines = full_text.split('\n') - char_count = 0 - for line in lines: - if char_count <= caret_pos <= char_count + len(line): - debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Fallback line: '{line}'", True) - self.check_indentation_change(obj, line) - break - char_count += len(line) + 1 # +1 for newline + if AXObject.supports_text(obj): + # Get all text and find current line + full_text = AXText.get_all_text(obj) + caret_pos = AXText.get_caret_offset(obj) + if full_text: + lines = full_text.split('\n') + char_count = 0 + for line in lines: + if char_count <= caret_pos <= char_count + len(line): + debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Fallback line: '{line}'", True) + self.check_indentation_change(obj, line) + break + char_count += len(line) + 1 # +1 for newline except Exception as fallback_e: debug.printMessage(debug.LEVEL_INFO, f"IndentationAudio: Fallback also failed: {fallback_e}", True) @@ -475,4 +474,4 @@ class IndentationAudio(Plugin): logger.info("IndentationAudio: Handled script change") except Exception as e: - logger.error(f"IndentationAudio: Error handling script change: {e}") \ No newline at end of file + logger.error(f"IndentationAudio: Error handling script change: {e}") diff --git a/src/cthulhu/plugins/SimplePluginSystem/__init__.py b/src/cthulhu/plugins/SimplePluginSystem/__init__.py index 782103c..301e5ea 100644 --- a/src/cthulhu/plugins/SimplePluginSystem/__init__.py +++ b/src/cthulhu/plugins/SimplePluginSystem/__init__.py @@ -20,6 +20,6 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu diff --git a/src/cthulhu/plugins/SpeechHistory/plugin.py b/src/cthulhu/plugins/SpeechHistory/plugin.py index 89fc1a7..138f5a1 100644 --- a/src/cthulhu/plugins/SpeechHistory/plugin.py +++ b/src/cthulhu/plugins/SpeechHistory/plugin.py @@ -114,8 +114,10 @@ class SpeechHistory(Plugin): self._create_window() self._window.show_all() - - if self._filterEntry: + if self._treeView and len(self._filterModel) > 0: + self._treeView.grab_focus() + self._treeView.set_cursor(Gtk.TreePath.new_first()) + elif self._filterEntry: self._filterEntry.grab_focus() debug.printMessage(debug.LEVEL_INFO, "SpeechHistory: Window shown", True) @@ -137,6 +139,8 @@ class SpeechHistory(Plugin): mainBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + self._filterText = "" + # Filter row filterRow = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) filterLabel = Gtk.Label(label="_Filter:") @@ -153,7 +157,7 @@ class SpeechHistory(Plugin): mainBox.pack_start(filterRow, False, False, 0) # List - self._listStore = Gtk.ListStore(int, str) + self._listStore = Gtk.ListStore(str) self._filterModel = self._listStore.filter_new() self._filterModel.set_visible_func(self._filter_visible_func) @@ -163,16 +167,10 @@ class SpeechHistory(Plugin): selection = self._treeView.get_selection() selection.set_mode(Gtk.SelectionMode.SINGLE) - idxRenderer = Gtk.CellRendererText() - idxColumn = Gtk.TreeViewColumn("Item", idxRenderer, text=0) - idxColumn.set_resizable(False) - idxColumn.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) - self._treeView.append_column(idxColumn) - textRenderer = Gtk.CellRendererText() textRenderer.set_property("wrap-width", 640) textRenderer.set_property("wrap-mode", 2) # Pango.WrapMode.WORD_CHAR - textColumn = Gtk.TreeViewColumn("Spoken Text", textRenderer, text=1) + textColumn = Gtk.TreeViewColumn("Spoken Text", textRenderer, text=0) textColumn.set_resizable(True) textColumn.set_expand(True) self._treeView.append_column(textColumn) @@ -221,7 +219,7 @@ class SpeechHistory(Plugin): if not filterText: return True - spokenText = model[treeIter][1] or "" + spokenText = model[treeIter][0] or "" return spokenText.lower().startswith(filterText) except Exception as e: debug.printMessage(debug.LEVEL_INFO, f"SpeechHistory: ERROR in filter func: {e}", True) @@ -236,11 +234,21 @@ class SpeechHistory(Plugin): self._listStore.clear() items = speech_history.get_items() - for idx, item in enumerate(items, start=1): - self._listStore.append([idx, item]) + debug.printMessage( + debug.LEVEL_INFO, + f"SpeechHistory: Retrieved {len(items)} items (paused={speech_history.is_capture_paused()})", + True, + ) + for item in items: + self._listStore.append([item]) if self._filterModel: self._filterModel.refilter() + debug.printMessage( + debug.LEVEL_INFO, + f"SpeechHistory: Filtered items visible={len(self._filterModel)} filter='{self._filterText}'", + True, + ) if selectFirst and self._treeView and len(self._filterModel) > 0: selection = self._treeView.get_selection() @@ -259,7 +267,7 @@ class SpeechHistory(Plugin): if not treeIter: return None - return model[treeIter][1] + return model[treeIter][0] except Exception as e: debug.printMessage(debug.LEVEL_INFO, f"SpeechHistory: ERROR getting selection: {e}", True) logger.exception("Error getting selected speech history item") @@ -403,4 +411,3 @@ class SpeechHistory(Plugin): except Exception as e: debug.printMessage(debug.LEVEL_INFO, f"SpeechHistory: ERROR presenting message: {e}", True) logger.exception("Error presenting message from SpeechHistory") - diff --git a/src/cthulhu/plugins/hello_world/__init__.py b/src/cthulhu/plugins/hello_world/__init__.py index 782103c..301e5ea 100644 --- a/src/cthulhu/plugins/hello_world/__init__.py +++ b/src/cthulhu/plugins/hello_world/__init__.py @@ -20,6 +20,6 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu diff --git a/src/cthulhu/plugins/self_voice/__init__.py b/src/cthulhu/plugins/self_voice/__init__.py index 782103c..301e5ea 100644 --- a/src/cthulhu/plugins/self_voice/__init__.py +++ b/src/cthulhu/plugins/self_voice/__init__.py @@ -20,6 +20,6 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu diff --git a/src/cthulhu/plugins/self_voice/plugin.py b/src/cthulhu/plugins/self_voice/plugin.py index 5a52d55..e2893ff 100644 --- a/src/cthulhu/plugins/self_voice/plugin.py +++ b/src/cthulhu/plugins/self_voice/plugin.py @@ -110,8 +110,8 @@ class SelfVoice(Plugin): else: # Use the script manager for standard messages script_manager = self.app.getDynamicApiManager().getAPI('ScriptManager') - scriptManager = script_manager.getManager() - scriptManager.getDefaultScript().presentMessage(message, resetStyles=False) + scriptManager = script_manager.get_manager() + scriptManager.get_default_script().presentMessage(message, resetStyles=False) def voiceWorker(self): """Worker thread that listens on a socket for messages to speak.""" diff --git a/src/cthulhu/pronunciation_dict.py b/src/cthulhu/pronunciation_dict.py index b199a44..d6c75fd 100644 --- a/src/cthulhu/pronunciation_dict.py +++ b/src/cthulhu/pronunciation_dict.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Exposes a dictionary, pronunciation_dict, that maps words to what they sound like.""" diff --git a/src/cthulhu/punctuation_settings.py b/src/cthulhu/punctuation_settings.py index 4f6ee32..f825685 100644 --- a/src/cthulhu/punctuation_settings.py +++ b/src/cthulhu/punctuation_settings.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Punctuation Verbosity settings. The Cthulhu punctuation settings are broken up into 4 modes. diff --git a/src/cthulhu/resource_manager.py b/src/cthulhu/resource_manager.py index d06705f..c768e10 100644 --- a/src/cthulhu/resource_manager.py +++ b/src/cthulhu/resource_manager.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu import traceback diff --git a/src/cthulhu/script.py b/src/cthulhu/script.py index f6c7c0a..29812ed 100644 --- a/src/cthulhu/script.py +++ b/src/cthulhu/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Each script maintains a set of key bindings, braille bindings, and AT-SPI event listeners. The key bindings are an instance of @@ -36,7 +36,7 @@ script manager. This Script class is not intended to be instantiated directly. Instead, it is expected that subclasses of the Script class will be created in their own module. The module defining the Script subclass -is also required to have a 'getScript(app)' method that returns an +is also required to have a 'get_script(app)' method that returns an instance of the Script subclass. See default.py for an example.""" __id__ = "$Id$" @@ -79,7 +79,7 @@ from . import where_am_i_presenter from .ax_object import AXObject _eventManager = event_manager.getManager() -_scriptManager = script_manager.getManager() +_scriptManager = script_manager.get_manager() _settingsManager = settings_manager.getManager() class Script: @@ -119,11 +119,11 @@ class Script: self.flatReviewPresenter = self.getFlatReviewPresenter() self.speechAndVerbosityManager = self.getSpeechAndVerbosityManager() self.dateAndTimePresenter = self.getDateAndTimePresenter() - self.objectNavigator = self.getObjectNavigator() + self.objectNavigator = self.get_objectNavigator() self.whereAmIPresenter = self.getWhereAmIPresenter() self.learnModePresenter = self.getLearnModePresenter() self.mouseReviewer = self.getMouseReviewer() - self.eventSynthesizer = self.getEventSynthesizer() + self.eventSynthesizer = self.get_event_synthesizer() self.actionPresenter = self.getActionPresenter() self.chat = self.getChat() @@ -292,7 +292,7 @@ class Script: def getDateAndTimePresenter(self): return date_and_time_presenter.getPresenter() - def getObjectNavigator(self): + def get_objectNavigator(self): return object_navigator.getNavigator() def getSpeechAndVerbosityManager(self): @@ -310,8 +310,8 @@ class Script: def getMouseReviewer(self): return mouse_review.getReviewer() - def getEventSynthesizer(self): - return ax_event_synthesizer.getSynthesizer() + def get_event_synthesizer(self): + return ax_event_synthesizer.get_synthesizer() def useStructuralNavigationModel(self, debugOutput=True): """Returns True if we should use structural navigation. Most @@ -598,7 +598,7 @@ class Script: return consumed - def locusOfFocusChanged(self, event, oldLocusOfFocus, newLocusOfFocus): + def locus_of_focus_changed(self, event, oldLocusOfFocus, newLocusOfFocus): """Called when the visual object with focus changes. The primary purpose of this method is to present locus of focus diff --git a/src/cthulhu/script_manager.py b/src/cthulhu/script_manager.py index f01c409..f21abd3 100644 --- a/src/cthulhu/script_manager.py +++ b/src/cthulhu/script_manager.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" @@ -34,9 +34,13 @@ import importlib from . import debug from . import cthulhu_state from .ax_object import AXObject -from .ax_utilities import AXUtilities from .scripts import apps, toolkits +def _get_ax_utilities(): + # Avoid circular import with ax_utilities -> ax_utilities_event -> focus_manager -> braille -> settings_manager -> script_manager. + from .ax_utilities import AXUtilities + return AXUtilities + class ScriptManager: def __init__(self): @@ -67,7 +71,7 @@ class ScriptManager: self._toolkitNames = \ {'WebKitGTK': 'WebKitGtk', 'GTK': 'gtk'} - self.setActiveScript(None, "__init__") + self.set_active_script(None, "__init__") self._active = False debug.printMessage(debug.LEVEL_INFO, "SCRIPT MANAGER: Initialized", True) @@ -75,9 +79,9 @@ class ScriptManager: """Called when this script manager is activated.""" debug.printMessage(debug.LEVEL_INFO, "SCRIPT MANAGER: Activating", True) - self._defaultScript = self.getScript(None) + self._defaultScript = self.get_script(None) self._defaultScript.registerEventListeners() - self.setActiveScript(self._defaultScript, "activate") + self.set_active_script(self._defaultScript, "activate") self._active = True debug.printMessage(debug.LEVEL_INFO, "SCRIPT MANAGER: Activated", True) @@ -88,14 +92,14 @@ class ScriptManager: if self._defaultScript: self._defaultScript.deregisterEventListeners() self._defaultScript = None - self.setActiveScript(None, "deactivate") + self.set_active_script(None, "deactivate") self.appScripts = {} self.toolkitScripts = {} self.customScripts = {} self._active = False debug.printMessage(debug.LEVEL_INFO, "SCRIPT MANAGER: Deactivated", True) - def getModuleName(self, app): + def get_module_name(self, app): """Returns the module name of the script to use for application app.""" if app is None: @@ -129,19 +133,19 @@ class ScriptManager: debug.printTokens(debug.LEVEL_INFO, tokens, True) return name - def _toolkitForObject(self, obj): + def _toolkit_for_object(self, obj): """Returns the name of the toolkit associated with obj.""" name = AXObject.get_attribute(obj, 'toolkit') return self._toolkitNames.get(name, name) - def _scriptForRole(self, obj): - if AXUtilities.is_terminal(obj): + def _script_for_role(self, obj): + if _get_ax_utilities().is_terminal(obj): return 'terminal' return '' - def _newNamedScript(self, app, name): + def _new_named_script(self, app, name): """Attempts to locate and load the named module. If successful, returns a script based on this module.""" @@ -161,8 +165,8 @@ class ScriptManager: tokens = ["SCRIPT MANAGER: Found", moduleName] debug.printTokens(debug.LEVEL_INFO, tokens, True) try: - if hasattr(module, 'getScript'): - script = module.getScript(app) + if hasattr(module, 'get_script'): + script = module.get_script(app) else: script = module.Script(app) break @@ -172,31 +176,31 @@ class ScriptManager: return script - def _createScript(self, app, obj=None): + def _create_script(self, app, obj=None): """For the given application, create a new script instance.""" - moduleName = self.getModuleName(app) - script = self._newNamedScript(app, moduleName) + moduleName = self.get_module_name(app) + script = self._new_named_script(app, moduleName) if script: return script - objToolkit = self._toolkitForObject(obj) - script = self._newNamedScript(app, objToolkit) + objToolkit = self._toolkit_for_object(obj) + script = self._new_named_script(app, objToolkit) if script: return script - toolkitName = AXObject.get_application_toolkit_name(app) + toolkitName = _get_ax_utilities().get_application_toolkit_name(app) if app and toolkitName: - script = self._newNamedScript(app, toolkitName) + script = self._new_named_script(app, toolkitName) if not script: - script = self.getDefaultScript(app) + script = self.get_default_script(app) tokens = ["SCRIPT MANAGER: Default script created for", app, "(obj: ", obj, ")"] debug.printTokens(debug.LEVEL_INFO, tokens, True) return script - def getDefaultScript(self, app=None): + def get_default_script(self, app=None): if not app and self._defaultScript: return self._defaultScript @@ -208,14 +212,14 @@ class ScriptManager: return script - def sanityCheckScript(self, script): + def sanity_check_script(self, script): if not self._active: return script - if AXUtilities.is_application_in_desktop(script.app): + if _get_ax_utilities().is_application_in_desktop(script.app): return script - newScript = self._getScriptForAppReplicant(script.app) + newScript = self._get_script_for_app_replicant(script.app) if newScript: return newScript @@ -223,26 +227,29 @@ class ScriptManager: debug.printTokens(debug.LEVEL_INFO, tokens, True) return script - def getScriptForMouseButtonEvent(self, event): - isActive = AXUtilities.is_active(cthulhu_state.activeWindow) + def get_script_for_mouse_button_event(self, event): + isActive = _get_ax_utilities().is_active(cthulhu_state.activeWindow) tokens = ["SCRIPT MANAGER:", cthulhu_state.activeWindow, "is active:", isActive] debug.printTokens(debug.LEVEL_INFO, tokens, True) if isActive and cthulhu_state.activeScript: return cthulhu_state.activeScript - script = self.getDefaultScript() + script = self.get_default_script() activeWindow = script.utilities.activeWindow() if not activeWindow: return script - focusedObject = AXUtilities.get_focused_object(activeWindow) + focusedObject = _get_ax_utilities().get_focused_object(activeWindow) if focusedObject: - return self.getScript(AXObject.get_application(focusedObject), focusedObject) + return self.get_script(AXObject.get_application(focusedObject), focusedObject) - return self.getScript(AXObject.get_application(activeWindow), activeWindow) + return self.get_script(AXObject.get_application(activeWindow), activeWindow) - def getScript(self, app, obj=None, sanityCheck=False): + def get_active_script(self): + return cthulhu_state.activeScript + + def get_script(self, app, obj=None, sanity_check=False): """Get a script for an app (and make it if necessary). This is used instead of a simple calls to Script's constructor. @@ -256,52 +263,52 @@ class ScriptManager: appScript = None toolkitScript = None - roleName = self._scriptForRole(obj) + roleName = self._script_for_role(obj) if roleName: customScripts = self.customScripts.get(app, {}) customScript = customScripts.get(roleName) if not customScript: - customScript = self._newNamedScript(app, roleName) + customScript = self._new_named_script(app, roleName) customScripts[roleName] = customScript self.customScripts[app] = customScripts - objToolkit = self._toolkitForObject(obj) + objToolkit = self._toolkit_for_object(obj) if objToolkit: toolkitScripts = self.toolkitScripts.get(app, {}) toolkitScript = toolkitScripts.get(objToolkit) if not toolkitScript: - toolkitScript = self._createScript(app, obj) + toolkitScript = self._create_script(app, obj) toolkitScripts[objToolkit] = toolkitScript self.toolkitScripts[app] = toolkitScripts try: if not app: - appScript = self.getDefaultScript() + appScript = self.get_default_script() elif app in self.appScripts: appScript = self.appScripts[app] else: - appScript = self._createScript(app, None) + appScript = self._create_script(app, None) self.appScripts[app] = appScript except Exception as error: tokens = ["SCRIPT MANAGER: Exception getting app script for", app, ":", error] debug.printTokens(debug.LEVEL_INFO, tokens, True) - appScript = self.getDefaultScript() + appScript = self.get_default_script() if customScript: return customScript # Only defer to the toolkit script for this object if the app script # is based on a different toolkit. - if toolkitScript and not (AXUtilities.is_frame(obj) or AXUtilities.is_status_bar(obj)) \ + if toolkitScript and not (_get_ax_utilities().is_frame(obj) or _get_ax_utilities().is_status_bar(obj)) \ and not issubclass(appScript.__class__, toolkitScript.__class__): return toolkitScript - if app and sanityCheck: - appScript = self.sanityCheckScript(appScript) + if app and sanity_check: + appScript = self.sanity_check_script(appScript) return appScript - def getOrCreateSleepModeScript(self, app): + def get_or_create_sleep_mode_script(self, app): """Gets or creates the sleep mode script.""" script = self._sleepModeScripts.get(app) if script is not None: @@ -313,7 +320,7 @@ class ScriptManager: self._sleepModeScripts[app] = script return script - def setActiveScript(self, newScript, reason=None): + def set_active_script(self, newScript, reason=None): """Set the new active script. Arguments: @@ -341,7 +348,7 @@ class ScriptManager: tokens = ["SCRIPT MANAGER: Setting active script to", newScript, "reason:", reason] debug.printTokens(debug.LEVEL_INFO, tokens, True) - def _getScriptForAppReplicant(self, app): + def _get_script_for_app_replicant(self, app): if not self._active: return None @@ -353,7 +360,7 @@ class ScriptManager: for a, script in items: if AXObject.get_process_id(a) != pid: continue - if a != app and AXUtilities.is_application_in_desktop(a): + if a != app and _get_ax_utilities().is_application_in_desktop(a): if script.app is None: script.app = a tokens = ["SCRIPT MANAGER: Script for app replicant:", script, script.app] @@ -362,7 +369,7 @@ class ScriptManager: return None - def reclaimScripts(self): + def reclaim_scripts(self): """Compares the list of known scripts to the list of known apps, deleting any scripts as necessary. """ @@ -372,7 +379,7 @@ class ScriptManager: appList = list(self.appScripts.keys()) for app in appList: - if AXUtilities.is_application_in_desktop(app): + if _get_ax_utilities().is_application_in_desktop(app): continue try: @@ -385,7 +392,7 @@ class ScriptManager: tokens = ["SCRIPT MANAGER: Old script for app found:", appScript, appScript.app] debug.printTokens(debug.LEVEL_INFO, tokens, True) - newScript = self._getScriptForAppReplicant(app) + newScript = self._get_script_for_app_replicant(app) if newScript: tokens = ["SCRIPT MANAGER: Transferring attributes:", newScript, newScript.app] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -417,5 +424,5 @@ class ScriptManager: _manager = ScriptManager() -def getManager(): +def get_manager(): return _manager diff --git a/src/cthulhu/script_utilities.py b/src/cthulhu/script_utilities.py index fce7fe6..f8b1c03 100644 --- a/src/cthulhu/script_utilities.py +++ b/src/cthulhu/script_utilities.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Commonly-required utility methods needed by -- and potentially customized by -- application and toolkit scripts. They have @@ -54,6 +54,7 @@ from . import debug from . import keynames from . import keybindings from . import input_event +from . import input_event_manager from . import mathsymbols from . import messages from . import cthulhu @@ -65,7 +66,12 @@ from . import settings_manager from . import text_attribute_names from .ax_object import AXObject from .ax_selection import AXSelection +from .ax_table import AXTable +from .ax_text import AXText +from .ax_hypertext import AXHypertext +from .ax_value import AXValue from .ax_utilities import AXUtilities +from .ax_utilities_relation import AXUtilitiesRelation _settingsManager = settings_manager.getManager() @@ -274,13 +280,12 @@ class Utilities: """ parent = AXObject.get_parent(obj) - try: - table = parent.queryTable() - except Exception: + table = AXTable.get_table(parent) + if table is None: + return [] + + if not AXUtilities.is_expanded(obj): return [] - else: - if not AXUtilities.is_expanded(obj): - return [] # First see if this accessible implements RELATION_NODE_PARENT_OF. # If it does, the full target list are the nodes. If it doesn't @@ -288,7 +293,7 @@ class Utilities: def pred(x): return AXObject.get_index_in_parent(x) >= 0 - nodes = AXObject.get_relation_targets(obj, Atspi.RelationType.NODE_PARENT_OF, pred) + nodes = [x for x in AXUtilitiesRelation.get_is_node_parent_of(obj) if pred(x)] tokens = ["SCRIPT UTILITIES:", len(nodes), "child nodes for", obj, "via node-parent-of"] debug.printTokens(debug.LEVEL_INFO, tokens, True) if nodes: @@ -301,13 +306,14 @@ class Utilities: # row, col = self.coordinatesForCell(obj) nodeLevel = self.nodeLevel(obj) - for i in range(row+1, table.nRows): - cell = table.getAccessibleAt(i, col) - relation = AXObject.get_relation(cell, Atspi.RelationType.NODE_CHILD_OF) - if not relation: + for i in range(row + 1, AXTable.get_row_count(table, prefer_attribute=False)): + cell = AXTable.get_cell_at(table, i, col) + if not cell: continue - - nodeOf = relation.get_target(0) + nodeOf = AXUtilitiesRelation.get_is_node_child_of(cell) + if not nodeOf: + continue + nodeOf = nodeOf[0] if self.isSameObject(obj, nodeOf): nodes.append(cell) elif self.nodeLevel(nodeOf) <= nodeLevel: @@ -397,11 +403,11 @@ class Utilities: def descriptionsForObject(self, obj): """Return a list of objects describing obj.""" - descriptions = AXObject.get_relation_targets(obj, Atspi.RelationType.DESCRIBED_BY) + descriptions = AXUtilitiesRelation.get_is_described_by(obj) if not descriptions: return [] - labels = AXObject.get_relation_targets(obj, Atspi.RelationType.LABELLED_BY) + labels = AXUtilitiesRelation.get_is_labelled_by(obj) if descriptions == labels: tokens = ["SCRIPT UTILITIES:", obj, "'s described-by targets are the same as labelled-by targets"] @@ -417,7 +423,7 @@ class Utilities: def detailsForObject(self, obj, textOnly=True): """Return a list of objects containing details for obj.""" - details = AXObject.get_relation_targets(obj, Atspi.RelationType.DETAILS) + details = AXUtilitiesRelation.get_details(obj) if not details and AXUtilities.is_toggle_button(obj) \ and AXUtilities.is_expanded(obj): details = [child for child in AXObject.iter_children(obj)] @@ -467,9 +473,7 @@ class Utilities: return name if AXObject.supports_text(obj): - # We should be able to use -1 for the final offset, but that crashes Nautilus. - text = obj.queryText() - displayedText = text.getText(0, text.characterCount) + displayedText = AXText.get_all_text(obj) if self.EMBEDDED_OBJECT_CHARACTER in displayedText: displayedText = None @@ -990,26 +994,17 @@ class Utilities: if not AXUtilities.is_progress_bar(obj): return False - try: - value = obj.queryValue() - except NotImplementedError: + if not AXObject.supports_value(obj): tokens = ["SCRIPT UTILITIES:", obj, "doesn't implement AtspiValue"] debug.printTokens(debug.LEVEL_INFO, tokens, True) return False - except Exception: - tokens = ["SCRIPT UTILITIES: Exception getting value for", obj] + + min_value = AXValue.get_minimum_value(obj) + max_value = AXValue.get_maximum_value(obj) + if max_value == min_value: + tokens = ["SCRIPT UTILITIES:", obj, "is busy indicator"] debug.printTokens(debug.LEVEL_INFO, tokens, True) return False - else: - try: - if value.maximumValue == value.minimumValue: - tokens = ["SCRIPT UTILITIES:", obj, "is busy indicator"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return False - except Exception: - tokens = ["SCRIPT UTILITIES:", obj, "is either busy indicator or broken"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return False return True @@ -1048,17 +1043,14 @@ class Utilities: return True, "Not handled by any other case" def getValueAsPercent(self, obj): - try: - value = obj.queryValue() - minval, val, maxval = value.minimumValue, value.currentValue, value.maximumValue - except NotImplementedError: + if not AXObject.supports_value(obj): tokens = ["SCRIPT UTILITIES:", obj, "doesn't implement AtspiValue"] debug.printTokens(debug.LEVEL_INFO, tokens, True) return None - except Exception: - tokens = ["SCRIPT UTILITIES: Exception getting value for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return None + + val = AXValue.get_current_value(obj) + minval = AXValue.get_minimum_value(obj) + maxval = AXValue.get_maximum_value(obj) if AXUtilities.is_indeterminate(obj): tokens = ["SCRIPT UTILITIES:", obj, "has state indeterminate and value of", val] @@ -1180,7 +1172,7 @@ class Utilities: if AXUtilities.is_document_spreadsheet(doc): return True - return obj.queryTable().nRows > 65536 + return AXTable.get_row_count(obj, prefer_attribute=False) > 65536 def isTextDocumentCell(self, obj): if not AXUtilities.is_table_cell_or_header(obj): @@ -1316,23 +1308,19 @@ class Utilities: Atspi.Role.TREE_ITEM] if role == Atspi.Role.TABLE and attrs.get('layout-guess') != 'true': - try: - table = obj.queryTable() - except NotImplementedError: + if not AXObject.supports_table(obj): tokens = ["SCRIPT UTILITIES: Table", obj, "does not implement table interface"] debug.printTokens(debug.LEVEL_INFO, tokens, True) layoutOnly = True - except Exception as error: - tokens = ["SCRIPT UTILITIES: Error querying table interface of", obj, ":", error] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - layoutOnly = True else: - if not (table.nRows and table.nColumns): + rows = AXTable.get_row_count(obj, prefer_attribute=False) + cols = AXTable.get_column_count(obj, prefer_attribute=False) + if not (rows and cols): layoutOnly = not AXUtilities.is_focused(obj) elif attrs.get('xml-roles') == 'table' or attrs.get('tag') == 'table': layoutOnly = False elif not (AXObject.get_name(obj) or self.displayedLabel(obj)): - layoutOnly = not (table.getColumnHeader(0) or table.getRowHeader(0)) + layoutOnly = not (AXTable.has_column_headers(obj) or AXTable.has_row_headers(obj)) elif role == Atspi.Role.TABLE_CELL and AXObject.get_child_count(obj): if parentRole == Atspi.Role.TREE_TABLE: layoutOnly = not AXObject.get_name(obj) @@ -1429,7 +1417,7 @@ class Utilities: def isSwitch(self, obj): return False - def getObjectFromPath(self, path): + def get_objectFromPath(self, path): start = self._script.app rv = None for p in path: @@ -1503,10 +1491,10 @@ class Utilities: # Comparing the extents of objects which claim to be different # addresses both managed descendants and implementations which # recreate accessibles for the same widget. - extents1 = \ - obj1.queryComponent().getExtents(Atspi.CoordType.WINDOW) - extents2 = \ - obj2.queryComponent().getExtents(Atspi.CoordType.WINDOW) + if not AXObject.supports_component(obj1) or not AXObject.supports_component(obj2): + return False + extents1 = Atspi.Component.get_extents(obj1, Atspi.CoordType.WINDOW) + extents2 = Atspi.Component.get_extents(obj2, Atspi.CoordType.WINDOW) # Objects which claim to be different and which are in different # locations are almost certainly not recreated objects. @@ -1548,7 +1536,7 @@ class Utilities: def isNotAncestor(acc): return not AXObject.find_ancestor(obj, lambda x: x == acc) - result = AXObject.get_relation_targets(obj, Atspi.RelationType.LABELLED_BY) + result = AXUtilitiesRelation.get_is_labelled_by(obj) return list(filter(isNotAncestor, result)) def linkBasenameToName(self, obj): @@ -1571,56 +1559,50 @@ class Utilities: basename command in a shell.""" basename = None + uri = AXHypertext.get_link_uri(obj) + if uri and len(uri): + # Sometimes the URI is an expression that includes a URL. + # Currently that can be found at the bottom of safeway.com. + # It can also be seen in the backwards.html test file. + # + expression = uri.split(',') + if len(expression) > 1: + for item in expression: + if item.find('://') >=0: + if not item[0].isalnum(): + item = item[1:-1] + if not item[-1].isalnum(): + item = item[0:-2] + uri = item + break - try: - hyperlink = obj.queryHyperlink() - except Exception: - pass - else: - uri = hyperlink.getURI(0) - if uri and len(uri): - # Sometimes the URI is an expression that includes a URL. - # Currently that can be found at the bottom of safeway.com. - # It can also be seen in the backwards.html test file. + # We're assuming that there IS a base name to be had. + # What if there's not? See backwards.html. + # + uri = uri.split('://')[-1] + if not uri: + return basename + + # Get the last thing after all the /'s, unless it ends + # in a /. If it ends in a /, we'll look to the stuff + # before the ending /. + # + if uri[-1] == "/": + basename = uri[0:-1] + basename = basename.split('/')[-1] + elif not uri.count("/"): + basename = uri + else: + basename = uri.split('/')[-1] + if basename.startswith("index"): + basename = uri.split('/')[-2] + + # Now, try to strip off the suffixes. # - expression = uri.split(',') - if len(expression) > 1: - for item in expression: - if item.find('://') >=0: - if not item[0].isalnum(): - item = item[1:-1] - if not item[-1].isalnum(): - item = item[0:-2] - uri = item - break - - # We're assuming that there IS a base name to be had. - # What if there's not? See backwards.html. - # - uri = uri.split('://')[-1] - if not uri: - return basename - - # Get the last thing after all the /'s, unless it ends - # in a /. If it ends in a /, we'll look to the stuff - # before the ending /. - # - if uri[-1] == "/": - basename = uri[0:-1] - basename = basename.split('/')[-1] - elif not uri.count("/"): - basename = uri - else: - basename = uri.split('/')[-1] - if basename.startswith("index"): - basename = uri.split('/')[-2] - - # Now, try to strip off the suffixes. - # - basename = basename.split('.')[0] - basename = basename.split('?')[0] - basename = basename.split('#')[0] - basename = basename.split('%')[0] + basename = basename.split('.')[0] + basename = basename.split('?')[0] + basename = basename.split('#')[0] + basename = basename.split('%')[0] return basename @@ -1639,20 +1621,16 @@ class Utilities: if not obj: return -1 - try: - obj.queryText() - except NotImplementedError: + if not AXObject.supports_text(obj): return -1 - try: - hypertext = obj.queryHypertext() - except NotImplementedError: + if not AXObject.supports_hypertext(obj): return -1 - for i in range(hypertext.getNLinks()): - link = hypertext.getLink(i) - if (characterIndex >= link.startIndex) \ - and (characterIndex <= link.endIndex): + for i, link in enumerate(AXHypertext.get_all_links(obj)): + start = AXHypertext.get_link_start_offset(link) + end = AXHypertext.get_link_end_offset(link) + if (characterIndex >= start) and (characterIndex <= end): return i return -1 @@ -1712,10 +1690,8 @@ class Utilities: node = obj done = False while not done: - relation = AXObject.get_relation(node, Atspi.RelationType.NODE_CHILD_OF) - node = None - if relation: - node = relation.get_target(0) + nodeOf = AXUtilitiesRelation.get_is_node_child_of(node) + node = nodeOf[0] if nodeOf else None # We want to avoid situations where something gives us an # infinite cycle of nodes. Bon Echo has been seen to do @@ -1748,8 +1724,13 @@ class Utilities: debug.printTokens(debug.LEVEL_INFO, tokens, True) return False + if not AXObject.supports_component(obj): + tokens = ["SCRIPT UTILITIES:", obj, "does not support component interface"] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + return False + try: - box = obj.queryComponent().getExtents(Atspi.CoordType.SCREEN) + box = Atspi.Component.get_extents(obj, Atspi.CoordType.SCREEN) except Exception: tokens = ["SCRIPT UTILITIES: Exception getting extents for", obj] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -1758,17 +1739,20 @@ class Utilities: tokens = ["SCRIPT UTILITIES: Extents for", obj, "are:", box] debug.printTokens(debug.LEVEL_INFO, tokens, True) - if box.x > 10000 or box.y > 10000: + boxTuple = self._extentsToTuple(box) + x, y, width, height = boxTuple + + if x > 10000 or y > 10000: tokens = ["SCRIPT UTILITIES:", obj, "seems to have bogus coordinates"] debug.printTokens(debug.LEVEL_INFO, tokens, True) return False - if box.x < 0 and box.y < 0 and tuple(box) != (-1, -1, -1, -1): + if x < 0 and y < 0 and boxTuple != (-1, -1, -1, -1): tokens = ["SCRIPT UTILITIES:", obj, "has negative coordinates"] debug.printTokens(debug.LEVEL_INFO, tokens, True) return False - if not (box.width or box.height): + if not (width or height): if not AXObject.get_child_count(obj): tokens = ["SCRIPT UTILITIES:", obj, "has no size and no children"] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -1783,7 +1767,8 @@ class Utilities: if boundingbox is None or not self._boundsIncludeChildren(AXObject.get_parent(obj)): return True - if not self.containsRegion(box, boundingbox) and tuple(box) != (-1, -1, -1, -1): + boundingbox = self._extentsToTuple(boundingbox) + if not self.containsRegion(boxTuple, boundingbox) and boxTuple != (-1, -1, -1, -1): tokens = ["SCRIPT UTILITIES:", obj, box, "not in", boundingbox] debug.printTokens(debug.LEVEL_INFO, tokens, True) return False @@ -1843,7 +1828,7 @@ class Utilities: if not AXObject.supports_text(obj): return False - return bool(re.search(r"\w+", obj.queryText().getText(0, -1))) + return bool(re.search(r"\w+", AXText.get_all_text(obj))) def getOnScreenObjects(self, root, extents=None): if not self.isOnScreen(root, extents): @@ -1871,13 +1856,15 @@ class Utilities: debug.printTokens(debug.LEVEL_INFO, tokens, True) if extents is None: - try: - component = root.queryComponent() - extents = component.getExtents(Atspi.CoordType.SCREEN) - except Exception: - tokens = ["SCRIPT UTILITIES: Exception getting extents of", root] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - extents = 0, 0, 0, 0 + if AXObject.supports_component(root): + try: + extents = Atspi.Component.get_extents(root, Atspi.CoordType.SCREEN) + except Exception: + tokens = ["SCRIPT UTILITIES: Exception getting extents of", root] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + extents = Atspi.Rect() + else: + extents = Atspi.Rect() if AXObject.supports_table(root) and AXObject.supports_selection(root): visibleCells = self.getVisibleTableCells(root) @@ -1980,7 +1967,7 @@ class Utilities: return obj def pred(x): - return x and not self.isStaticTextLeaf(x) and self.displayedText(x).strip() + return AXObject.get_name(x) or AXText.get_all_text(x) child = AXObject.find_descendant(obj, pred) if child is not None: @@ -2103,9 +2090,12 @@ class Utilities: def onSameLine(obj1, obj2, delta=0): """Determines if obj1 and obj2 are on the same line.""" + if not AXObject.supports_component(obj1) or not AXObject.supports_component(obj2): + return False + try: - bbox1 = obj1.queryComponent().getExtents(Atspi.CoordType.SCREEN) - bbox2 = obj2.queryComponent().getExtents(Atspi.CoordType.SCREEN) + bbox1 = Atspi.Component.get_extents(obj1, Atspi.CoordType.SCREEN) + bbox2 = Atspi.Component.get_extents(obj2, Atspi.CoordType.SCREEN) except Exception: return False @@ -2136,17 +2126,22 @@ class Utilities: @staticmethod def sizeComparison(obj1, obj2): - try: - bbox = obj1.queryComponent().getExtents(Atspi.CoordType.SCREEN) - width1, height1 = bbox.width, bbox.height - except Exception: - width1, height1 = 0, 0 + width1, height1 = 0, 0 + width2, height2 = 0, 0 - try: - bbox = obj2.queryComponent().getExtents(Atspi.CoordType.SCREEN) - width2, height2 = bbox.width, bbox.height - except Exception: - width2, height2 = 0, 0 + if AXObject.supports_component(obj1): + try: + bbox = Atspi.Component.get_extents(obj1, Atspi.CoordType.SCREEN) + width1, height1 = bbox.width, bbox.height + except Exception: + pass + + if AXObject.supports_component(obj2): + try: + bbox = Atspi.Component.get_extents(obj2, Atspi.CoordType.SCREEN) + width2, height2 = bbox.width, bbox.height + except Exception: + pass return (width1 * height1) - (width2 * height2) @@ -2156,17 +2151,22 @@ class Utilities: 0, or 1 to indicate if obj1 physically is before, is in the same place as, or is after obj2.""" - try: - bbox = obj1.queryComponent().getExtents(Atspi.CoordType.SCREEN) - x1, y1 = bbox.x, bbox.y - except Exception: - x1, y1 = 0, 0 + x1, y1 = 0, 0 + x2, y2 = 0, 0 - try: - bbox = obj2.queryComponent().getExtents(Atspi.CoordType.SCREEN) - x2, y2 = bbox.x, bbox.y - except Exception: - x2, y2 = 0, 0 + if AXObject.supports_component(obj1): + try: + bbox = Atspi.Component.get_extents(obj1, Atspi.CoordType.SCREEN) + x1, y1 = bbox.x, bbox.y + except Exception: + pass + + if AXObject.supports_component(obj2): + try: + bbox = Atspi.Component.get_extents(obj2, Atspi.CoordType.SCREEN) + x2, y2 = bbox.x, bbox.y + except Exception: + pass rv = y1 - y2 or x1 - x2 @@ -2183,8 +2183,11 @@ class Utilities: return rv def getTextBoundingBox(self, obj, start, end): + if not AXObject.supports_text(obj): + return -1, -1, 0, 0 + try: - extents = obj.queryText().getRangeExtents(start, end, Atspi.CoordType.SCREEN) + extents = Atspi.Text.get_range_extents(obj, start, end, Atspi.CoordType.SCREEN) except Exception: tokens = ["SCRIPT UTILITIES: Exception getting range extents of", obj] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -2193,8 +2196,11 @@ class Utilities: return extents def getBoundingBox(self, obj): + if not AXObject.supports_component(obj): + return -1, -1, 0, 0 + try: - extents = obj.queryComponent().getExtents(Atspi.CoordType.SCREEN) + extents = Atspi.Component.get_extents(obj, Atspi.CoordType.SCREEN) except Exception: tokens = ["SCRIPT UTILITIES: Exception getting extents of", obj] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -2209,8 +2215,11 @@ class Utilities: if AXUtilities.is_application(obj): return False + if not AXObject.supports_component(obj): + return True + try: - extents = obj.queryComponent().getExtents(Atspi.CoordType.SCREEN) + extents = Atspi.Component.get_extents(obj, Atspi.CoordType.SCREEN) except Exception: tokens = ["SCRIPT UTILITIES: Exception getting extents for", obj] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -2251,7 +2260,7 @@ class Utilities: def _include(x): if not (x and AXObject.get_role(x) in labelRoles): return False - if AXObject.get_relations(x): + if AXUtilitiesRelation.get_relations(x): return False if onlyShowing and not AXUtilities.is_showing(x): return False @@ -2326,10 +2335,8 @@ class Utilities: - obj: the Accessible object. """ - try: - return obj.queryHyperlink().getURI(0) - except Exception: - return None + uri = AXHypertext.get_link_uri(obj) + return uri or None ######################################################################### # # @@ -2347,23 +2354,22 @@ class Utilities: depending on the direction of selection """ - try: - text = obj.queryText() - except Exception: + if not AXObject.supports_text(obj): return - if text.getNSelections() <= 0: - caretOffset = text.caretOffset + selections = AXText.get_selected_ranges(obj) + if not selections: + caretOffset = AXText.get_caret_offset(obj) startOffset = min(offset, caretOffset) endOffset = max(offset, caretOffset) - text.addSelection(startOffset, endOffset) else: - startOffset, endOffset = text.getSelection(0) + startOffset, endOffset = selections[0] if offset < startOffset: startOffset = offset else: endOffset = offset - text.setSelection(0, startOffset, endOffset) + + AXText.set_selected_text(obj, startOffset, endOffset) def findPreviousObject(self, obj): """Finds the object before this one.""" @@ -2371,11 +2377,11 @@ class Utilities: if not obj or self.isZombie(obj): return None - relation = AXObject.get_relation(obj, Atspi.RelationType.FLOWS_FROM) - if relation: - return relation.get_target(0) + flowsFrom = AXUtilitiesRelation.get_flows_from(obj) + if flowsFrom: + return flowsFrom[0] - return AXObject.get_previous_object(obj) + return AXUtilities.get_previous_object(obj) def findNextObject(self, obj): """Finds the object after this one.""" @@ -2383,11 +2389,11 @@ class Utilities: if not obj or self.isZombie(obj): return None - relation = AXObject.get_relation(obj, Atspi.RelationType.FLOWS_TO) - if relation: - return relation.get_target(0) + flowsTo = AXUtilitiesRelation.get_flows_to(obj) + if flowsTo: + return flowsTo[0] - return AXObject.get_next_object(obj) + return AXUtilities.get_next_object(obj) def allSelectedText(self, obj): """Get all the text applicable text selections for the given object. @@ -2441,53 +2447,15 @@ class Utilities: the text interface. """ - try: - text = obj.queryText() - except Exception: - return [] - - rv = [] - try: - nSelections = text.getNSelections() - except Exception: - nSelections = 0 - for i in range(nSelections): - rv.append(text.getSelection(i)) - - return rv + return AXText.get_selected_ranges(obj) def getChildAtOffset(self, obj, offset): - try: - hypertext = obj.queryHypertext() - except NotImplementedError: - tokens = ["SCRIPT UTILITIES:", obj, "does not implement the hypertext interface"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return None - except Exception: - tokens = ["SCRIPT UTILITIES: Exception querying hypertext interface for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + child = AXHypertext.get_child_at_offset(obj, offset) + if not child: return None - index = hypertext.getLinkIndex(offset) - if index == -1: - return None - - hyperlink = hypertext.getLink(index) - if not hyperlink: - tokens = ["SCRIPT UTILITIES: No hyperlink object at index", index, "for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return None - - child = hyperlink.getObject(0) - tokens = ["SCRIPT UTILITIES: Hyperlink object at index", index, "for", obj, "is", child] + tokens = ["SCRIPT UTILITIES: Hyperlink object at offset", offset, "for", obj, "is", child] debug.printTokens(debug.LEVEL_INFO, tokens, True) - if offset != hyperlink.startIndex: - msg = ( - f"SCRIPT UTILITIES: Hyperlink start index {hyperlink.startIndex} " - f"should match the offset {offset}" - ) - debug.printMessage(debug.LEVEL_INFO, msg, True) - return child def findChildAtOffset(self, obj, offset): @@ -2537,27 +2505,19 @@ class Utilities: """ offset = -1 - try: - hyperlink = obj.queryHyperlink() - except NotImplementedError: - tokens = ["SCRIPT UTILITIES:", obj, "does not implement the hyperlink interface"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + # We need to make sure that this is an embedded object in + # some accessible text (as opposed to an imagemap link). + # + parent = AXObject.get_parent(obj) + if AXObject.supports_text(parent): + offset = AXHypertext.get_link_start_offset(obj) else: - # We need to make sure that this is an embedded object in - # some accessible text (as opposed to an imagemap link). - # - parent = AXObject.get_parent(obj) - try: - parent.queryText() - offset = hyperlink.startIndex - except Exception: - tokens = ["SCRIPT UTILITIES: Exception getting startIndex for", - obj, "in parent", parent] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - else: - tokens = ["SCRIPT UTILITIES: startIndex of", obj, f"is {offset}"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + tokens = ["SCRIPT UTILITIES: Exception getting startIndex for", + obj, "in parent", parent] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + tokens = ["SCRIPT UTILITIES: startIndex of", obj, f"is {offset}"] + debug.printTokens(debug.LEVEL_INFO, tokens, True) return offset def clearTextSelection(self, obj): @@ -2567,20 +2527,14 @@ class Utilities: - obj: the Accessible object. """ - try: - text = obj.queryText() - except Exception: + if not AXObject.supports_text(obj): return - for i in range(text.getNSelections()): - text.removeSelection(i) + for i in range(AXText._get_n_selections(obj)): + AXText._remove_selection(obj, i) def containsOnlyEOCs(self, obj): - try: - string = obj.queryText().getText(0, -1) - except Exception: - return False - + string = AXText.get_all_text(obj) return string and not re.search(r"[^\ufffc]", string) def expandEOCs(self, obj, startOffset=0, endOffset=-1): @@ -2657,11 +2611,10 @@ class Utilities: return False def getCharacterAtOffset(self, obj, offset=None): - text = self.queryNonEmptyText(obj) - if text: + if AXObject.supports_text(obj): if offset is None: - offset = text.caretOffset - return text.getText(offset, offset + 1) + offset = AXText.get_caret_offset(obj) + return AXText.get_substring(obj, offset, offset + 1) return "" @@ -2673,17 +2626,12 @@ class Utilities: - obj: an accessible object """ - try: - text = obj.queryText() - charCount = text.characterCount - except NotImplementedError: - pass - except Exception: - tokens = ["SCRIPT UTILITIES: Exception getting character count of", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - else: - if charCount: - return text + if not AXObject.supports_text(obj): + return None + + charCount = AXText.get_character_count(obj) + if charCount: + return obj return None @@ -2700,7 +2648,7 @@ class Utilities: if AXUtilities.is_password_text(event.source): text = self.queryNonEmptyText(event.source) if text: - string = text.getText(0, -1) + string = AXText.get_all_text(event.source) if string: tokens = ["HACK: Returning last char in '", string, "'"] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -2722,31 +2670,24 @@ class Utilities: textContents = "" startOffset = endOffset = 0 - try: - textObj = obj.queryText() - except Exception: - nSelections = 0 - else: - nSelections = textObj.getNSelections() - - for i in range(0, nSelections): - [startOffset, endOffset] = textObj.getSelection(i) - if startOffset == endOffset: + for start, end in AXText.get_selected_ranges(obj): + if start == end: continue - selectedText = self.expandEOCs(obj, startOffset, endOffset) - if i > 0: + selectedText = self.expandEOCs(obj, start, end) + if textContents: textContents += " " textContents += selectedText + if startOffset == endOffset == 0: + startOffset = start + endOffset = end return [textContents, startOffset, endOffset] def getCaretContext(self): obj = cthulhu_state.locusOfFocus - try: - offset = obj.queryText().caretOffset - except NotImplementedError: - offset = 0 - except Exception: + if AXObject.supports_text(obj): + offset = AXText.get_caret_offset(obj) + else: offset = -1 return obj, offset @@ -2766,12 +2707,7 @@ class Utilities: - obj: Given accessible object. - offset: Offset to hich to set the caret. """ - try: - texti = obj.queryText() - except Exception: - return None - - texti.setCaretOffset(offset) + AXText.set_caret_offset(obj, offset) def substring(self, obj, startOffset, endOffset): """Returns the substring of the given object's text specialization. @@ -2783,12 +2719,7 @@ class Utilities: of -1 means the last character """ - try: - text = obj.queryText() - except Exception: - return "" - - return text.getText(startOffset, endOffset) + return AXText.get_substring(obj, startOffset, endOffset) def getAppNameForAttribute(self, attribName): """Converts the given Atk attribute name into the application's @@ -2824,13 +2755,11 @@ class Utilities: def getAllTextAttributesForObject(self, obj, startOffset=0, endOffset=-1): """Returns a list of (start, end, attrsDict) tuples for obj.""" - try: - text = obj.queryText() - except Exception: + if not AXObject.supports_text(obj): return [] if endOffset == -1: - endOffset = text.characterCount + endOffset = AXText.get_character_count(obj) tokens = ["SCRIPT UTILITIES: Getting text attributes for", obj, f"chars: {startOffset}-{endOffset}"] @@ -2840,17 +2769,10 @@ class Utilities: rv = [] offset = startOffset while offset < endOffset: - try: - attrList, start, end = text.getAttributeRun(offset) - tokens = [f"SCRIPT UTILITIES: At {offset}:", attrList, f"({start}, {end})"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - except Exception as error: - msg = f"SCRIPT UTILITIES: Exception getting attributes at {offset}: {error}" - debug.printMessage(debug.LEVEL_INFO, msg, True) - return rv - - attrDict = dict([attr.split(':', 1) for attr in attrList]) - rv.append((max(start, offset), end, attrDict)) + attrs, start, end = AXText.get_text_attributes_at_offset(obj, offset) + tokens = [f"SCRIPT UTILITIES: At {offset}:", attrs, f"({start}, {end})"] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + rv.append((max(start, offset), end, attrs)) offset = max(end, offset + 1) endTime = time.time() @@ -2873,27 +2795,14 @@ class Utilities: supprt the text attribute. """ - rv = {} - try: - text = acc.queryText() - except Exception: - return rv, 0, 0 - - if get_defaults: - stringAndDict = self.stringToKeysAndDict(text.getDefaultAttributes()) - rv.update(stringAndDict[1]) + if not AXObject.supports_text(acc): + return {}, 0, 0 if offset is None: - offset = text.caretOffset + offset = AXText.get_caret_offset(acc) - attrString, start, end = text.getAttributes(offset) - stringAndDict = self.stringToKeysAndDict(attrString) - rv.update(stringAndDict[1]) - - start = min(start, offset) - end = max(end, offset + 1) - - return rv, start, end + attrs, start, end = AXText.get_text_attributes_at_offset(acc, offset) + return attrs, min(start, offset), max(end, offset + 1) def localizeTextAttribute(self, key, value): if key == "weight" and (value == "bold" or int(value) > 400): @@ -3070,25 +2979,22 @@ class Utilities: from . import punctuation_settings endOffset = startOffset + len(line) - try: - hyperText = obj.queryHypertext() - nLinks = hyperText.getNLinks() - except Exception: - nLinks = 0 + links = AXHypertext.get_all_links(obj) if AXObject.supports_hypertext(obj) else [] adjustedLine = list(line) - for n in range(nLinks, 0, -1): - link = hyperText.getLink(n - 1) - if not link: + for link in reversed(links): + start_index = AXHypertext.get_link_start_offset(link) + end_index = AXHypertext.get_link_end_offset(link) + if start_index < 0 or end_index < 0: continue # We only care about links in the string, line: # - if startOffset < link.endIndex <= endOffset: - index = link.endIndex - startOffset - elif startOffset <= link.startIndex < endOffset: + if startOffset < end_index <= endOffset: + index = end_index - startOffset + elif startOffset <= start_index < endOffset: index = len(line) - if link.endIndex < endOffset: + if end_index < endOffset: index -= 1 else: continue @@ -3310,7 +3216,7 @@ class Utilities: if not self.lastInputEventWasPrintableKey(): return False - string = event.source.queryText().getText(0, -1) + string = AXText.get_all_text(event.source) if string.endswith(event.any_data): selection, start, end = self.selectedText(event.source) if selection == event.any_data: @@ -3359,15 +3265,28 @@ class Utilities: if coordType is None: coordType = Atspi.CoordType.SCREEN + if not AXObject.supports_component(obj1) or not AXObject.supports_component(obj2): + return 0, 0, 0, 0 + try: - extents1 = obj1.queryComponent().getExtents(coordType) - extents2 = obj2.queryComponent().getExtents(coordType) + extents1 = Atspi.Component.get_extents(obj1, coordType) + extents2 = Atspi.Component.get_extents(obj2, coordType) except Exception: return 0, 0, 0, 0 return self.intersection(extents1, extents2) def intersection(self, extents1, extents2): + def _toTuple(extents): + if extents is None: + return 0, 0, 0, 0 + if hasattr(extents, "x") and hasattr(extents, "y") \ + and hasattr(extents, "width") and hasattr(extents, "height"): + return extents.x, extents.y, extents.width, extents.height + return extents + + extents1 = _toTuple(extents1) + extents2 = _toTuple(extents2) x1, y1, width1, height1 = extents1 x2, y2, width2, height2 = extents2 @@ -3392,6 +3311,15 @@ class Utilities: def containsRegion(self, extents1, extents2): return self.intersection(extents1, extents2) != (0, 0, 0, 0) + @staticmethod + def _extentsToTuple(extents): + if extents is None: + return 0, 0, 0, 0 + if hasattr(extents, "x") and hasattr(extents, "y") \ + and hasattr(extents, "width") and hasattr(extents, "height"): + return extents.x, extents.y, extents.width, extents.height + return tuple(extents) + @staticmethod def _allNamesForKeyCode(keycode): keymap = Gdk.Keymap.get_default() @@ -3546,28 +3474,16 @@ class Utilities: if valuetext: return valuetext - try: - value = obj.queryValue() - except NotImplementedError: + if not AXObject.supports_value(obj): return "" - else: - currentValue = value.currentValue + + currentValue = AXValue.get_current_value(obj) # "The reports of my implementation are greatly exaggerated." + maxValue = AXValue.get_maximum_value(obj) + minValue = AXValue.get_minimum_value(obj) try: - maxValue = value.maximumValue - except Exception as error: - maxValue = 0.0 - tokens = ["SCRIPT UTILITIES: Could not get maximum value for", obj, ":", error] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - try: - minValue = value.minimumValue - except Exception as error: - minValue = 0.0 - tokens = ["SCRIPT UTILITIES: Could not get minimum value for", obj, ":", error] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - try: - minIncrement = value.minimumIncrement + minIncrement = Atspi.Value.get_minimum_increment(obj) except Exception as error: minIncrement = (maxValue - minValue) / 100.0 tokens = ["SCRIPT UTILITIES: Could not get minimum increment for", obj, ":", error] @@ -3607,7 +3523,7 @@ class Utilities: def getLineContentsAtOffset(self, obj, offset, layoutMode=True, useCache=True): return [] - def getObjectContentsAtOffset(self, obj, offset=0, useCache=True): + def get_objectContentsAtOffset(self, obj, offset=0, useCache=True): return [] def previousContext(self, obj=None, offset=-1, skipSpace=False): @@ -3626,21 +3542,16 @@ class Utilities: offset = 0 text = self.queryNonEmptyText(root) if text: - offset = text.characterCount - 1 + offset = AXText.get_character_count(root) - 1 return root, offset def getHyperlinkRange(self, obj): """Returns the text range in parent associated with obj.""" - try: - hyperlink = obj.queryHyperlink() - start, end = hyperlink.startIndex, hyperlink.endIndex - except NotImplementedError: - tokens = ["SCRIPT UTILITIES:", obj, "does not implement the hyperlink interface"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return -1, -1 - except Exception: + start = AXHypertext.get_link_start_offset(obj) + end = AXHypertext.get_link_end_offset(obj) + if start < 0 or end < 0: tokens = ["SCRIPT UTILITIES: Exception getting hyperlink indices for", obj] debug.printTokens(debug.LEVEL_INFO, tokens, True) return -1, -1 @@ -3722,9 +3633,9 @@ class Utilities: def selectedChildCount(self, obj): if AXObject.supports_table(obj): - table = obj.queryTable() - if table.nSelectedRows: - return table.nSelectedRows + count = AXTable.get_selected_row_count(obj) + if count: + return count return AXSelection.get_selected_child_count(obj) @@ -3922,30 +3833,7 @@ class Utilities: debug.printMessage(debug.LEVEL_INFO, msg, True) return [] - if AXObject.supports_table_cell(obj): - tableCell = obj.queryTableCell() - try: - headers = tableCell.columnHeaderCells - except Exception: - tokens = ["SCRIPT UTILITIES: Exception getting column headers for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - else: - return headers - - parent = AXObject.find_ancestor(obj, AXObject.supports_table) - try: - table = parent.queryTable() - except Exception: - return [] - - row, col = self.coordinatesForCell(obj) - rowspan, colspan = self.rowAndColumnSpan(obj) - - headers = [] - for c in range(col, col+colspan): - headers.append(table.getColumnHeader(c)) - - return headers + return AXTable.get_column_headers(obj) def rowHeadersForCell(self, obj): result = self._rowHeadersForCell(obj) @@ -3966,30 +3854,7 @@ class Utilities: debug.printMessage(debug.LEVEL_INFO, msg, True) return [] - if AXObject.supports_table_cell(obj): - tableCell = obj.queryTableCell() - try: - headers = tableCell.rowHeaderCells - except Exception: - tokens = ["SCRIPT UTILITIES: Exception getting row headers for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - else: - return headers - - parent = AXObject.find_ancestor(obj, AXObject.supports_table) - try: - table = parent.queryTable() - except Exception: - return [] - - row, col = self.coordinatesForCell(obj) - rowspan, colspan = self.rowAndColumnSpan(obj) - - headers = [] - for r in range(row, row+rowspan): - headers.append(table.getRowHeader(r)) - - return headers + return AXTable.get_row_headers(obj) def columnHeaderForCell(self, obj): headers = self.columnHeadersForCell(obj) @@ -4009,79 +3874,13 @@ class Utilities: return True def coordinatesForCell(self, obj, preferAttribute=True, findCellAncestor=False): - if not AXUtilities.is_table_cell_or_header(obj): - if not findCellAncestor: - return -1, -1 - - cell = AXObject.find_ancestor(obj, AXUtilities.is_table_cell_or_header) - return self.coordinatesForCell(cell, preferAttribute, False) - - if AXObject.supports_table_cell(obj) \ - and self._shouldUseTableCellInterfaceForCoordinates(): - tableCell = obj.queryTableCell() - try: - successful, row, col = tableCell.position - except Exception: - tokens = ["SCRIPT UTILITIES: Exception getting table cell position of", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - else: - if successful: - tokens = ["SCRIPT UTILITIES: position of", obj, f"is row: {row}, col: {col}"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return row, col - tokens = ["SCRIPT UTILITIES: Failed to get position of", obj, "via table cell"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - - parent = AXObject.find_ancestor(obj, AXObject.supports_table) - if not parent: - tokens = ["SCRIPT UTILITIES: Couldn't find table-implementing ancestor for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return -1, -1 - - try: - table = parent.queryTable() - except Exception: - tokens = ["SCRIPT UTILITIES: Exception querying table interface", parent] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return -1, -1 - - index = self.cellIndex(obj) - try: - row = table.getRowAtIndex(index) - col = table.getColumnAtIndex(index) - except Exception: - tokens = ["SCRIPT UTILITIES: Exception getting row and column at index from", parent] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return -1, -1 - - return row, col + return AXTable.get_cell_coordinates( + obj, prefer_attribute=preferAttribute, find_cell=findCellAncestor) def rowAndColumnSpan(self, obj): if not AXUtilities.is_table_cell_or_header(obj): return -1, -1 - - if AXObject.supports_table_cell(obj): - tableCell = obj.queryTableCell() - try: - rowSpan, colSpan = tableCell.rowSpan, tableCell.columnSpan - except Exception: - tokens = ["SCRIPT UTILITIES: Exception getting table row and col span of", - obj, "via table cell"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - else: - return rowSpan, colSpan - - parent = AXObject.find_ancestor(obj, AXObject.supports_table) - try: - table = parent.queryTable() - except Exception: - return -1, -1 - - row, col = self.coordinatesForCell(obj) - if (row < 0 or col < 0): - return -1, -1 - - return table.getRowExtentAt(row, col), table.getColumnExtentAt(row, col) + return AXTable.get_cell_spans(obj, prefer_attribute=True) def setSizeUnknown(self, obj): return AXUtilities.is_indeterminate(obj) @@ -4090,12 +3889,12 @@ class Utilities: return AXUtilities.is_indeterminate(obj) def rowAndColumnCount(self, obj, preferAttribute=True): - try: - table = obj.queryTable() - except Exception: + if not AXObject.supports_table(obj): return -1, -1 - return table.nRows, table.nColumns + rows = AXTable.get_row_count(obj, prefer_attribute=preferAttribute) + cols = AXTable.get_column_count(obj, prefer_attribute=preferAttribute) + return rows, cols def _objectBoundsMightBeBogus(self, obj): return False @@ -4111,22 +3910,23 @@ class Utilities: if self._objectMightBeBogus(obj): return False - try: - component = obj.queryComponent() - except Exception: + if not AXObject.supports_component(obj): return False if coordType is None: coordType = Atspi.CoordType.SCREEN - if component.contains(x, y, coordType): - return True + try: + if Atspi.Component.contains(obj, x, y, coordType): + return True - x1, y1 = x + margin, y + margin - if component.contains(x1, y1, coordType): - tokens = ["SCRIPT UTILITIES: ", obj, f"contains ({x1},{y1}); not ({x},{y}"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return True + x1, y1 = x + margin, y + margin + if Atspi.Component.contains(obj, x1, y1, coordType): + tokens = ["SCRIPT UTILITIES: ", obj, f"contains ({x1},{y1}); not ({x},{y}"] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + return True + except Exception: + return False return False @@ -4170,17 +3970,21 @@ class Utilities: if self.isHidden(root): return None - try: - component = root.queryComponent() - except Exception: - tokens = ["SCRIPT UTILITIES: Exception querying component of", root] + if not AXObject.supports_component(root): + tokens = ["SCRIPT UTILITIES:", root, "does not support component interface"] debug.printTokens(debug.LEVEL_INFO, tokens, True) return None if coordType is None: coordType = Atspi.CoordType.SCREEN - result = component.getAccessibleAtPoint(x, y, coordType) + try: + result = Atspi.Component.get_accessible_at_point(root, x, y, coordType) + except Exception: + tokens = ["SCRIPT UTILITIES: Exception getting accessible at point for", root] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + return None + tokens = ["SCRIPT UTILITIES: ", result, "is descendant of", root, f"at ({x}, {y})"] debug.printTokens(debug.LEVEL_INFO, tokens, True) return result @@ -4218,7 +4022,7 @@ class Utilities: if not self.containsPoint(child, x, y, coordType): continue if self.queryNonEmptyText(child): - string = child.queryText().getText(0, -1) + string = AXText.get_all_text(child) if re.search(r"[^\ufffc\s]", string): candidates.append(child) if AXUtilities.is_showing(child): @@ -4242,19 +4046,17 @@ class Utilities: if not AXObject.supports_text(obj): return False - text = obj.queryText() - string = text.getText(0, -1) + string = AXText.get_all_text(obj) chunks = list(filter(lambda x: x.strip(), string.split("\n\n"))) return len(chunks) > 1 def getWordAtOffsetAdjustedForNavigation(self, obj, offset=None): - try: - text = obj.queryText() - if offset is None: - offset = text.caretOffset - except Exception: + if not AXObject.supports_text(obj): return "", 0, 0 + if offset is None: + offset = AXText.get_caret_offset(obj) + word, start, end = self.getWordAtOffset(obj, offset) prevObj, prevOffset = self._script.pointOfReference.get( "penultimateCursorPosition", (None, -1)) @@ -4272,7 +4074,7 @@ class Utilities: start = prevOffset end = offset - word = text.getText(start, end) + word = AXText.get_substring(obj, start, end) debugString = word.replace("\n", "\\n") msg = ( f"SCRIPT UTILITIES: Adjusted word at offset {offset} for ongoing word nav is " @@ -4306,11 +4108,11 @@ class Utilities: # If the character to the left of our present position is neither a space, nor # an alphanumeric character, then suspect that character is a navigation boundary # where we would have landed before via the native next word command. - lastChar = text.getText(offset - 1, offset) + lastChar = AXText.get_substring(obj, offset - 1, offset) if not (lastChar.isspace() or lastChar.isalnum()): start = offset - 1 - word = text.getText(start, end) + word = AXText.get_substring(obj, start, end) # We only want to present the newline character when we cross a boundary moving from one # word to another. If we're in the same word, strip it out. @@ -4319,9 +4121,9 @@ class Utilities: start += 1 elif word.endswith("\n"): end -= 1 - word = text.getText(start, end) + word = AXText.get_substring(obj, start, end) - word = text.getText(start, end) + word = AXText.get_substring(obj, start, end) debugString = word.replace("\n", "\\n") msg = ( f"SCRIPT UTILITIES: Adjusted word at offset {offset} for new word nav is " @@ -4331,14 +4133,13 @@ class Utilities: return word, start, end def getWordAtOffset(self, obj, offset=None): - try: - text = obj.queryText() - if offset is None: - offset = text.caretOffset - except Exception: + if not AXObject.supports_text(obj): return "", 0, 0 - word, start, end = text.getTextAtOffset(offset, Atspi.TextBoundaryType.WORD_START) + if offset is None: + offset = AXText.get_caret_offset(obj) + + word, start, end = AXText.get_word_at_offset(obj, offset) debugString = word.replace("\n", "\\n") msg = ( f"SCRIPT UTILITIES: Word at offset {offset} is " @@ -4359,18 +4160,46 @@ class Utilities: boundary = Atspi.TextBoundaryType.LINE_START x, y = self._adjustPointForObj(obj, x, y, coordType) - offset = text.getOffsetAtPoint(x, y, coordType) - if not 0 <= offset < text.characterCount: + try: + offset = Atspi.Text.get_offset_at_point(obj, x, y, coordType) + except Exception: return "", 0, 0 - string, start, end = text.getTextAtOffset(offset, boundary) + if not 0 <= offset < AXText.get_character_count(obj): + return "", 0, 0 + + if boundary == Atspi.TextBoundaryType.CHAR: + string, start, end = AXText.get_character_at_offset(obj, offset) + elif boundary == Atspi.TextBoundaryType.WORD_START: + string, start, end = AXText.get_word_at_offset(obj, offset) + elif boundary == Atspi.TextBoundaryType.LINE_START: + string, start, end = AXText.get_line_at_offset(obj, offset) + elif boundary == Atspi.TextBoundaryType.SENTENCE_START: + string, start, end = AXText.get_sentence_at_offset(obj, offset) + elif boundary == Atspi.TextBoundaryType.PARAGRAPH_START: + string, start, end = AXText.get_paragraph_at_offset(obj, offset) + else: + try: + result = Atspi.Text.get_string_at_offset(obj, offset, boundary) + except Exception: + return "", 0, 0 + if result is None: + return "", 0, 0 + string = result.content + start = result.start_offset + end = result.end_offset + if not string: return "", start, end if boundary == Atspi.TextBoundaryType.WORD_START and not string.strip(): return "", 0, 0 - extents = text.getRangeExtents(start, end, coordType) + try: + extents = Atspi.Text.get_range_extents(obj, start, end, coordType) + except Exception: + return "", 0, 0 + if not self.containsRegion(extents, (x, y, 1, 1)) and string != "\n": return "", 0, 0 @@ -4387,10 +4216,11 @@ class Utilities: return string, start, end def visibleRows(self, obj, boundingbox): - try: - table = obj.queryTable() - nRows = table.nRows - except Exception: + if not AXObject.supports_table(obj): + return [] + + nRows = AXTable.get_row_count(obj, prefer_attribute=False) + if nRows < 0: return [] tokens = ["SCRIPT UTILITIES: ", obj, f"has {nRows} rows"] @@ -4404,16 +4234,18 @@ class Utilities: debug.printTokens(debug.LEVEL_INFO, tokens, True) # Just in case the row above is a static header row in a scrollable table. - try: - extents = cell.queryComponent().getExtents(Atspi.CoordType.SCREEN) - except Exception: - nextIndex = startIndex + if cell and AXObject.supports_component(cell): + try: + extents = Atspi.Component.get_extents(cell, Atspi.CoordType.SCREEN) + cell = self.descendantAtPoint(obj, x, y + extents.height + 1) + row, col = self.coordinatesForCell(cell) + nextIndex = max(startIndex, row) + tokens = ["SCRIPT UTILITIES: Next cell:", cell, f"(row: {row}"] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + except Exception: + nextIndex = startIndex else: - cell = self.descendantAtPoint(obj, x, y + extents.height + 1) - row, col = self.coordinatesForCell(cell) - nextIndex = max(startIndex, row) - tokens = ["SCRIPT UTILITIES: Next cell:", cell, f"(row: {row}"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + nextIndex = startIndex cell = self.descendantAtPoint(obj, x, y + height - 1) row, col = self.coordinatesForCell(cell) @@ -4431,14 +4263,14 @@ class Utilities: return rows def getVisibleTableCells(self, obj): - try: - table = obj.queryTable() - except Exception: + if not AXObject.supports_table(obj): + return [] + + if not AXObject.supports_component(obj): return [] try: - component = obj.queryComponent() - extents = component.getExtents(Atspi.CoordType.SCREEN) + extents = Atspi.Component.get_extents(obj, Atspi.CoordType.SCREEN) except Exception: tokens = ["SCRIPT UTILITIES: Exception getting extents of", obj] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -4454,12 +4286,15 @@ class Utilities: cells = [] for col in range(colStartIndex, colEndIndex): - colHeader = table.getColumnHeader(col) - if colHeader: - cells.append(colHeader) + try: + colHeader = Atspi.Table.get_column_header(obj, col) + if colHeader: + cells.append(colHeader) + except Exception: + pass for row in rows: try: - cell = table.getAccessibleAt(row, col) + cell = Atspi.Table.get_accessible_at(obj, row, col) except Exception: continue if cell and self.isOnScreen(cell): @@ -4474,31 +4309,40 @@ class Utilities: return startIndex, endIndex parent = self.getTable(obj) - try: - component = parent.queryComponent() - except Exception: - tokens = ["SCRIPT UTILITIES: Exception querying component interface of", parent] + if not parent or not AXObject.supports_component(parent): + tokens = ["SCRIPT UTILITIES:", parent, "does not support component interface"] debug.printTokens(debug.LEVEL_INFO, tokens, True) return startIndex, endIndex - x, y, width, height = component.getExtents(Atspi.CoordType.SCREEN) - cell = component.getAccessibleAtPoint(x+1, y, Atspi.CoordType.SCREEN) - if cell: - row, column = self.coordinatesForCell(cell) - startIndex = column + try: + extents = Atspi.Component.get_extents(parent, Atspi.CoordType.SCREEN) + x, y, width, height = extents.x, extents.y, extents.width, extents.height + except Exception: + tokens = ["SCRIPT UTILITIES: Exception getting extents of", parent] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + return startIndex, endIndex - cell = component.getAccessibleAtPoint(x+width-1, y, Atspi.CoordType.SCREEN) - if cell: - row, column = self.coordinatesForCell(cell) - endIndex = column + 1 + try: + cell = Atspi.Component.get_accessible_at_point(parent, x+1, y, Atspi.CoordType.SCREEN) + if cell: + row, column = self.coordinatesForCell(cell) + startIndex = column + except Exception: + pass + + try: + cell = Atspi.Component.get_accessible_at_point(parent, x+width-1, y, Atspi.CoordType.SCREEN) + if cell: + row, column = self.coordinatesForCell(cell) + endIndex = column + 1 + except Exception: + pass return startIndex, endIndex def getShowingCellsInSameRow(self, obj, forceFullRow=False): parent = self.getTable(obj) - try: - table = parent.queryTable() - except Exception: + if not parent or not AXObject.supports_table(parent): tokens = ["SCRIPT UTILITIES: Exception querying table interface of", parent] debug.printTokens(debug.LEVEL_INFO, tokens, True) return [] @@ -4508,7 +4352,7 @@ class Utilities: return [] if forceFullRow: - startIndex, endIndex = 0, table.nColumns + startIndex, endIndex = 0, AXTable.get_column_count(parent, prefer_attribute=False) else: startIndex, endIndex = self._getTableRowRange(obj) if startIndex == endIndex: @@ -4516,19 +4360,17 @@ class Utilities: cells = [] for i in range(startIndex, endIndex): - cell = table.getAccessibleAt(row, i) - if AXUtilities.is_showing(cell): + cell = AXTable.get_cell_at(parent, row, i) + if cell and AXUtilities.is_showing(cell): cells.append(cell) return cells def cellForCoordinates(self, obj, row, column, showingOnly=False): - try: - table = obj.queryTable() - except Exception: + if not AXObject.supports_table(obj): return None - cell = table.getAccessibleAt(row, column) + cell = AXTable.get_cell_at(obj, row, column) if not showingOnly: return cell @@ -4541,28 +4383,20 @@ class Utilities: if not AXUtilities.is_table_cell(obj): return False - parent = AXObject.find_ancestor(obj, AXObject.supports_table) - try: - table = parent.queryTable() - except Exception: + table = AXTable.get_table(obj) + if table is None: return False row, col = self.coordinatesForCell(obj) - return row + 1 == table.nRows and col + 1 == table.nColumns + rows = AXTable.get_row_count(table, prefer_attribute=False) + cols = AXTable.get_column_count(table, prefer_attribute=False) + return row + 1 == rows and col + 1 == cols def isNonUniformTable(self, obj, maxRows=25, maxCols=25): - try: - table = obj.queryTable() - except Exception: + if not AXObject.supports_table(obj): return False - for r in range(min(maxRows, table.nRows)): - for c in range(min(maxCols, table.nColumns)): - if table.getRowExtentAt(r, c) > 1 \ - or table.getColumnExtentAt(r, c) > 1: - return True - - return False + return AXTable.is_non_uniform_table(obj, maxRows, maxCols) def isShowingAndVisible(self, obj): if AXUtilities.is_showing(obj) and AXUtilities.is_visible(obj): @@ -4636,13 +4470,13 @@ class Utilities: return replicant def getFunctionalChildCount(self, obj): - relation = AXObject.get_relation(obj, Atspi.RelationType.NODE_PARENT_OF) - if relation: - return relation.get_n_targets() + nodeParents = AXUtilitiesRelation.get_is_node_parent_of(obj) + if nodeParents: + return len(nodeParents) return AXObject.get_child_count(obj) def getFunctionalChildren(self, obj, sibling=None): - result = AXObject.get_relation_targets(obj, Atspi.RelationType.NODE_PARENT_OF) + result = AXUtilitiesRelation.get_is_node_parent_of(obj) if result: return result if self.isDescriptionListTerm(sibling): @@ -4652,9 +4486,9 @@ class Utilities: return [x for x in AXObject.iter_children(obj)] def getFunctionalParent(self, obj): - relation = AXObject.get_relation(obj, Atspi.RelationType.NODE_CHILD_OF) - if relation: - return relation.get_target(0) + nodeChildren = AXUtilitiesRelation.get_is_node_child_of(obj) + if nodeChildren: + return nodeChildren[0] return AXObject.get_parent(obj) def getPositionAndSetSize(self, obj, **args): @@ -4741,16 +4575,12 @@ class Utilities: return start, end, string def updateCachedTextSelection(self, obj): - try: - text = obj.queryText() - except NotImplementedError: + if not AXObject.supports_text(obj): tokens = ["SCRIPT UTILITIES:", obj, "doesn't implement AtspiText"] debug.printTokens(debug.LEVEL_INFO, tokens, True) text = None - except Exception: - tokens = ["SCRIPT UTILITIES: Exception querying text interface for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - text = None + else: + text = obj if self._script.pointOfReference.get('entireDocumentSelected'): selectedText, selectedStart, selectedEnd = self.allSelectedText(obj) @@ -4771,14 +4601,9 @@ class Utilities: # selections in a single accessible object. start, end, string = 0, 0, '' if text: - try: - start, end = text.getSelection(0) - except Exception: - tokens = ["SCRIPT UTILITIES: Exception getting selected text for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - start = end = 0 - if start != end: - string = text.getText(start, end) + string, start, end = AXText.get_selected_text(obj) + if string: + string = self.expandEOCs(obj, start, end) tokens = ["SCRIPT UTILITIES: New selection for", obj, f"is '{string}' ({start}, {end})"] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -4839,254 +4664,108 @@ class Utilities: return event and event.isFromApplication(self._script.app) def lastInputEventWasPrintableKey(self): - event = cthulhu_state.lastInputEvent - if not isinstance(event, input_event.KeyboardEvent): - return False - - return event.isPrintableKey() + return input_event_manager.get_manager().last_event_was_printable_key() def lastInputEventWasCommand(self): - keyString, mods = self.lastKeyAndModifiers() - return mods & keybindings.CTRL_MODIFIER_MASK + return input_event_manager.get_manager().last_event_was_command() def lastInputEventWasPageSwitch(self): - keyString, mods = self.lastKeyAndModifiers() - if keyString.isnumeric(): - return mods & keybindings.ALT_MODIFIER_MASK - - if keyString in ["Page_Up", "Page_Down"]: - return mods & keybindings.CTRL_MODIFIER_MASK - - return False + return input_event_manager.get_manager().last_event_was_page_switch() def lastInputEventWasUnmodifiedArrow(self): - keyString, mods = self.lastKeyAndModifiers() - if keyString not in ["Left", "Right", "Up", "Down"]: - return False - - if mods & keybindings.CTRL_MODIFIER_MASK \ - or mods & keybindings.SHIFT_MODIFIER_MASK \ - or mods & keybindings.ALT_MODIFIER_MASK \ - or mods & keybindings.CTHULHU_MODIFIER_MASK: - return False - - return True + return input_event_manager.get_manager().last_event_was_unmodified_arrow() def lastInputEventWasCaretNav(self): - return self.lastInputEventWasCharNav() \ - or self.lastInputEventWasWordNav() \ - or self.lastInputEventWasLineNav() \ - or self.lastInputEventWasLineBoundaryNav() + return input_event_manager.get_manager().last_event_was_caret_navigation() def lastInputEventWasCharNav(self): - keyString, mods = self.lastKeyAndModifiers() - if keyString not in ["Left", "Right"]: - return False - - if mods & keybindings.CTRL_MODIFIER_MASK \ - or mods & keybindings.ALT_MODIFIER_MASK: - return False - - return True + return input_event_manager.get_manager().last_event_was_character_navigation() def lastInputEventWasWordNav(self): - keyString, mods = self.lastKeyAndModifiers() - if keyString not in ["Left", "Right"]: - return False - - return mods & keybindings.CTRL_MODIFIER_MASK + return input_event_manager.get_manager().last_event_was_word_navigation() def lastInputEventWasPrevWordNav(self): - keyString, mods = self.lastKeyAndModifiers() - if not keyString == "Left": - return False - - return mods & keybindings.CTRL_MODIFIER_MASK + return input_event_manager.get_manager().last_event_was_previous_word_navigation() def lastInputEventWasNextWordNav(self): - keyString, mods = self.lastKeyAndModifiers() - if not keyString == "Right": - return False - - return mods & keybindings.CTRL_MODIFIER_MASK + return input_event_manager.get_manager().last_event_was_next_word_navigation() def lastInputEventWasLineNav(self): - keyString, mods = self.lastKeyAndModifiers() - if keyString not in ["Up", "Down"]: + if not input_event_manager.get_manager().last_event_was_line_navigation(): return False if self.isEditableDescendantOfComboBox(cthulhu_state.locusOfFocus): return False - return not (mods & keybindings.CTRL_MODIFIER_MASK) - - def lastInputEventWasLineBoundaryNav(self): - keyString, mods = self.lastKeyAndModifiers() - if keyString not in ["Home", "End"]: - return False - - return not (mods & keybindings.CTRL_MODIFIER_MASK) - - def lastInputEventWasPageNav(self): - keyString, mods = self.lastKeyAndModifiers() - if keyString not in ["Page_Up", "Page_Down"]: - return False - - if self.isEditableDescendantOfComboBox(cthulhu_state.locusOfFocus): - return False - - return not (mods & keybindings.CTRL_MODIFIER_MASK) - - def lastInputEventWasFileBoundaryNav(self): - keyString, mods = self.lastKeyAndModifiers() - if keyString not in ["Home", "End"]: - return False - - return mods & keybindings.CTRL_MODIFIER_MASK - - def lastInputEventWasCaretNavWithSelection(self): - keyString, mods = self.lastKeyAndModifiers() - if mods & keybindings.SHIFT_MODIFIER_MASK: - return keyString in ["Home", "End", "Up", "Down", "Left", "Right"] - - return False - - def lastInputEventWasUndo(self): - keycode, mods = self._lastKeyCodeAndModifiers() - keynames = self._allNamesForKeyCode(keycode) - if 'z' not in keynames: - return False - - if mods & keybindings.CTRL_MODIFIER_MASK: - return not (mods & keybindings.SHIFT_MODIFIER_MASK) - - return False - - def lastInputEventWasRedo(self): - keycode, mods = self._lastKeyCodeAndModifiers() - keynames = self._allNamesForKeyCode(keycode) - if 'z' not in keynames: - return False - - if mods & keybindings.CTRL_MODIFIER_MASK: - return mods & keybindings.SHIFT_MODIFIER_MASK - - return False - - def lastInputEventWasCut(self): - keycode, mods = self._lastKeyCodeAndModifiers() - keynames = self._allNamesForKeyCode(keycode) - if 'x' not in keynames: - return False - - if mods & keybindings.CTRL_MODIFIER_MASK: - return not (mods & keybindings.SHIFT_MODIFIER_MASK) - - return False - - def lastInputEventWasCopy(self): - keycode, mods = self._lastKeyCodeAndModifiers() - keynames = self._allNamesForKeyCode(keycode) - if 'c' not in keynames: - return False - - if mods & keybindings.CTRL_MODIFIER_MASK: - return not (mods & keybindings.SHIFT_MODIFIER_MASK) - - return False - - def lastInputEventWasPaste(self): - keycode, mods = self._lastKeyCodeAndModifiers() - keynames = self._allNamesForKeyCode(keycode) - if 'v' not in keynames: - return False - - if mods & keybindings.CTRL_MODIFIER_MASK: - return not (mods & keybindings.SHIFT_MODIFIER_MASK) - - return False - - def lastInputEventWasSelectAll(self): - keycode, mods = self._lastKeyCodeAndModifiers() - keynames = self._allNamesForKeyCode(keycode) - if 'a' not in keynames: - return False - - if mods & keybindings.CTRL_MODIFIER_MASK: - return not (mods & keybindings.SHIFT_MODIFIER_MASK) - - return False - - def lastInputEventWasDelete(self): - keyString, mods = self.lastKeyAndModifiers() - if keyString in ["Delete", "KP_Delete"]: - return True - - keycode, mods = self._lastKeyCodeAndModifiers() - keynames = self._allNamesForKeyCode(keycode) - if 'd' not in keynames: - return False - - return mods & keybindings.CTRL_MODIFIER_MASK - - def lastInputEventWasTab(self): - keyString, mods = self.lastKeyAndModifiers() - if keyString not in ["Tab", "ISO_Left_Tab"]: - return False - - if mods & keybindings.CTRL_MODIFIER_MASK \ - or mods & keybindings.ALT_MODIFIER_MASK \ - or mods & keybindings.CTHULHU_MODIFIER_MASK: - return False - return True + def lastInputEventWasLineBoundaryNav(self): + return input_event_manager.get_manager().last_event_was_line_boundary_navigation() + + def lastInputEventWasPageNav(self): + if not input_event_manager.get_manager().last_event_was_page_navigation(): + return False + + if self.isEditableDescendantOfComboBox(cthulhu_state.locusOfFocus): + return False + + return True + + def lastInputEventWasFileBoundaryNav(self): + return input_event_manager.get_manager().last_event_was_file_boundary_navigation() + + def lastInputEventWasCaretNavWithSelection(self): + return input_event_manager.get_manager().last_event_was_caret_selection() + + def lastInputEventWasUndo(self): + return input_event_manager.get_manager().last_event_was_undo() + + def lastInputEventWasRedo(self): + return input_event_manager.get_manager().last_event_was_redo() + + def lastInputEventWasCut(self): + return input_event_manager.get_manager().last_event_was_cut() + + def lastInputEventWasCopy(self): + return input_event_manager.get_manager().last_event_was_copy() + + def lastInputEventWasPaste(self): + return input_event_manager.get_manager().last_event_was_paste() + + def lastInputEventWasSelectAll(self): + return input_event_manager.get_manager().last_event_was_select_all() + + def lastInputEventWasDelete(self): + return input_event_manager.get_manager().last_event_was_delete() + + def lastInputEventWasTab(self): + return input_event_manager.get_manager().last_event_was_tab() + def lastInputEventWasMouseButton(self): - return isinstance(cthulhu_state.lastInputEvent, input_event.MouseButtonEvent) + return input_event_manager.get_manager().last_event_was_mouse_button() def lastInputEventWasPrimaryMouseClick(self): - event = cthulhu_state.lastInputEvent - if isinstance(event, input_event.MouseButtonEvent): - return event.button == "1" and event.pressed - - return False + return input_event_manager.get_manager().last_event_was_primary_click() def lastInputEventWasMiddleMouseClick(self): - event = cthulhu_state.lastInputEvent - if isinstance(event, input_event.MouseButtonEvent): - return event.button == "2" and event.pressed - - return False + return input_event_manager.get_manager().last_event_was_middle_click() def lastInputEventWasSecondaryMouseClick(self): - event = cthulhu_state.lastInputEvent - if isinstance(event, input_event.MouseButtonEvent): - return event.button == "3" and event.pressed - - return False + return input_event_manager.get_manager().last_event_was_secondary_click() def lastInputEventWasPrimaryMouseRelease(self): - event = cthulhu_state.lastInputEvent - if isinstance(event, input_event.MouseButtonEvent): - return event.button == "1" and not event.pressed - - return False + return input_event_manager.get_manager().last_event_was_primary_release() def lastInputEventWasMiddleMouseRelease(self): - event = cthulhu_state.lastInputEvent - if isinstance(event, input_event.MouseButtonEvent): - return event.button == "2" and not event.pressed - - return False + return input_event_manager.get_manager().last_event_was_middle_release() def lastInputEventWasSecondaryMouseRelease(self): - event = cthulhu_state.lastInputEvent - if isinstance(event, input_event.MouseButtonEvent): - return event.button == "3" and not event.pressed - - return False + return input_event_manager.get_manager().last_event_was_secondary_release() def lastInputEventWasTableSort(self, delta=0.5): + if not input_event_manager.get_manager().last_event_was_table_sort(): + return False + event = cthulhu_state.lastInputEvent if not event: return False @@ -5404,14 +5083,18 @@ class Utilities: if not AXObject.supports_table(obj): return False - table = obj.queryTable() - if table.nSelectedRows == table.nRows: - msg = f"SCRIPT UTILITIES: All {table.nRows} rows believed to be selected" + rows = AXTable.get_row_count(obj, prefer_attribute=False) + cols = AXTable.get_column_count(obj, prefer_attribute=False) + selected_rows = AXTable.get_selected_row_count(obj) + selected_cols = AXTable.get_selected_column_count(obj) + + if selected_rows == rows: + msg = f"SCRIPT UTILITIES: All {rows} rows believed to be selected" debug.printMessage(debug.LEVEL_INFO, msg, True) return True - if table.nSelectedColumns == table.nColumns: - msg = f"SCRIPT UTILITIES: All {table.nColumns} columns believed to be selected" + if selected_cols == cols: + msg = f"SCRIPT UTILITIES: All {cols} columns believed to be selected" debug.printMessage(debug.LEVEL_INFO, msg, True) return True @@ -5484,9 +5167,8 @@ class Utilities: self.handleTextSelectionChange(child, False) speakMessage = speakMessage and not _settingsManager.getSetting('onlySpeakDisplayedText') - text = obj.queryText() for start, end, message in changes: - string = text.getText(start, end) + string = AXText.get_substring(obj, start, end) endsWithChild = string.endswith(self.EMBEDDED_OBJECT_CHARACTER) if endsWithChild: end -= 1 @@ -5600,19 +5282,11 @@ class Utilities: debug.printMessage(debug.LEVEL_INFO, msg, True) return False - def isOld(target): - return target == oldLocusOfFocus - - def isNew(target): - return target == newLocusOfFocus - - if AXObject.get_relation_targets(newLocusOfFocus, - Atspi.RelationType.CONTROLLER_FOR, isOld): + if oldLocusOfFocus in AXUtilitiesRelation.get_is_controller_for(newLocusOfFocus): msg += "new locusOfFocus controls old locusOfFocus" debug.printMessage(debug.LEVEL_INFO, msg, True) return False - if AXObject.get_relation_targets(oldLocusOfFocus, - Atspi.RelationType.CONTROLLER_FOR, isNew): + if newLocusOfFocus in AXUtilitiesRelation.get_is_controller_for(oldLocusOfFocus): msg += "old locusOfFocus controls new locusOfFocus" debug.printMessage(debug.LEVEL_INFO, msg, True) return False diff --git a/src/cthulhu/scripts/__init__.py b/src/cthulhu/scripts/__init__.py index 782103c..301e5ea 100644 --- a/src/cthulhu/scripts/__init__.py +++ b/src/cthulhu/scripts/__init__.py @@ -20,6 +20,6 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu diff --git a/src/cthulhu/scripts/apps/Banshee/__init__.py b/src/cthulhu/scripts/apps/Banshee/__init__.py index 91372dc..b006f6c 100644 --- a/src/cthulhu/scripts/apps/Banshee/__init__.py +++ b/src/cthulhu/scripts/apps/Banshee/__init__.py @@ -20,7 +20,7 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from .script import Script diff --git a/src/cthulhu/scripts/apps/Banshee/script.py b/src/cthulhu/scripts/apps/Banshee/script.py index e7e0add..21f71f2 100644 --- a/src/cthulhu/scripts/apps/Banshee/script.py +++ b/src/cthulhu/scripts/apps/Banshee/script.py @@ -20,11 +20,12 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu import cthulhu.scripts.default as default import cthulhu.cthulhu_state as cthulhu_state +from cthulhu.ax_value import AXValue from .script_utilities import Utilities @@ -47,8 +48,7 @@ class Script(default.Script): def onValueChanged(self, event): obj = event.source if self.utilities.isSeekSlider(obj): - value = obj.queryValue() - current_value = int(value.currentValue)/1000 + current_value = int(AXValue.get_current_value(obj)) / 1000 if current_value in range(self._last_seek_value, self._last_seek_value + 4): if self.utilities.isSameObject(obj, cthulhu_state.locusOfFocus): self.updateBraille(obj) diff --git a/src/cthulhu/scripts/apps/Banshee/script_utilities.py b/src/cthulhu/scripts/apps/Banshee/script_utilities.py index 3f833d3..f113449 100644 --- a/src/cthulhu/scripts/apps/Banshee/script_utilities.py +++ b/src/cthulhu/scripts/apps/Banshee/script_utilities.py @@ -20,11 +20,12 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu import cthulhu.script_utilities as script_utilities from cthulhu.ax_object import AXObject +from cthulhu.ax_value import AXValue from cthulhu.ax_utilities import AXUtilities @@ -56,9 +57,7 @@ class Utilities(script_utilities.Utilities): if not self.isSeekSlider(obj): return script_utilities.Utilities.textForValue(self, obj) - try: - value = obj.queryValue() - except NotImplementedError: + if not AXObject.supports_value(obj): return script_utilities.Utilities.textForValue(self, obj) - else: - return self._formatDuration(int(value.currentValue)/1000) + + return self._formatDuration(int(AXValue.get_current_value(obj)) / 1000) diff --git a/src/cthulhu/scripts/apps/Eclipse/__init__.py b/src/cthulhu/scripts/apps/Eclipse/__init__.py index 91372dc..b006f6c 100644 --- a/src/cthulhu/scripts/apps/Eclipse/__init__.py +++ b/src/cthulhu/scripts/apps/Eclipse/__init__.py @@ -20,7 +20,7 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from .script import Script diff --git a/src/cthulhu/scripts/apps/Eclipse/script.py b/src/cthulhu/scripts/apps/Eclipse/script.py index 5b9b22e..1060ace 100644 --- a/src/cthulhu/scripts/apps/Eclipse/script.py +++ b/src/cthulhu/scripts/apps/Eclipse/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for Eclipse.""" __id__ = "$Id$" @@ -34,6 +34,7 @@ __license__ = "LGPL" import cthulhu.debug as debug import cthulhu.cthulhu as cthulhu import cthulhu.scripts.toolkits.GAIL as GAIL +from cthulhu.ax_text import AXText from cthulhu.ax_utilities import AXUtilities ######################################################################## @@ -111,7 +112,7 @@ class Script(GAIL.Script): """ if self.utilities.isTextArea(event.source): - length = event.source.queryText().characterCount + length = AXText.get_character_count(event.source) if event.detail1 == 0 and event.detail2 == length: # seems to be generated by a reformat (ctrl+shift+f) # or by commenting some block (ctrl+/). @@ -137,7 +138,7 @@ class Script(GAIL.Script): def _saveLastTextPosition(self, obj): if self.utilities.isTextArea(obj): - self._saveLastCursorPosition(obj, obj.queryText().caretOffset) + self._saveLastCursorPosition(obj, AXText.get_caret_offset(obj)) def onSelectionChanged(self, event): """Callback for object:selection-changed accessibility events.""" @@ -148,4 +149,3 @@ class Script(GAIL.Script): return GAIL.Script.onSelectionChanged(self, event) - diff --git a/src/cthulhu/scripts/apps/SeaMonkey/__init__.py b/src/cthulhu/scripts/apps/SeaMonkey/__init__.py index c5216a2..ca4874a 100644 --- a/src/cthulhu/scripts/apps/SeaMonkey/__init__.py +++ b/src/cthulhu/scripts/apps/SeaMonkey/__init__.py @@ -20,12 +20,12 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for SeaMonkey.""" -# https://gitlab.gnome.org/GNOME/cthulhu/-/issues/358 +# https://git.stormux.org/storm/cthulhu # ruff: noqa: F401 from .script import Script diff --git a/src/cthulhu/scripts/apps/SeaMonkey/script.py b/src/cthulhu/scripts/apps/SeaMonkey/script.py index d3301d5..2322e0a 100644 --- a/src/cthulhu/scripts/apps/SeaMonkey/script.py +++ b/src/cthulhu/scripts/apps/SeaMonkey/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for SeaMonkey.""" diff --git a/src/cthulhu/scripts/apps/Thunderbird/__init__.py b/src/cthulhu/scripts/apps/Thunderbird/__init__.py index 95d8f9c..88b431a 100644 --- a/src/cthulhu/scripts/apps/Thunderbird/__init__.py +++ b/src/cthulhu/scripts/apps/Thunderbird/__init__.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """ Custom script for Thunderbird 3. """ @@ -32,6 +32,6 @@ __date__ = "$Date$" __copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." __license__ = "LGPL" -# https://gitlab.gnome.org/GNOME/cthulhu/-/issues/358 +# https://git.stormux.org/storm/cthulhu # ruff: noqa: F401 from .script import Script diff --git a/src/cthulhu/scripts/apps/Thunderbird/meson.build b/src/cthulhu/scripts/apps/Thunderbird/meson.build index e2166bb..56489ce 100644 --- a/src/cthulhu/scripts/apps/Thunderbird/meson.build +++ b/src/cthulhu/scripts/apps/Thunderbird/meson.build @@ -1,10 +1,11 @@ thunderbird_python_sources = files([ '__init__.py', 'script.py', + 'script_utilities.py', 'spellcheck.py', ]) python3.install_sources( thunderbird_python_sources, subdir: 'cthulhu/scripts/apps/Thunderbird' -) \ No newline at end of file +) diff --git a/src/cthulhu/scripts/apps/Thunderbird/script.py b/src/cthulhu/scripts/apps/Thunderbird/script.py index 482dc06..a93a092 100644 --- a/src/cthulhu/scripts/apps/Thunderbird/script.py +++ b/src/cthulhu/scripts/apps/Thunderbird/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for Thunderbird.""" @@ -40,9 +40,11 @@ import cthulhu.settings_manager as settings_manager import cthulhu.cthulhu_state as cthulhu_state import cthulhu.scripts.toolkits.Gecko as Gecko from cthulhu.ax_object import AXObject +from cthulhu.ax_text import AXText from cthulhu.ax_utilities import AXUtilities from .spellcheck import SpellCheck +from .script_utilities import Utilities _settingsManager = settings_manager.getManager() @@ -97,6 +99,11 @@ class Script(Gecko.Script): return SpellCheck(self) + def getUtilities(self): + """Returns the utilities for this script.""" + + return Utilities(self) + def getAppPreferencesGUI(self): """Return a GtkGrid containing the application unique configuration GUI items for the current application.""" @@ -124,7 +131,7 @@ class Script(Gecko.Script): return prefs - def locusOfFocusChanged(self, event, oldFocus, newFocus): + def locus_of_focus_changed(self, event, oldFocus, newFocus): """Handles changes of focus of interest to the script.""" if self.spellcheck.isSuggestionsItem(newFocus): @@ -134,7 +141,7 @@ class Script(Gecko.Script): self.spellcheck.presentSuggestionListItem(includeLabel=includeLabel) return - super().locusOfFocusChanged(event, oldFocus, newFocus) + super().locus_of_focus_changed(event, oldFocus, newFocus) def useFocusMode(self, obj, prevObj=None): if self.utilities.isEditableMessage(obj): @@ -306,12 +313,8 @@ class Script(Gecko.Script): # Mozilla cannot seem to get their ":system" suffix right # to save their lives, so we'll add yet another sad hack. - try: - text = event.source.queryText() - except Exception: - hasSelection = False - else: - hasSelection = text.getNSelections() > 0 + selections = AXText.get_selected_ranges(event.source) + hasSelection = bool(selections) if hasSelection or isSystemEvent: voice = self.speechGenerator.voice(obj=event.source, string=event.any_data) self.speakMessage(event.any_data, voice=voice) @@ -329,9 +332,10 @@ class Script(Gecko.Script): return if self.utilities.isEditableMessage(obj) and self.spellcheck.isActive(): - text = obj.queryText() - selStart, selEnd = text.getSelection(0) - self.spellcheck.setDocumentPosition(obj, selStart) + selections = AXText.get_selected_ranges(obj) + if selections: + selStart, selEnd = selections[0] + self.spellcheck.setDocumentPosition(obj, selStart) return super().onTextSelectionChanged(event) @@ -356,6 +360,8 @@ class Script(Gecko.Script): [obj, offset] = self.utilities.findFirstCaretContext(documentFrame, 0) self.utilities.setCaretPosition(obj, offset) self.updateBraille(obj) + if obj and self._navSuspended and self.utilities.inDocumentContent(obj): + self._setNavigationSuspended(False, "message content loaded") if _settingsManager.getSetting('pageSummaryOnLoad'): tokens = ["THUNDERBIRD: Getting page summary for obj", obj] diff --git a/src/cthulhu/scripts/apps/Thunderbird/script_utilities.py b/src/cthulhu/scripts/apps/Thunderbird/script_utilities.py new file mode 100644 index 0000000..f09daa5 --- /dev/null +++ b/src/cthulhu/scripts/apps/Thunderbird/script_utilities.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 Stormux +# Copyright (c) 2010-2012 The Orca Team +# Copyright (c) 2012 Igalia, S.L. +# Copyright (c) 2005-2010 Sun Microsystems Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., Franklin Street, Fifth Floor, +# Boston MA 02110-1301 USA. +# +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu + +"""Thunderbird-specific utility overrides.""" + +__id__ = "$Id$" +__version__ = "$Revision$" +__date__ = "$Date$" +__copyright__ = "Copyright (c) 2010 Joanmarie Diggs." +__license__ = "LGPL" + +import cthulhu.debug as debug +from cthulhu.ax_object import AXObject +from cthulhu.ax_utilities import AXUtilities +from cthulhu.scripts.toolkits.Gecko.script_utilities import Utilities as GeckoUtilities + + +class Utilities(GeckoUtilities): + + def __init__(self, script): + super().__init__(script) + + def getDocumentForObject(self, obj): + document = super().getDocumentForObject(obj) + if document: + return document + + documentFrame = self._messageDocumentFrameFor(obj) + if not documentFrame: + return None + + tokens = ["THUNDERBIRD: Treating document frame as document:", documentFrame] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + return documentFrame + + def getTopLevelDocumentForObject(self, obj): + document = super().getTopLevelDocumentForObject(obj) + if document: + return document + + documentFrame = self._messageDocumentFrameFor(obj) + if not documentFrame: + return None + + tokens = ["THUNDERBIRD: Treating document frame as top-level document:", documentFrame] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + return documentFrame + + def _messageDocumentFrameFor(self, obj): + if not obj: + return None + + if AXUtilities.is_document_frame(obj) and self._isMessageDocumentFrame(obj): + return obj + + documentFrame = AXObject.find_ancestor(obj, AXUtilities.is_document_frame) + if documentFrame and self._isMessageDocumentFrame(documentFrame): + return documentFrame + + return None + + def _isMessageDocumentFrame(self, obj): + if not obj or not AXUtilities.is_document_frame(obj): + return False + + uri = self.documentFrameURI(obj) + if uri: + for prefix in ("about:message", "mailbox:", "imap:", "imaps:", + "news:", "nntp:", "pop:", "pop3:", + "smtp:", "smtps:"): + if uri.startswith(prefix): + return True + + name = (AXObject.get_name(obj) or "").lower() + return "message" in name diff --git a/src/cthulhu/scripts/apps/Thunderbird/spellcheck.py b/src/cthulhu/scripts/apps/Thunderbird/spellcheck.py index abbb8a1..5e1f615 100644 --- a/src/cthulhu/scripts/apps/Thunderbird/spellcheck.py +++ b/src/cthulhu/scripts/apps/Thunderbird/spellcheck.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Customized support for spellcheck in Thunderbird.""" @@ -35,6 +35,7 @@ import cthulhu.cthulhu_state as cthulhu_state import cthulhu.spellcheck as spellcheck from cthulhu.ax_object import AXObject from cthulhu.ax_utilities import AXUtilities +from cthulhu.ax_utilities_relation import AXUtilitiesRelation class SpellCheck(spellcheck.SpellCheck): @@ -79,7 +80,7 @@ class SpellCheck(spellcheck.SpellCheck): def isError(x): return AXUtilities.is_label(x) \ and ":" not in AXObject.get_name(x) \ - and not AXObject.get_relations(x) + and not AXUtilitiesRelation.get_relations(x) return AXObject.find_descendant(root, isError) diff --git a/src/cthulhu/scripts/apps/__init__.py b/src/cthulhu/scripts/apps/__init__.py index 4fa84d8..1d73da5 100644 --- a/src/cthulhu/scripts/apps/__init__.py +++ b/src/cthulhu/scripts/apps/__init__.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __all__ = ['Banshee', 'Eclipse', diff --git a/src/cthulhu/scripts/apps/epiphany/__init__.py b/src/cthulhu/scripts/apps/epiphany/__init__.py index 74133ed..9f97092 100644 --- a/src/cthulhu/scripts/apps/epiphany/__init__.py +++ b/src/cthulhu/scripts/apps/epiphany/__init__.py @@ -20,12 +20,12 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for epiphany.""" -# https://gitlab.gnome.org/GNOME/cthulhu/-/issues/358 +# https://git.stormux.org/storm/cthulhu # ruff: noqa: F401 from .script import Script diff --git a/src/cthulhu/scripts/apps/epiphany/script.py b/src/cthulhu/scripts/apps/epiphany/script.py index 13db3cc..6792b8c 100644 --- a/src/cthulhu/scripts/apps/epiphany/script.py +++ b/src/cthulhu/scripts/apps/epiphany/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for epiphany.""" diff --git a/src/cthulhu/scripts/apps/evince/__init__.py b/src/cthulhu/scripts/apps/evince/__init__.py index 92d06d1..18e4096 100644 --- a/src/cthulhu/scripts/apps/evince/__init__.py +++ b/src/cthulhu/scripts/apps/evince/__init__.py @@ -20,11 +20,11 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for evince.""" -# https://gitlab.gnome.org/GNOME/cthulhu/-/issues/358 +# https://git.stormux.org/storm/cthulhu # ruff: noqa: F401 from .script import Script diff --git a/src/cthulhu/scripts/apps/evince/script.py b/src/cthulhu/scripts/apps/evince/script.py index b8c2b0e..c0bd333 100644 --- a/src/cthulhu/scripts/apps/evince/script.py +++ b/src/cthulhu/scripts/apps/evince/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for evince.""" diff --git a/src/cthulhu/scripts/apps/evolution/__init__.py b/src/cthulhu/scripts/apps/evolution/__init__.py index 87978a5..2570c89 100644 --- a/src/cthulhu/scripts/apps/evolution/__init__.py +++ b/src/cthulhu/scripts/apps/evolution/__init__.py @@ -20,11 +20,11 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for Evolution.""" -# https://gitlab.gnome.org/GNOME/cthulhu/-/issues/358 +# https://git.stormux.org/storm/cthulhu # ruff: noqa: F401 from .script import Script diff --git a/src/cthulhu/scripts/apps/evolution/braille_generator.py b/src/cthulhu/scripts/apps/evolution/braille_generator.py index 666d57e..462d467 100644 --- a/src/cthulhu/scripts/apps/evolution/braille_generator.py +++ b/src/cthulhu/scripts/apps/evolution/braille_generator.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" diff --git a/src/cthulhu/scripts/apps/evolution/script.py b/src/cthulhu/scripts/apps/evolution/script.py index 9630530..d716712 100644 --- a/src/cthulhu/scripts/apps/evolution/script.py +++ b/src/cthulhu/scripts/apps/evolution/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for Evolution.""" diff --git a/src/cthulhu/scripts/apps/evolution/script_utilities.py b/src/cthulhu/scripts/apps/evolution/script_utilities.py index 53709ba..54bc9ab 100644 --- a/src/cthulhu/scripts/apps/evolution/script_utilities.py +++ b/src/cthulhu/scripts/apps/evolution/script_utilities.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" diff --git a/src/cthulhu/scripts/apps/evolution/speech_generator.py b/src/cthulhu/scripts/apps/evolution/speech_generator.py index 0625ed9..9157133 100644 --- a/src/cthulhu/scripts/apps/evolution/speech_generator.py +++ b/src/cthulhu/scripts/apps/evolution/speech_generator.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" diff --git a/src/cthulhu/scripts/apps/gajim/__init__.py b/src/cthulhu/scripts/apps/gajim/__init__.py index 91372dc..b006f6c 100644 --- a/src/cthulhu/scripts/apps/gajim/__init__.py +++ b/src/cthulhu/scripts/apps/gajim/__init__.py @@ -20,7 +20,7 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from .script import Script diff --git a/src/cthulhu/scripts/apps/gajim/script.py b/src/cthulhu/scripts/apps/gajim/script.py index e26517c..4161f08 100644 --- a/src/cthulhu/scripts/apps/gajim/script.py +++ b/src/cthulhu/scripts/apps/gajim/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for Gajim.""" diff --git a/src/cthulhu/scripts/apps/gcalctool/__init__.py b/src/cthulhu/scripts/apps/gcalctool/__init__.py index 87978a5..2570c89 100644 --- a/src/cthulhu/scripts/apps/gcalctool/__init__.py +++ b/src/cthulhu/scripts/apps/gcalctool/__init__.py @@ -20,11 +20,11 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for Evolution.""" -# https://gitlab.gnome.org/GNOME/cthulhu/-/issues/358 +# https://git.stormux.org/storm/cthulhu # ruff: noqa: F401 from .script import Script diff --git a/src/cthulhu/scripts/apps/gcalctool/script.py b/src/cthulhu/scripts/apps/gcalctool/script.py index 9b10f66..c09b460 100644 --- a/src/cthulhu/scripts/apps/gcalctool/script.py +++ b/src/cthulhu/scripts/apps/gcalctool/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Provides a custom script for gcalctool.""" @@ -50,7 +50,7 @@ class Script(gtk.Script): def __init__(self, app): """Creates a new script for the given application. Callers - should use the getScript factory method instead of calling + should use the get_script factory method instead of calling this constructor directly. Arguments: diff --git a/src/cthulhu/scripts/apps/gedit/__init__.py b/src/cthulhu/scripts/apps/gedit/__init__.py index 935e252..7dc74c9 100644 --- a/src/cthulhu/scripts/apps/gedit/__init__.py +++ b/src/cthulhu/scripts/apps/gedit/__init__.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for gedit.""" @@ -31,6 +31,6 @@ __date__ = "$Date$" __copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." __license__ = "LGPL" -# https://gitlab.gnome.org/GNOME/cthulhu/-/issues/358 +# https://git.stormux.org/storm/cthulhu # ruff: noqa: F401 from .script import Script diff --git a/src/cthulhu/scripts/apps/gedit/script.py b/src/cthulhu/scripts/apps/gedit/script.py index 291e7ff..371eaec 100644 --- a/src/cthulhu/scripts/apps/gedit/script.py +++ b/src/cthulhu/scripts/apps/gedit/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for gedit.""" @@ -68,7 +68,7 @@ class Script(gtk.Script): return self.spellcheck.getPreferencesFromGUI() - def locusOfFocusChanged(self, event, oldFocus, newFocus): + def locus_of_focus_changed(self, event, oldFocus, newFocus): """Handles changes of focus of interest to the script.""" if self.spellcheck.isSuggestionsItem(newFocus): @@ -78,7 +78,7 @@ class Script(gtk.Script): self.spellcheck.presentSuggestionListItem(includeLabel=includeLabel) return - super().locusOfFocusChanged(event, oldFocus, newFocus) + super().locus_of_focus_changed(event, oldFocus, newFocus) def onActiveDescendantChanged(self, event): """Callback for object:active-descendant-changed accessibility events.""" diff --git a/src/cthulhu/scripts/apps/gedit/spellcheck.py b/src/cthulhu/scripts/apps/gedit/spellcheck.py index 78c775d..6e81441 100644 --- a/src/cthulhu/scripts/apps/gedit/spellcheck.py +++ b/src/cthulhu/scripts/apps/gedit/spellcheck.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Customized support for spellcheck in Gedit.""" @@ -38,6 +38,7 @@ from gi.repository import Atspi import cthulhu.spellcheck as spellcheck from cthulhu.ax_object import AXObject from cthulhu.ax_utilities import AXUtilities +from cthulhu.ax_utilities_relation import AXUtilitiesRelation class SpellCheck(spellcheck.SpellCheck): @@ -73,7 +74,7 @@ class SpellCheck(spellcheck.SpellCheck): def isError(x): return AXUtilities.is_label(x) \ - and ":" not in AXObject.get_name(x) and not AXObject.get_relations(x) + and ":" not in AXObject.get_name(x) and not AXUtilitiesRelation.get_relations(x) return AXObject.find_descendant(panel, isError) diff --git a/src/cthulhu/scripts/apps/gnome-documents/__init__.py b/src/cthulhu/scripts/apps/gnome-documents/__init__.py index fc73e57..7020fea 100644 --- a/src/cthulhu/scripts/apps/gnome-documents/__init__.py +++ b/src/cthulhu/scripts/apps/gnome-documents/__init__.py @@ -20,12 +20,12 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for gnome-documents.""" -# https://gitlab.gnome.org/GNOME/cthulhu/-/issues/358 +# https://git.stormux.org/storm/cthulhu # ruff: noqa: F401 from .script import Script from .script_utilities import Utilities diff --git a/src/cthulhu/scripts/apps/gnome-documents/script.py b/src/cthulhu/scripts/apps/gnome-documents/script.py index f891976..8a0bba5 100644 --- a/src/cthulhu/scripts/apps/gnome-documents/script.py +++ b/src/cthulhu/scripts/apps/gnome-documents/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for gnome-documents.""" @@ -33,6 +33,8 @@ __license__ = "LGPL" import cthulhu.scripts.toolkits.gtk as gtk import cthulhu.cthulhu_state as cthulhu_state +from cthulhu.ax_object import AXObject +from cthulhu.ax_text import AXText from cthulhu.ax_utilities import AXUtilities from .speech_generator import SpeechGenerator @@ -69,12 +71,8 @@ class Script(gtk.Script): # HACK: Reposition the caret offset from the last character to the # first so that SayAll will say all. - try: - text = cthulhu_state.locusOfFocus.queryText() - except NotImplementedError: - pass - else: - text.setCaretOffset(0) + if AXObject.supports_text(cthulhu_state.locusOfFocus): + AXText.set_caret_offset(cthulhu_state.locusOfFocus, 0) return self.sayAll(None) gtk.Script.onNameChanged(self, event) diff --git a/src/cthulhu/scripts/apps/gnome-documents/script_utilities.py b/src/cthulhu/scripts/apps/gnome-documents/script_utilities.py index a889953..6080a9c 100644 --- a/src/cthulhu/scripts/apps/gnome-documents/script_utilities.py +++ b/src/cthulhu/scripts/apps/gnome-documents/script_utilities.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" diff --git a/src/cthulhu/scripts/apps/gnome-documents/speech_generator.py b/src/cthulhu/scripts/apps/gnome-documents/speech_generator.py index c7c026a..050193a 100644 --- a/src/cthulhu/scripts/apps/gnome-documents/speech_generator.py +++ b/src/cthulhu/scripts/apps/gnome-documents/speech_generator.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom speech generator for gnome-documents.""" diff --git a/src/cthulhu/scripts/apps/gnome-shell/__init__.py b/src/cthulhu/scripts/apps/gnome-shell/__init__.py index 91372dc..b006f6c 100644 --- a/src/cthulhu/scripts/apps/gnome-shell/__init__.py +++ b/src/cthulhu/scripts/apps/gnome-shell/__init__.py @@ -20,7 +20,7 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from .script import Script diff --git a/src/cthulhu/scripts/apps/gnome-shell/formatting.py b/src/cthulhu/scripts/apps/gnome-shell/formatting.py index 7ecbfa5..1893347 100644 --- a/src/cthulhu/scripts/apps/gnome-shell/formatting.py +++ b/src/cthulhu/scripts/apps/gnome-shell/formatting.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" diff --git a/src/cthulhu/scripts/apps/gnome-shell/script.py b/src/cthulhu/scripts/apps/gnome-shell/script.py index 578c30e..75dd8d2 100644 --- a/src/cthulhu/scripts/apps/gnome-shell/script.py +++ b/src/cthulhu/scripts/apps/gnome-shell/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" @@ -34,6 +34,7 @@ import cthulhu.cthulhu as cthulhu import cthulhu.cthulhu_state as cthulhu_state import cthulhu.scripts.toolkits.clutter as clutter from cthulhu.ax_object import AXObject +from cthulhu.ax_text import AXText from cthulhu.ax_utilities import AXUtilities from .formatting import Formatting @@ -66,7 +67,7 @@ class Script(clutter.Script): return clutter.Script.skipObjectEvent(self, event) - def locusOfFocusChanged(self, event, oldFocus, newFocus): + def locus_of_focus_changed(self, event, oldFocus, newFocus): if event is not None and event.type == "window:activate" \ and newFocus is not None and not AXObject.get_name(newFocus): queuedEvent = self._getQueuedEvent("object:state-changed:focused", True) @@ -75,7 +76,7 @@ class Script(clutter.Script): debug.printMessage(debug.LEVEL_INFO, msg, True) return - super().locusOfFocusChanged(event, oldFocus, newFocus) + super().locus_of_focus_changed(event, oldFocus, newFocus) def onNameChanged(self, event): """Callback for object:property-change:accessible-name events.""" @@ -132,16 +133,15 @@ class Script(clutter.Script): clutter.Script.onFocusedChanged(self, event) def echoPreviousWord(self, obj, offset=None): - try: - text = obj.queryText() - except NotImplementedError: + if not AXObject.supports_text(obj): return False if not offset: - if text.caretOffset == -1: - offset = text.characterCount - 1 + caretOffset = AXText.get_caret_offset(obj) + if caretOffset == -1: + offset = AXText.get_character_count(obj) - 1 else: - offset = text.caretOffset - 1 + offset = caretOffset - 1 if offset == 0: return False diff --git a/src/cthulhu/scripts/apps/gnome-shell/script_utilities.py b/src/cthulhu/scripts/apps/gnome-shell/script_utilities.py index 091151f..d5958d7 100644 --- a/src/cthulhu/scripts/apps/gnome-shell/script_utilities.py +++ b/src/cthulhu/scripts/apps/gnome-shell/script_utilities.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" @@ -32,6 +32,7 @@ __license__ = "LGPL" import cthulhu.debug as debug import cthulhu.script_utilities as script_utilities from cthulhu.ax_object import AXObject +from cthulhu.ax_text import AXText from cthulhu.ax_selection import AXSelection from cthulhu.ax_utilities import AXUtilities @@ -65,7 +66,7 @@ class Utilities(script_utilities.Utilities): text = self.queryNonEmptyText(event.source) if text: - string = text.getText(0, -1) + string = AXText.get_all_text(event.source) if string: msg = f"GNOME SHELL: Returning last char in '{string}'" debug.printMessage(debug.LEVEL_INFO, msg, True) @@ -85,8 +86,8 @@ class Utilities(script_utilities.Utilities): debug.printTokens(debug.LEVEL_INFO, tokens, True) text = self.queryNonEmptyText(obj) - if text.getNSelections() > 0: - string = text.getText(0, -1) + if AXText.get_selected_ranges(obj): + string = AXText.get_all_text(obj) start, end = 0, len(string) tokens = [f"GNOME SHELL: Returning '{string}' ({start}, {end}) for", obj] diff --git a/src/cthulhu/scripts/apps/kwin/__init__.py b/src/cthulhu/scripts/apps/kwin/__init__.py index 470f7b8..99c9ba5 100644 --- a/src/cthulhu/scripts/apps/kwin/__init__.py +++ b/src/cthulhu/scripts/apps/kwin/__init__.py @@ -20,12 +20,12 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for kwin.""" -# https://gitlab.gnome.org/GNOME/cthulhu/-/issues/358 +# https://git.stormux.org/storm/cthulhu # ruff: noqa: F401 from .script import Script diff --git a/src/cthulhu/scripts/apps/kwin/script.py b/src/cthulhu/scripts/apps/kwin/script.py index f26e7bf..dd05c9a 100644 --- a/src/cthulhu/scripts/apps/kwin/script.py +++ b/src/cthulhu/scripts/apps/kwin/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for kwin.""" diff --git a/src/cthulhu/scripts/apps/kwin/script_utilities.py b/src/cthulhu/scripts/apps/kwin/script_utilities.py index a87d152..5c9dcf5 100644 --- a/src/cthulhu/scripts/apps/kwin/script_utilities.py +++ b/src/cthulhu/scripts/apps/kwin/script_utilities.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" diff --git a/src/cthulhu/scripts/apps/notification-daemon/__init__.py b/src/cthulhu/scripts/apps/notification-daemon/__init__.py index e782ac8..0fa412f 100644 --- a/src/cthulhu/scripts/apps/notification-daemon/__init__.py +++ b/src/cthulhu/scripts/apps/notification-daemon/__init__.py @@ -20,12 +20,12 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """ Custom script for notification daemon.""" -# https://gitlab.gnome.org/GNOME/cthulhu/-/issues/358 +# https://git.stormux.org/storm/cthulhu # ruff: noqa: F401 from .script import Script diff --git a/src/cthulhu/scripts/apps/notification-daemon/script.py b/src/cthulhu/scripts/apps/notification-daemon/script.py index 41eeebe..52861b7 100644 --- a/src/cthulhu/scripts/apps/notification-daemon/script.py +++ b/src/cthulhu/scripts/apps/notification-daemon/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """ Custom script for The notification daemon.""" diff --git a/src/cthulhu/scripts/apps/notify-osd/__init__.py b/src/cthulhu/scripts/apps/notify-osd/__init__.py index b8d617c..6696eff 100644 --- a/src/cthulhu/scripts/apps/notify-osd/__init__.py +++ b/src/cthulhu/scripts/apps/notify-osd/__init__.py @@ -20,12 +20,12 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """ Custom script for The notify-osd""" -# https://gitlab.gnome.org/GNOME/cthulhu/-/issues/358 +# https://git.stormux.org/storm/cthulhu # ruff: noqa: F401 from .script import Script diff --git a/src/cthulhu/scripts/apps/notify-osd/script.py b/src/cthulhu/scripts/apps/notify-osd/script.py index b541bb4..767a3fa 100644 --- a/src/cthulhu/scripts/apps/notify-osd/script.py +++ b/src/cthulhu/scripts/apps/notify-osd/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """ Custom script for The notify-osd""" @@ -36,6 +36,7 @@ import cthulhu.scripts.default as default import cthulhu.settings as settings import cthulhu.settings_manager as settings_manager from cthulhu.ax_object import AXObject +from cthulhu.ax_value import AXValue _settingsManager = settings_manager.getManager() @@ -47,10 +48,9 @@ _settingsManager = settings_manager.getManager() class Script(default.Script): def onValueChanged(self, event): - try: - ivalue = event.source.queryValue() - value = int(ivalue.currentValue) - except NotImplementedError: + if AXObject.supports_value(event.source): + value = int(AXValue.get_current_value(event.source)) + else: value = -1 if value >= 0: @@ -63,10 +63,9 @@ class Script(default.Script): def onNameChanged(self, event): """Callback for object:property-change:accessible-name events.""" - try: - ivalue = event.source.queryValue() - value = ivalue.currentValue - except NotImplementedError: + if AXObject.supports_value(event.source): + value = AXValue.get_current_value(event.source) + else: value = -1 message = "" diff --git a/src/cthulhu/scripts/apps/pidgin/__init__.py b/src/cthulhu/scripts/apps/pidgin/__init__.py index 91372dc..b006f6c 100644 --- a/src/cthulhu/scripts/apps/pidgin/__init__.py +++ b/src/cthulhu/scripts/apps/pidgin/__init__.py @@ -20,7 +20,7 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from .script import Script diff --git a/src/cthulhu/scripts/apps/pidgin/chat.py b/src/cthulhu/scripts/apps/pidgin/chat.py index c7cfc39..2c92b70 100644 --- a/src/cthulhu/scripts/apps/pidgin/chat.py +++ b/src/cthulhu/scripts/apps/pidgin/chat.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom chat module for Pidgin.""" diff --git a/src/cthulhu/scripts/apps/pidgin/script.py b/src/cthulhu/scripts/apps/pidgin/script.py index 3f65fd5..046ce21 100644 --- a/src/cthulhu/scripts/apps/pidgin/script.py +++ b/src/cthulhu/scripts/apps/pidgin/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for pidgin.""" diff --git a/src/cthulhu/scripts/apps/pidgin/script_utilities.py b/src/cthulhu/scripts/apps/pidgin/script_utilities.py index eb3349c..fab236e 100644 --- a/src/cthulhu/scripts/apps/pidgin/script_utilities.py +++ b/src/cthulhu/scripts/apps/pidgin/script_utilities.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Commonly-required utility methods needed by -- and potentially customized by -- application and toolkit scripts. They have @@ -41,7 +41,9 @@ from gi.repository import Atspi import cthulhu.debug as debug import cthulhu.script_utilities as script_utilities from cthulhu.ax_object import AXObject +from cthulhu.ax_table import AXTable from cthulhu.ax_utilities import AXUtilities +from cthulhu.ax_utilities_relation import AXUtilitiesRelation ############################################################################# # # @@ -82,18 +84,18 @@ class Utilities(script_utilities.Utilities): return script_utilities.Utilities.childNodes(self, obj) parent = AXObject.get_parent(obj) - try: - table = parent.queryTable() - except Exception: + table = AXTable.get_table(parent) + if table is None: + return [] + + if not AXUtilities.is_expanded(obj): return [] - else: - if not AXUtilities.is_expanded(obj): - return [] nodes = [] - index = self.cellIndex(obj) - row = table.getRowAtIndex(index) - col = table.getColumnAtIndex(index + 1) + row, col = AXTable.get_cell_coordinates(obj, prefer_attribute=False) + if row < 0 or col < 0: + return [] + col += 1 nodeLevel = self.nodeLevel(obj) # Candidates will be in the rows beneath the current row. @@ -101,14 +103,16 @@ class Utilities(script_utilities.Utilities): # soon as the node level of a candidate is equal or less # than our current level. # - for i in range(row+1, table.nRows): - cell = table.getAccessibleAt(i, col) + for i in range(row + 1, AXTable.get_row_count(table, prefer_attribute=False)): + cell = AXTable.get_cell_at(table, i, col) + if not cell: + continue nodeCell = AXObject.get_previous_sibling(cell) - relation = AXObject.get_relation(nodeCell, Atspi.RelationType.NODE_CHILD_OF) - if not relation: + nodeOf = AXUtilitiesRelation.get_is_node_child_of(nodeCell) + if not nodeOf: continue - nodeOf = relation.getTarget(0) + nodeOf = nodeOf[0] if self.isSameObject(obj, nodeOf): nodes.append(cell) elif self.nodeLevel(nodeOf) <= nodeLevel: @@ -134,19 +138,15 @@ class Utilities(script_utilities.Utilities): obj = AXObject.get_previous_sibling(obj) parent = AXObject.get_parent(obj) - try: - parent.queryTable() - except Exception: + if AXTable.get_table(parent) is None: return -1 nodes = [] node = obj done = False while not done: - relation = AXObject.get_relation(node, Atspi.RelationType.NODE_CHILD_OF) - node = None - if relation: - node = relation.get_target(0) + nodeOf = AXUtilitiesRelation.get_is_node_child_of(node) + node = nodeOf[0] if nodeOf else None # We want to avoid situations where something gives us an # infinite cycle of nodes. Bon Echo has been seen to do diff --git a/src/cthulhu/scripts/apps/pidgin/speech_generator.py b/src/cthulhu/scripts/apps/pidgin/speech_generator.py index 80ebe1a..ccf0978 100644 --- a/src/cthulhu/scripts/apps/pidgin/speech_generator.py +++ b/src/cthulhu/scripts/apps/pidgin/speech_generator.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" diff --git a/src/cthulhu/scripts/apps/smuxi-frontend-gnome/__init__.py b/src/cthulhu/scripts/apps/smuxi-frontend-gnome/__init__.py index 91372dc..b006f6c 100644 --- a/src/cthulhu/scripts/apps/smuxi-frontend-gnome/__init__.py +++ b/src/cthulhu/scripts/apps/smuxi-frontend-gnome/__init__.py @@ -20,7 +20,7 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from .script import Script diff --git a/src/cthulhu/scripts/apps/smuxi-frontend-gnome/chat.py b/src/cthulhu/scripts/apps/smuxi-frontend-gnome/chat.py index bbfeaaf..b307c99 100644 --- a/src/cthulhu/scripts/apps/smuxi-frontend-gnome/chat.py +++ b/src/cthulhu/scripts/apps/smuxi-frontend-gnome/chat.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom chat module for Smuxi.""" diff --git a/src/cthulhu/scripts/apps/smuxi-frontend-gnome/script.py b/src/cthulhu/scripts/apps/smuxi-frontend-gnome/script.py index edfb179..0118954 100644 --- a/src/cthulhu/scripts/apps/smuxi-frontend-gnome/script.py +++ b/src/cthulhu/scripts/apps/smuxi-frontend-gnome/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for Smuxi.""" diff --git a/src/cthulhu/scripts/apps/soffice/__init__.py b/src/cthulhu/scripts/apps/soffice/__init__.py index 91372dc..b006f6c 100644 --- a/src/cthulhu/scripts/apps/soffice/__init__.py +++ b/src/cthulhu/scripts/apps/soffice/__init__.py @@ -20,7 +20,7 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from .script import Script diff --git a/src/cthulhu/scripts/apps/soffice/braille_generator.py b/src/cthulhu/scripts/apps/soffice/braille_generator.py index 02c6a3c..dc12fac 100644 --- a/src/cthulhu/scripts/apps/soffice/braille_generator.py +++ b/src/cthulhu/scripts/apps/soffice/braille_generator.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for StarOffice and OpenOffice.""" diff --git a/src/cthulhu/scripts/apps/soffice/formatting.py b/src/cthulhu/scripts/apps/soffice/formatting.py index 985bf47..de453f1 100644 --- a/src/cthulhu/scripts/apps/soffice/formatting.py +++ b/src/cthulhu/scripts/apps/soffice/formatting.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom formatting for OpenOffice and StarOffice.""" diff --git a/src/cthulhu/scripts/apps/soffice/script.py b/src/cthulhu/scripts/apps/soffice/script.py index 5266eb5..b6dab74 100644 --- a/src/cthulhu/scripts/apps/soffice/script.py +++ b/src/cthulhu/scripts/apps/soffice/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for LibreOffice.""" @@ -49,6 +49,7 @@ import cthulhu.cthulhu_state as cthulhu_state import cthulhu.settings_manager as settings_manager import cthulhu.structural_navigation as structural_navigation from cthulhu.ax_object import AXObject +from cthulhu.ax_text import AXText from cthulhu.ax_utilities import AXUtilities from .braille_generator import BrailleGenerator @@ -323,21 +324,20 @@ class Script(default.Script): or not self.utilities.isTextArea(cthulhu_state.locusOfFocus): return default.Script.panBrailleLeft(self, inputEvent, panAmount) - text = cthulhu_state.locusOfFocus.queryText() - string, startOffset, endOffset = text.getTextAtOffset( - text.caretOffset, Atspi.TextBoundaryType.LINE_START) + if not AXObject.supports_text(cthulhu_state.locusOfFocus): + return default.Script.panBrailleLeft(self, inputEvent, panAmount) + + caretOffset = AXText.get_caret_offset(cthulhu_state.locusOfFocus) + string, startOffset, endOffset = AXText.get_line_at_offset( + cthulhu_state.locusOfFocus, caretOffset) if 0 < startOffset: - text.setCaretOffset(startOffset-1) + AXText.set_caret_offset(cthulhu_state.locusOfFocus, startOffset - 1) return True obj = self.utilities.findPreviousObject(cthulhu_state.locusOfFocus) - try: - text = obj.queryText() - except Exception: - pass - else: + if AXObject.supports_text(obj): cthulhu.setLocusOfFocus(None, obj, notifyScript=False) - text.setCaretOffset(text.characterCount) + AXText.set_caret_offset(obj, AXText.get_character_count(obj)) return True return default.Script.panBrailleLeft(self, inputEvent, panAmount) @@ -353,21 +353,20 @@ class Script(default.Script): or not self.utilities.isTextArea(cthulhu_state.locusOfFocus): return default.Script.panBrailleRight(self, inputEvent, panAmount) - text = cthulhu_state.locusOfFocus.queryText() - string, startOffset, endOffset = text.getTextAtOffset( - text.caretOffset, Atspi.TextBoundaryType.LINE_START) - if endOffset < text.characterCount: - text.setCaretOffset(endOffset) + if not AXObject.supports_text(cthulhu_state.locusOfFocus): + return default.Script.panBrailleRight(self, inputEvent, panAmount) + + caretOffset = AXText.get_caret_offset(cthulhu_state.locusOfFocus) + string, startOffset, endOffset = AXText.get_line_at_offset( + cthulhu_state.locusOfFocus, caretOffset) + if endOffset < AXText.get_character_count(cthulhu_state.locusOfFocus): + AXText.set_caret_offset(cthulhu_state.locusOfFocus, endOffset) return True obj = self.utilities.findNextObject(cthulhu_state.locusOfFocus) - try: - text = obj.queryText() - except Exception: - pass - else: + if AXObject.supports_text(obj): cthulhu.setLocusOfFocus(None, obj, notifyScript=False) - text.setCaretOffset(0) + AXText.set_caret_offset(obj, 0) return True return default.Script.panBrailleRight(self, inputEvent, panAmount) @@ -491,7 +490,7 @@ class Script(default.Script): return True - def locusOfFocusChanged(self, event, oldLocusOfFocus, newLocusOfFocus): + def locus_of_focus_changed(self, event, oldLocusOfFocus, newLocusOfFocus): """Called when the visual object with focus changes. Arguments: @@ -558,16 +557,13 @@ class Script(default.Script): voice = self.speechGenerator.voice(obj=newLocusOfFocus, string=string) self.speakMessage(string, voice=voice) self.updateBraille(newLocusOfFocus) - try: - text = newLocusOfFocus.queryText() - except Exception: - pass - else: - self._saveLastCursorPosition(newLocusOfFocus, text.caretOffset) + if AXObject.supports_text(newLocusOfFocus): + self._saveLastCursorPosition( + newLocusOfFocus, AXText.get_caret_offset(newLocusOfFocus)) return # Pass the event onto the parent class to be handled in the default way. - default.Script.locusOfFocusChanged(self, event, + default.Script.locus_of_focus_changed(self, event, oldLocusOfFocus, newLocusOfFocus) if not newLocusOfFocus: return @@ -860,7 +856,10 @@ class Script(default.Script): if isinstance(cthulhu_state.lastInputEvent, input_event.MouseButtonEvent): x = cthulhu_state.lastInputEvent.x y = cthulhu_state.lastInputEvent.y - weToggledIt = obj.queryComponent().contains(x, y, 0) + if AXObject.supports_component(obj): + weToggledIt = Atspi.Component.contains(obj, x, y, Atspi.CoordType.SCREEN) + else: + weToggledIt = False elif AXUtilities.is_focused(obj): weToggledIt = True else: @@ -952,14 +951,11 @@ class Script(default.Script): """To-be-removed. Returns the string, caretOffset, startOffset.""" if AXObject.get_role(AXObject.get_parent(obj)) == Atspi.Role.COMBO_BOX: - try: - text = obj.queryText() - except NotImplementedError: + if not AXObject.supports_text(obj): return ["", 0, 0] - if text.caretOffset < 0: - [lineString, startOffset, endOffset] = text.getTextAtOffset( - 0, Atspi.TextBoundaryType.LINE_START) + if AXText.get_caret_offset(obj) < 0: + lineString, startOffset, endOffset = AXText.get_line_at_offset(obj, 0) # Sometimes we get the trailing line-feed -- remove it # diff --git a/src/cthulhu/scripts/apps/soffice/script_utilities.py b/src/cthulhu/scripts/apps/soffice/script_utilities.py index e088aaa..bbf266b 100644 --- a/src/cthulhu/scripts/apps/soffice/script_utilities.py +++ b/src/cthulhu/scripts/apps/soffice/script_utilities.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Commonly-required utility methods needed by -- and potentially customized by -- application and toolkit scripts. They have @@ -44,8 +44,11 @@ import cthulhu.messages as messages import cthulhu.cthulhu_state as cthulhu_state import cthulhu.script_utilities as script_utilities from cthulhu.ax_object import AXObject +from cthulhu.ax_table import AXTable from cthulhu.ax_selection import AXSelection +from cthulhu.ax_text import AXText from cthulhu.ax_utilities import AXUtilities +from cthulhu.ax_utilities_relation import AXUtilitiesRelation ############################################################################# @@ -147,15 +150,11 @@ class Utilities(script_utilities.Utilities): if table is not None and not AXUtilities.is_table(table): table = AXObject.get_parent(table) - try: - iTable = table.queryTable() - except Exception: + table = AXTable.get_table(table) + if table is None: return -1, -1, None - index = self.cellIndex(cell) - row = iTable.getRowAtIndex(index) - column = iTable.getColumnAtIndex(index) - + row, column = AXTable.get_cell_coordinates(cell, prefer_attribute=False) return row, column, table def rowHeadersForCell(self, obj): @@ -193,13 +192,12 @@ class Utilities(script_utilities.Utilities): getColHeader = \ getColHeader and objCol!= self._script.pointOfReference.get("lastColumn") - parentTable = table.queryTable() rowHeader, colHeader = None, None if getColHeader: - colHeader = parentTable.getAccessibleAt(headersRow, objCol) + colHeader = AXTable.get_cell_at(table, headersRow, objCol) if getRowHeader: - rowHeader = parentTable.getAccessibleAt(objRow, headersCol) + rowHeader = AXTable.get_cell_at(table, objRow, headersCol) return rowHeader, colHeader @@ -330,16 +328,12 @@ class Utilities(script_utilities.Utilities): @staticmethod def _flowsFromOrToSelection(obj): - relationSet = AXObject.get_relations(obj) + relationSet = AXUtilitiesRelation.get_relations(obj) flows = [Atspi.RelationType.FLOWS_FROM, Atspi.RelationType.FLOWS_TO] relations = filter(lambda r: r.getRelationType() in flows, relationSet) targets = [r.getTarget(0) for r in relations] for target in targets: - try: - nSelections = target.queryText().getNSelections() - except Exception: - return False - if nSelections: + if AXText.get_selected_ranges(target): return True return False @@ -494,11 +488,7 @@ class Utilities(script_utilities.Utilities): if event.type.startswith("focus:"): if lastKey == "Return": - try: - charCount = event.source.queryText().characterCount - except Exception: - charCount = 0 - return charCount > 0 + return AXText.get_character_count(event.source) > 0 return False @@ -598,9 +588,7 @@ class Utilities(script_utilities.Utilities): return AXSelection.get_selected_children(obj) def getFirstCaretPosition(self, obj): - try: - obj.queryText() - except Exception: + if not AXObject.supports_text(obj): if AXObject.get_child_count(obj): return self.getFirstCaretPosition(AXObject.get_child(obj, 0)) @@ -639,16 +627,13 @@ class Utilities(script_utilities.Utilities): return res def _getCellNameForCoordinates(self, obj, row, col, includeContents=False): - try: - table = obj.queryTable() - except Exception: + if not AXObject.supports_table(obj): tokens = ["SOFFICE: Exception querying Table interface of", obj] debug.printTokens(debug.LEVEL_INFO, tokens, True) return - try: - cell = table.getAccessibleAt(row, col) - except Exception: + cell = AXTable.get_cell_at(obj, row, col) + if not cell: tokens = [f"SOFFICE: Exception getting cell ({row},{col}) of", obj] debug.printTokens(debug.LEVEL_INFO, tokens, True) return @@ -750,9 +735,9 @@ class Utilities(script_utilities.Utilities): if not (AXObject.supports_table(obj) and AXObject.supports_selection(obj)): return True - table = obj.queryTable() - cols = set(table.getSelectedColumns()) - rows = set(table.getSelectedRows()) + cols = set(AXTable.get_selected_columns(obj)) + rows = set(AXTable.get_selected_rows(obj)) + total_cols = AXTable.get_column_count(obj, prefer_attribute=False) selectedCols = sorted(cols.difference(set(self._calcSelectedColumns))) unselectedCols = sorted(set(self._calcSelectedColumns).difference(cols)) @@ -775,11 +760,11 @@ class Utilities(script_utilities.Utilities): self._calcSelectedColumns = list(cols) self._calcSelectedRows = list(rows) - if len(cols) == table.nColumns: + if len(cols) == total_cols: self._script.speakMessage(messages.DOCUMENT_SELECTED_ALL) return True - if not len(cols) and len(unselectedCols) == table.nColumns: + if not len(cols) and len(unselectedCols) == total_cols: self._script.speakMessage(messages.DOCUMENT_UNSELECTED_ALL) return True diff --git a/src/cthulhu/scripts/apps/soffice/speech_generator.py b/src/cthulhu/scripts/apps/soffice/speech_generator.py index e7afee9..db8645c 100644 --- a/src/cthulhu/scripts/apps/soffice/speech_generator.py +++ b/src/cthulhu/scripts/apps/soffice/speech_generator.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for StarOffice and OpenOffice.""" @@ -39,6 +39,7 @@ import cthulhu.messages as messages import cthulhu.settings_manager as settings_manager import cthulhu.speech_generator as speech_generator from cthulhu.ax_object import AXObject +from cthulhu.ax_text import AXText from cthulhu.ax_utilities import AXUtilities _settingsManager = settings_manager.getManager() @@ -342,24 +343,29 @@ class SpeechGenerator(speech_generator.SpeechGenerator): return [] result = [] - try: - text = obj.queryText() - objectText = \ - self._script.utilities.substring(obj, 0, -1) - extents = obj.queryComponent().getExtents(Atspi.CoordType.SCREEN) - except NotImplementedError: - pass - else: - tooLongCount = 0 - for i in range(0, len(objectText)): - [x, y, width, height] = text.getRangeExtents(i, i + 1, 0) - if x < extents.x: - tooLongCount += 1 - elif (x + width) > extents.x + extents.width: - tooLongCount += len(objectText) - i - break - if tooLongCount > 0: - result = [messages.charactersTooLong(tooLongCount)] + if AXObject.supports_text(obj): + objectText = self._script.utilities.substring(obj, 0, -1) + extents = None + if AXObject.supports_component(obj): + try: + extents = Atspi.Component.get_extents(obj, Atspi.CoordType.SCREEN) + except Exception: + extents = None + + if extents is not None: + tooLongCount = 0 + for i in range(0, len(objectText)): + rect = Atspi.Text.get_range_extents( + obj, i, i + 1, Atspi.CoordType.SCREEN) + x = rect.x + width = rect.width + if x < extents.x: + tooLongCount += 1 + elif (x + width) > extents.x + extents.width: + tooLongCount += len(objectText) - i + break + if tooLongCount > 0: + result = [messages.charactersTooLong(tooLongCount)] if result: result.extend(self.voice(speech_generator.SYSTEM, obj=obj, **args)) return result diff --git a/src/cthulhu/scripts/apps/soffice/spellcheck.py b/src/cthulhu/scripts/apps/soffice/spellcheck.py index 3bf0fe3..00ca72c 100644 --- a/src/cthulhu/scripts/apps/soffice/spellcheck.py +++ b/src/cthulhu/scripts/apps/soffice/spellcheck.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Customized support for spellcheck in LibreOffice.""" @@ -35,6 +35,7 @@ from cthulhu import debug from cthulhu import messages from cthulhu import spellcheck from cthulhu.ax_object import AXObject +from cthulhu.ax_text import AXText from cthulhu.ax_utilities import AXUtilities class SpellCheck(spellcheck.SpellCheck): @@ -115,17 +116,15 @@ class SpellCheck(spellcheck.SpellCheck): return index + 1, total def getMisspelledWord(self): - try: - text = self._errorWidget.queryText() - except Exception: + if not AXObject.supports_text(self._errorWidget): return "" offset, string = 0, "" - while 0 <= offset < text.characterCount: - attributes, start, end = text.getAttributeRun(offset, False) - attrs = dict([attr.split(":", 1) for attr in attributes]) + char_count = AXText.get_character_count(self._errorWidget) + while 0 <= offset < char_count: + attrs, start, end = AXText.get_text_attributes_at_offset(self._errorWidget, offset) if attrs.get("fg-color", "").replace(" ", "") == "255,0,0": - return text.getText(start, end) + return AXText.get_substring(self._errorWidget, start, end) offset = max(end, offset + 1) return string @@ -134,12 +133,10 @@ class SpellCheck(spellcheck.SpellCheck): if not self.isActive(): return False - try: - text = self._errorWidget.queryText() - except Exception: + if not AXObject.supports_text(self._errorWidget): return False - string = text.getText(0, -1) + string = AXText.get_all_text(self._errorWidget) if not string: return False diff --git a/src/cthulhu/scripts/apps/xfwm4/__init__.py b/src/cthulhu/scripts/apps/xfwm4/__init__.py index 0d25fd0..a2a1c8a 100644 --- a/src/cthulhu/scripts/apps/xfwm4/__init__.py +++ b/src/cthulhu/scripts/apps/xfwm4/__init__.py @@ -20,12 +20,12 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for xfwm4.""" -# https://gitlab.gnome.org/GNOME/cthulhu/-/issues/358 +# https://git.stormux.org/storm/cthulhu # ruff: noqa: F401 from .script import Script diff --git a/src/cthulhu/scripts/apps/xfwm4/script.py b/src/cthulhu/scripts/apps/xfwm4/script.py index 68f14e6..98ffc95 100644 --- a/src/cthulhu/scripts/apps/xfwm4/script.py +++ b/src/cthulhu/scripts/apps/xfwm4/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for xfwm4.""" diff --git a/src/cthulhu/scripts/default.py b/src/cthulhu/scripts/default.py index 8c38dfc..5199f32 100644 --- a/src/cthulhu/scripts/default.py +++ b/src/cthulhu/scripts/default.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """The default Script for presenting information to the user using both speech and Braille. This is based primarily on the de-facto @@ -37,7 +37,9 @@ __license__ = "LGPL" import gi gi.require_version('Atspi', '2.0') +gi.require_version('Gdk', '3.0') from gi.repository import Atspi +from gi.repository import Gdk import re import time @@ -49,6 +51,7 @@ import cthulhu.debug as debug import cthulhu.find as find import cthulhu.flat_review as flat_review import cthulhu.input_event as input_event +import cthulhu.input_event_manager as input_event_manager import cthulhu.keybindings as keybindings import cthulhu.messages as messages import cthulhu.cthulhu as cthulhu @@ -62,9 +65,12 @@ import cthulhu.sound as sound import cthulhu.speech as speech import cthulhu.speechserver as speechserver from cthulhu.ax_object import AXObject +from cthulhu.ax_value import AXValue +from cthulhu.ax_text import AXText from cthulhu.ax_utilities import AXUtilities +from cthulhu.ax_utilities_relation import AXUtilitiesRelation -_scriptManager = script_manager.getManager() +_scriptManager = script_manager.get_manager() _settingsManager = settings_manager.getManager() ######################################################################## @@ -124,6 +130,7 @@ class Script(script.Script): self._sayAllIsInterrupted = False self._sayAllContexts = [] self.grab_ids = [] + self._modifierGrabIds = [] if app: Atspi.Accessible.set_cache_mask( @@ -583,6 +590,7 @@ class Script(script.Script): for b in bound: for id in cthulhu.addKeyGrab(b): self.grab_ids.append(id) + self._addModifierGrabs() def removeKeyGrabs(self): """ Removes this script's AT-SPI key grabs. """ @@ -591,6 +599,35 @@ class Script(script.Script): for id in self.grab_ids: cthulhu.removeKeyGrab(id) self.grab_ids = [] + self._removeModifierGrabs() + + def _addModifierGrabs(self): + if cthulhu_state.device is None: + return + + if self._modifierGrabIds: + return + + manager = input_event_manager.get_manager() + for modifier in settings.cthulhuModifierKeys: + if modifier not in ["Insert", "KP_Insert"]: + continue + keyval = Gdk.keyval_from_name(modifier) + keycode = keybindings.getKeycode(modifier) + if not keyval or not keycode: + continue + grabId = manager.add_grab_for_modifier(modifier, keyval, keycode) + if grabId != -1: + self._modifierGrabIds.append((modifier, grabId)) + + def _removeModifierGrabs(self): + if not self._modifierGrabIds: + return + + manager = input_event_manager.get_manager() + for modifier, grabId in self._modifierGrabIds: + manager.remove_grab_for_modifier(modifier, grabId) + self._modifierGrabIds = [] def refreshKeyGrabs(self): """ Refreshes the enabled key grabs for this script. """ @@ -610,7 +647,7 @@ class Script(script.Script): def _saveFocusedObjectInfo(self, obj): """Saves some basic information about obj. Note that this method is - intended to be called primarily (if not only) by locusOfFocusChanged(). + intended to be called primarily (if not only) by locus_of_focus_changed(). It is expected that accessible event callbacks will update the point of reference data specific to that event. The goal here is to weed out duplicate events.""" @@ -635,14 +672,11 @@ class Script(script.Script): # We want to save the offset for text objects because some apps and # toolkits emit caret-moved events immediately after a text object # gains focus, even though the caret has not actually moved. - try: - text = obj.queryText() - caretOffset = text.caretOffset - except Exception: - pass - else: - self._saveLastCursorPosition(obj, max(0, caretOffset)) - self.utilities.updateCachedTextSelection(obj) + if AXObject.supports_text(obj): + caretOffset = AXText.get_caret_offset(obj) + if caretOffset >= 0: + self._saveLastCursorPosition(obj, max(0, caretOffset)) + self.utilities.updateCachedTextSelection(obj) # We want to save the current row and column of a newly focused # or selected table cell so that on subsequent cell focus/selection @@ -655,7 +689,7 @@ class Script(script.Script): self.pointOfReference['selectedChange'] = hash(obj), AXUtilities.is_selected(obj) self.pointOfReference['expandedChange'] = hash(obj), AXUtilities.is_expanded(obj) - def locusOfFocusChanged(self, event, oldLocusOfFocus, newLocusOfFocus): + def locus_of_focus_changed(self, event, oldLocusOfFocus, newLocusOfFocus): """Called when the visual object with focus changes. Arguments: @@ -872,13 +906,14 @@ class Script(script.Script): # caret position, we will get a caret event, which will # then update the braille. # - text = cthulhu_state.locusOfFocus.queryText() - [lineString, startOffset, endOffset] = text.getTextAtOffset( - text.caretOffset, - Atspi.TextBoundaryType.LINE_START) + obj = cthulhu_state.locusOfFocus + if not AXObject.supports_text(obj): + return True + caretOffset = AXText.get_caret_offset(obj) + lineString, startOffset, endOffset = AXText.get_line_at_offset(obj, caretOffset) movedCaret = False if startOffset > 0: - movedCaret = text.setCaretOffset(startOffset - 1) + movedCaret = AXText.set_caret_offset(obj, startOffset - 1) # If we didn't move the caret and we're in a terminal, we # jump into flat review to review the text. See @@ -943,12 +978,13 @@ class Script(script.Script): # tacking mode. When we set the caret position, we will get a # caret event, which will then update the braille. # - text = cthulhu_state.locusOfFocus.queryText() - [lineString, startOffset, endOffset] = text.getTextAtOffset( - text.caretOffset, - Atspi.TextBoundaryType.LINE_START) - if endOffset < text.characterCount: - text.setCaretOffset(endOffset) + obj = cthulhu_state.locusOfFocus + if not AXObject.supports_text(obj): + return True + caretOffset = AXText.get_caret_offset(obj) + lineString, startOffset, endOffset = AXText.get_line_at_offset(obj, caretOffset) + if endOffset < AXText.get_character_count(obj): + AXText.set_caret_offset(obj, endOffset) else: self.panBrailleInDirection(panAmount, panToLeft=False) # We might be panning through a flashed message. @@ -1009,9 +1045,10 @@ class Script(script.Script): if caretOffset >= 0: self.utilities.adjustTextSelection(obj, caretOffset) - texti = obj.queryText() - startOffset, endOffset = texti.getSelection(0) - self.utilities.setClipboardText(texti.getText(startOffset, endOffset)) + selections = AXText.get_selected_ranges(obj) + if selections: + startOffset, endOffset = selections[0] + self.utilities.setClipboardText(AXText.get_substring(obj, startOffset, endOffset)) return True @@ -1097,17 +1134,13 @@ class Script(script.Script): self.presentMessage(messages.LOCATION_NOT_FOUND_FULL) return True - try: - text = obj.queryText() - except NotImplementedError: + if not AXObject.supports_text(obj): utterances = self.speechGenerator.generateSpeech(obj) utterances.extend(self.tutorialGenerator.getTutorial(obj, False)) speech.speak(utterances) - except AttributeError: - pass else: if offset is None: - offset = text.caretOffset + offset = AXText.get_caret_offset(obj) speech.sayAll(self.textLines(obj, offset), self.__sayAllProgressCallback) @@ -1321,16 +1354,17 @@ class Script(script.Script): if self.flatReviewPresenter.is_active(): self.flatReviewPresenter.quit() - text = event.source.queryText() - try: - text.caretOffset - except Exception as error: - tokens = ["DEFAULT: Exception getting caretOffset for", event.source, ":", error] + if not AXObject.supports_text(event.source): + return + + caretOffset = AXText.get_caret_offset(event.source) + if caretOffset < 0: + tokens = ["DEFAULT: Invalid caretOffset for", event.source] debug.printTokens(debug.LEVEL_INFO, tokens, True) return - self._saveLastCursorPosition(event.source, text.caretOffset) - if text.getNSelections() > 0: + self._saveLastCursorPosition(event.source, caretOffset) + if AXText.get_selected_ranges(event.source): msg = "DEFAULT: Event source has text selections" debug.printMessage(debug.LEVEL_INFO, msg, True) self.utilities.handleTextSelectionChange(event.source) @@ -1663,8 +1697,8 @@ class Script(script.Script): return if _settingsManager.getSetting('speakMisspelledIndicator'): - offset = text.caretOffset - if not text.getText(offset, offset+1).isalnum(): + offset = AXText.get_caret_offset(event.source) + if not AXText.get_substring(event.source, offset, offset + 1).isalnum(): offset -= 1 if self.utilities.isWordMisspelled(event.source, offset-1) \ or self.utilities.isWordMisspelled(event.source, offset+1): @@ -1838,17 +1872,12 @@ class Script(script.Script): obj = event.source role = AXObject.get_role(obj) - try: - value = obj.queryValue() - currentValue = value.currentValue - except NotImplementedError: + if not AXObject.supports_value(obj): tokens = ["DEFAULT:", obj, "doesn't implement AtspiValue"] debug.printTokens(debug.LEVEL_INFO, tokens, True) return - except Exception: - tokens = ["DEFAULT: Exception getting current value for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return + + currentValue = AXValue.get_current_value(obj) if "oldValue" in self.pointOfReference \ and (currentValue == self.pointOfReference["oldValue"]): @@ -1956,7 +1985,7 @@ class Script(script.Script): cthulhu.setLocusOfFocus(event, None) cthulhu.setActiveWindow(None) - _scriptManager.setActiveScript(None, "Window deactivated") + _scriptManager.set_active_script(None, "Window deactivated") def onClipboardContentsChanged(self, *args): if self.flatReviewPresenter.is_active(): @@ -2050,13 +2079,9 @@ class Script(script.Script): if context.endOffset - context.startOffset > minCharCount: break - try: - text = context.obj.queryText() - except Exception: - pass - else: + if AXObject.supports_text(context.obj): cthulhu.setLocusOfFocus(None, context.obj, notifyScript=False) - text.setCaretOffset(context.startOffset) + AXText.set_caret_offset(context.obj, context.startOffset) self.sayAll(None, context.obj, context.startOffset) return True @@ -2065,13 +2090,9 @@ class Script(script.Script): if not _settingsManager.getSetting('rewindAndFastForwardInSayAll'): return False - try: - text = context.obj.queryText() - except Exception: - pass - else: + if AXObject.supports_text(context.obj): cthulhu.setLocusOfFocus(None, context.obj, notifyScript=False) - text.setCaretOffset(context.endOffset) + AXText.set_caret_offset(context.obj, context.endOffset) self.sayAll(None, context.obj, context.endOffset) return True @@ -2082,11 +2103,9 @@ class Script(script.Script): # the visual progress of what is being spoken as well as # positioning the cursor when speech has stopped.]]] # - try: - text = context.obj.queryText() - char = text.getText(context.currentOffset, context.currentOffset+1) - except Exception: + if not AXObject.supports_text(context.obj): return + char = AXText.get_substring(context.obj, context.currentOffset, context.currentOffset + 1) # Setting the caret at the offset of an embedded object results in # focus changes. @@ -2110,16 +2129,17 @@ class Script(script.Script): self._inSayAll = False self._sayAllContexts = [] cthulhu.emitRegionChanged(context.obj, context.currentOffset) - text.setCaretOffset(context.currentOffset) + AXText.set_caret_offset(context.obj, context.currentOffset) elif progressType == speechserver.SayAllContext.COMPLETED: cthulhu.setLocusOfFocus(None, context.obj, notifyScript=False) cthulhu.emitRegionChanged(context.obj, context.currentOffset, mode=cthulhu.SAY_ALL) - text.setCaretOffset(context.currentOffset) + AXText.set_caret_offset(context.obj, context.currentOffset) # If there is a selection, clear it. See bug #489504 for more details. # - if text.getNSelections() > 0: - text.setSelection(0, context.currentOffset, context.currentOffset) + if AXText.get_selected_ranges(context.obj): + AXText.set_selected_text( + context.obj, context.currentOffset, context.currentOffset) def inSayAll(self, treatInterruptedAsIn=True): if self._inSayAll: @@ -2151,20 +2171,17 @@ class Script(script.Script): interface. """ - try: - text = obj.queryText() - except NotImplementedError: + if not AXObject.supports_text(obj): return False - offset = text.caretOffset - 1 - previousOffset = text.caretOffset - 2 + caretOffset = AXText.get_caret_offset(obj) + offset = caretOffset - 1 + previousOffset = caretOffset - 2 if (offset < 0 or previousOffset < 0): return False - [currentChar, startOffset, endOffset] = \ - text.getTextAtOffset(offset, Atspi.TextBoundaryType.CHAR) - [previousChar, startOffset, endOffset] = \ - text.getTextAtOffset(previousOffset, Atspi.TextBoundaryType.CHAR) + currentChar, _, _ = AXText.get_character_at_offset(obj, offset) + previousChar, _, _ = AXText.get_character_at_offset(obj, previousOffset) if not self.utilities.isSentenceDelimiter(currentChar, previousChar): return False @@ -2173,16 +2190,15 @@ class Script(script.Script): # work our way to the beginning of the sentence, stopping when # we hit another sentence delimiter. # - sentenceEndOffset = text.caretOffset - 2 + sentenceEndOffset = caretOffset - 2 sentenceStartOffset = sentenceEndOffset while sentenceStartOffset >= 0: - [currentChar, startOffset, endOffset] = \ - text.getTextAtOffset(sentenceStartOffset, - Atspi.TextBoundaryType.CHAR) - [previousChar, startOffset, endOffset] = \ - text.getTextAtOffset(sentenceStartOffset-1, - Atspi.TextBoundaryType.CHAR) + currentChar, _, _ = AXText.get_character_at_offset(obj, sentenceStartOffset) + if sentenceStartOffset - 1 >= 0: + previousChar, _, _ = AXText.get_character_at_offset(obj, sentenceStartOffset - 1) + else: + previousChar = "" if self.utilities.isSentenceDelimiter(currentChar, previousChar): break else: @@ -2225,24 +2241,20 @@ class Script(script.Script): end of the word. """ - try: - text = obj.queryText() - except NotImplementedError: + if not AXObject.supports_text(obj): return False if not offset: - if text.caretOffset == -1: - offset = text.characterCount + caretOffset = AXText.get_caret_offset(obj) + if caretOffset == -1: + offset = AXText.get_character_count(obj) else: - offset = text.caretOffset - 1 + offset = caretOffset - 1 if (offset < 0): return False - [char, startOffset, endOffset] = \ - text.getTextAtOffset( \ - offset, - Atspi.TextBoundaryType.CHAR) + char, _, _ = AXText.get_character_at_offset(obj, offset) if not self.utilities.isWordDelimiter(char): return False @@ -2255,10 +2267,7 @@ class Script(script.Script): wordStartOffset = wordEndOffset while wordStartOffset >= 0: - [char, startOffset, endOffset] = \ - text.getTextAtOffset( \ - wordStartOffset, - Atspi.TextBoundaryType.CHAR) + char, _, _ = AXText.get_character_at_offset(obj, wordStartOffset) if self.utilities.isWordDelimiter(char): break else: @@ -2292,8 +2301,10 @@ class Script(script.Script): interface """ - text = obj.queryText() - offset = text.caretOffset + if not AXObject.supports_text(obj): + return + + offset = AXText.get_caret_offset(obj) # If we have selected text and the last event was a move to the # right, then speak the character to the left of where the text @@ -2304,8 +2315,7 @@ class Script(script.Script): and eventString in ["Right", "Down"]: offset -= 1 - character, startOffset, endOffset = text.getTextAtOffset( - offset, Atspi.TextBoundaryType.CHAR) + character, startOffset, endOffset = AXText.get_character_at_offset(obj, offset) cthulhu.emitRegionChanged(obj, startOffset, endOffset, cthulhu.CARET_TRACKING) if not character or character == '\r': @@ -2313,9 +2323,8 @@ class Script(script.Script): speakBlankLines = _settingsManager.getSetting('speakBlankLines') if character == "\n": - line = text.getTextAtOffset(max(0, offset), - Atspi.TextBoundaryType.LINE_START) - if not line[0] or line[0] == "\n": + line, _, _ = AXText.get_line_at_offset(obj, max(0, offset)) + if not line or line == "\n": # This is a blank line. Announce it if the user requested # that blank lines be spoken. if speakBlankLines: @@ -2419,13 +2428,12 @@ class Script(script.Script): def sayWord(self, obj): """Speaks the word at the caret, taking into account the previous caret position.""" - try: - text = obj.queryText() - offset = text.caretOffset - except Exception: + if not AXObject.supports_text(obj): self.sayCharacter(obj) return + offset = AXText.get_caret_offset(obj) + word, startOffset, endOffset = \ self.utilities.getWordAtOffsetAdjustedForNavigation(obj, offset) @@ -2437,7 +2445,7 @@ class Script(script.Script): startOffset += 1 elif word.endswith("\n"): endOffset -= 1 - word = text.getText(startOffset, endOffset) + word = AXText.get_substring(obj, startOffset, endOffset) # sayPhrase is useful because it handles punctuation verbalization, but we don't want # to trigger its whitespace presentation. @@ -2445,7 +2453,7 @@ class Script(script.Script): if matches: startOffset += matches[0].start() endOffset -= len(word) - matches[-1].end() - word = text.getText(startOffset, endOffset) + word = AXText.get_substring(obj, startOffset, endOffset) string = word.replace("\n", "\\n") msg = ( @@ -2637,27 +2645,25 @@ class Script(script.Script): """ self._sayAllIsInterrupted = False - try: - text = obj.queryText() - except Exception: + if not AXObject.supports_text(obj): self._inSayAll = False self._sayAllContexts = [] return self._inSayAll = True - length = text.characterCount + length = AXText.get_character_count(obj) if offset is None: - offset = text.caretOffset + offset = AXText.get_caret_offset(obj) # Determine the correct "say all by" mode to use. # sayAllStyle = _settingsManager.getSetting('sayAllStyle') if sayAllStyle == settings.SAYALL_STYLE_SENTENCE: - mode = Atspi.TextBoundaryType.SENTENCE_START + mode = "sentence" elif sayAllStyle == settings.SAYALL_STYLE_LINE: - mode = Atspi.TextBoundaryType.LINE_START + mode = "line" else: - mode = Atspi.TextBoundaryType.LINE_START + mode = "line" priorObj = obj @@ -2669,24 +2675,25 @@ class Script(script.Script): lastEndOffset = -1 while offset < length: - [lineString, startOffset, endOffset] = text.getTextAtOffset( - offset, mode) + if mode == "sentence": + lineString, startOffset, endOffset = AXText.get_sentence_at_offset(obj, offset) + else: + lineString, startOffset, endOffset = AXText.get_line_at_offset(obj, offset) # Some applications that don't support sentence boundaries # will provide the line boundary results instead; others # will return nothing. # if not lineString: - mode = Atspi.TextBoundaryType.LINE_START - [lineString, startOffset, endOffset] = \ - text.getTextAtOffset(offset, mode) + mode = "line" + lineString, startOffset, endOffset = AXText.get_line_at_offset(obj, offset) - if endOffset > text.characterCount: + if endOffset > length: tokens = ["DEFAULT: end offset", endOffset, " > character count", - text.characterCount, - "resulting from text.getTextAtOffset(", offset, mode, ") for", obj] + length, + "resulting from getTextAtOffset(", offset, mode, ") for", obj] debug.printTokens(debug.LEVEL_INFO, tokens, True) - endOffset = text.characterCount + endOffset = length # [[[WDW - HACK: this is here because getTextAtOffset # tends not to be implemented consistently across toolkits. @@ -2722,17 +2729,15 @@ class Script(script.Script): yield [context, voice] moreLines = False - relation = AXObject.get_relation(obj, Atspi.RelationType.FLOWS_TO) - if relation: + flowsTo = AXUtilitiesRelation.get_flows_to(obj) + if flowsTo: priorObj = obj - obj = relation.getTarget(0) + obj = flowsTo[0] - try: - text = obj.queryText() - except NotImplementedError: + if not AXObject.supports_text(obj): return - length = text.characterCount + length = AXText.get_character_count(obj) offset = 0 moreLines = True break @@ -2749,89 +2754,50 @@ class Script(script.Script): def getTextLineAtCaret(self, obj, offset=None, startOffset=None, endOffset=None): """To-be-removed. Returns the string, caretOffset, startOffset.""" - try: - text = obj.queryText() - offset = text.caretOffset - characterCount = text.characterCount - except NotImplementedError: - return ["", 0, 0] - except Exception: - tokens = ["DEFAULT: Exception getting offset and length for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + if not AXObject.supports_text(obj): return ["", 0, 0] + offset = AXText.get_caret_offset(obj) + characterCount = AXText.get_character_count(obj) if characterCount == 0: return ["", 0, 0] + if startOffset is not None and endOffset is not None: + return [AXText.get_substring(obj, startOffset, endOffset), offset, startOffset] + targetOffset = startOffset if targetOffset is None: targetOffset = max(0, offset) - # The offset might be positioned at the very end of the text area. - # In these cases, calling text.getTextAtOffset on an offset that's - # not positioned to a character can yield unexpected results. In - # particular, we'll see the Gecko toolkit return a start and end - # offset of (0, 0), and we'll see other implementations, such as - # gedit, return reasonable results (i.e., gedit will give us the - # last line). - # - # In order to accommodate the differing behavior of different - # AT-SPI implementations, we'll make sure we give getTextAtOffset - # the offset of an actual character. Then, we'll do a little check - # to see if that character is a newline - if it is, we'll treat it - # as the line. - # if targetOffset == characterCount: fixedTargetOffset = max(0, targetOffset - 1) - character = text.getText(fixedTargetOffset, fixedTargetOffset + 1) + character, _, _ = AXText.get_character_at_offset(obj, fixedTargetOffset) else: fixedTargetOffset = targetOffset character = None - if (targetOffset == characterCount) \ - and (character == "\n"): + if (targetOffset == characterCount) and (character == "\n"): lineString = "" startOffset = fixedTargetOffset else: - # Get the line containing the caret. [[[TODO: HACK WDW - If - # there's only 1 character in the string, well, we get it. We - # do this because Gecko's implementation of getTextAtOffset - # is broken if there is just one character in the string.]]] - # - if (characterCount == 1): - lineString = text.getText(fixedTargetOffset, fixedTargetOffset + 1) + if characterCount == 1: + lineString = AXText.get_substring(obj, fixedTargetOffset, fixedTargetOffset + 1) startOffset = fixedTargetOffset else: if fixedTargetOffset == -1: fixedTargetOffset = characterCount - try: - [lineString, startOffset, endOffset] = text.getTextAtOffset( - fixedTargetOffset, Atspi.TextBoundaryType.LINE_START) - - # Chrome fix: Handle case where get_line_at_offset returns the line - # after the offset, which seems to happen when the character at offset - # is an embedded object at a line boundary. - if 0 <= fixedTargetOffset < startOffset: - backup_offset = fixedTargetOffset - 1 - tokens = [f"DEFAULT: Start offset {startOffset} is greater than target offset {fixedTargetOffset}. Trying with offset {backup_offset}"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - [lineString, startOffset, endOffset] = text.getTextAtOffset( - backup_offset, Atspi.TextBoundaryType.LINE_START) - - except Exception: - return ["", 0, 0] + lineString, startOffset, endOffset = AXText.get_line_at_offset(obj, fixedTargetOffset) + + if 0 <= fixedTargetOffset < startOffset: + backup_offset = fixedTargetOffset - 1 + tokens = [f"DEFAULT: Start offset {startOffset} is greater than target offset {fixedTargetOffset}. Trying with offset {backup_offset}"] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + lineString, startOffset, endOffset = AXText.get_line_at_offset(obj, backup_offset) - # Sometimes we get the trailing line-feed-- remove it - # It is important that these are in order. - # In some circumstances we might get: - # word word\r\n - # so remove \n, and then remove \r. - # See bgo#619332. - # lineString = lineString.rstrip('\n') lineString = lineString.rstrip('\r') - return [lineString, text.caretOffset, startOffset] + return [lineString, offset, startOffset] def phoneticSpellCurrentItem(self, itemString): """Phonetically spell the current flat review word or line. @@ -2875,29 +2841,24 @@ class Script(script.Script): """ if _settingsManager.getSetting('speakMisspelledIndicator'): - try: - text = obj.queryText() - except Exception: + if not AXObject.supports_text(obj): return # If we're on whitespace, we cannot be on a misspelled word. # - charAndOffsets = \ - text.getTextAtOffset(offset, Atspi.TextBoundaryType.CHAR) - if not charAndOffsets[0].strip() \ - or self.utilities.isWordDelimiter(charAndOffsets[0]): - self._lastWordCheckedForSpelling = charAndOffsets[0] + char, _, _ = AXText.get_character_at_offset(obj, offset) + if not char.strip() or self.utilities.isWordDelimiter(char): + self._lastWordCheckedForSpelling = char return - wordAndOffsets = \ - text.getTextAtOffset(offset, Atspi.TextBoundaryType.WORD_START) + word, _, _ = AXText.get_word_at_offset(obj, offset) if self.utilities.isWordMisspelled(obj, offset) \ - and wordAndOffsets[0] != self._lastWordCheckedForSpelling: + and word != self._lastWordCheckedForSpelling: self.speakMessage(messages.MISSPELLED) # Store this word so that we do not continue to present the # presence of the red squiggly as the user arrows amongst # the characters. # - self._lastWordCheckedForSpelling = wordAndOffsets[0] + self._lastWordCheckedForSpelling = word ############################################################################ # # @@ -2919,7 +2880,7 @@ class Script(script.Script): """Convenience method to present the KeyboardEvent event. Returns True if we fully present the event; False otherwise.""" - if not event.isPressedKey(): + if not event.is_pressed_key(): self._sayAllIsInterrupted = False self.utilities.clearCachedCommandState() @@ -2936,11 +2897,11 @@ class Script(script.Script): if role == Atspi.Role.PASSWORD_TEXT and not event.isLockingKey(): return False - if not event.isPressedKey(): + if not event.is_pressed_key(): return False braille.displayKeyEvent(event) - cthulhuModifierPressed = event.isCthulhuModifier() and event.isPressedKey() + cthulhuModifierPressed = event.isCthulhuModifier() and event.is_pressed_key() if event.isCharacterEchoable() and not cthulhuModifierPressed: return False @@ -3371,7 +3332,7 @@ class Script(script.Script): rather than calling speech.speakKeyEvent directly.""" string = None - if event.isPrintableKey(): + if event.is_printable_key(): string = event.event_string voice = self.speechGenerator.voice(string=string) diff --git a/src/cthulhu/scripts/self_voicing.py b/src/cthulhu/scripts/self_voicing.py index e49ec2e..20f0dc7 100644 --- a/src/cthulhu/scripts/self_voicing.py +++ b/src/cthulhu/scripts/self_voicing.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """A script to do nothing. This is for self-voicing apps.""" diff --git a/src/cthulhu/scripts/sleepmode/__init__.py b/src/cthulhu/scripts/sleepmode/__init__.py index 241725e..bb5107e 100644 --- a/src/cthulhu/scripts/sleepmode/__init__.py +++ b/src/cthulhu/scripts/sleepmode/__init__.py @@ -20,12 +20,12 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Sleep mode script for Cthulhu.""" -from .script import Script, getScript +from .script import Script, get_script -# Ensure getScript is available at module level -__all__ = ['Script', 'getScript'] \ No newline at end of file +# Ensure get_script is available at module level +__all__ = ['Script', 'get_script'] \ No newline at end of file diff --git a/src/cthulhu/scripts/sleepmode/braille_generator.py b/src/cthulhu/scripts/sleepmode/braille_generator.py index abe134d..4edf484 100644 --- a/src/cthulhu/scripts/sleepmode/braille_generator.py +++ b/src/cthulhu/scripts/sleepmode/braille_generator.py @@ -19,8 +19,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Braille Generator for Sleep Mode. Does nothing.""" diff --git a/src/cthulhu/scripts/sleepmode/script.py b/src/cthulhu/scripts/sleepmode/script.py index 58f8b3e..dfb2271 100644 --- a/src/cthulhu/scripts/sleepmode/script.py +++ b/src/cthulhu/scripts/sleepmode/script.py @@ -19,8 +19,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Script for sleep mode where Cthulhu ignores events and commands. @@ -151,6 +151,6 @@ class Script(default.Script): return True -def getScript(app): +def get_script(app): """Returns the script for the given application.""" return Script(app) \ No newline at end of file diff --git a/src/cthulhu/scripts/sleepmode/script_utilities.py b/src/cthulhu/scripts/sleepmode/script_utilities.py index 13c5ee5..0744a41 100644 --- a/src/cthulhu/scripts/sleepmode/script_utilities.py +++ b/src/cthulhu/scripts/sleepmode/script_utilities.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Utilities for Sleep Mode. Helps ensure we do nothing. When nothing is done, nothing is left undone.""" diff --git a/src/cthulhu/scripts/sleepmode/speech_generator.py b/src/cthulhu/scripts/sleepmode/speech_generator.py index 686d1cf..37da1df 100644 --- a/src/cthulhu/scripts/sleepmode/speech_generator.py +++ b/src/cthulhu/scripts/sleepmode/speech_generator.py @@ -19,8 +19,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Speech Generator for Sleep Mode. Does nothing.""" diff --git a/src/cthulhu/scripts/switcher/__init__.py b/src/cthulhu/scripts/switcher/__init__.py index 1a92748..348807d 100644 --- a/src/cthulhu/scripts/switcher/__init__.py +++ b/src/cthulhu/scripts/switcher/__init__.py @@ -20,12 +20,12 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for basic switchers like Metacity.""" -# https://gitlab.gnome.org/GNOME/cthulhu/-/issues/358 +# https://git.stormux.org/storm/cthulhu # ruff: noqa: F401 from .script import Script from .script_utilities import Utilities diff --git a/src/cthulhu/scripts/switcher/script.py b/src/cthulhu/scripts/switcher/script.py index 4c8ae34..e3e40a7 100644 --- a/src/cthulhu/scripts/switcher/script.py +++ b/src/cthulhu/scripts/switcher/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for basic switchers like Metacity.""" diff --git a/src/cthulhu/scripts/switcher/script_utilities.py b/src/cthulhu/scripts/switcher/script_utilities.py index 90a870f..cc4d21a 100644 --- a/src/cthulhu/scripts/switcher/script_utilities.py +++ b/src/cthulhu/scripts/switcher/script_utilities.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" diff --git a/src/cthulhu/scripts/terminal/__init__.py b/src/cthulhu/scripts/terminal/__init__.py index 2eb914f..57ee8c8 100644 --- a/src/cthulhu/scripts/terminal/__init__.py +++ b/src/cthulhu/scripts/terminal/__init__.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from .braille_generator import BrailleGenerator from .script import Script diff --git a/src/cthulhu/scripts/terminal/braille_generator.py b/src/cthulhu/scripts/terminal/braille_generator.py index fb51d18..1854e20 100644 --- a/src/cthulhu/scripts/terminal/braille_generator.py +++ b/src/cthulhu/scripts/terminal/braille_generator.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" diff --git a/src/cthulhu/scripts/terminal/script.py b/src/cthulhu/scripts/terminal/script.py index 055c732..f098fcd 100644 --- a/src/cthulhu/scripts/terminal/script.py +++ b/src/cthulhu/scripts/terminal/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" @@ -31,6 +31,8 @@ __license__ = "LGPL" from cthulhu import debug from cthulhu import cthulhu +from cthulhu.ax_object import AXObject +from cthulhu.ax_text import AXText from cthulhu.scripts import default from .braille_generator import BrailleGenerator @@ -107,19 +109,15 @@ class Script(default.Script): debug.printMessage(debug.LEVEL_INFO, msg, True) return - try: - text = event.source.queryText() - except Exception: - pass - else: - self._saveLastCursorPosition(event.source, text.caretOffset) + if AXObject.supports_text(event.source): + self._saveLastCursorPosition(event.source, AXText.get_caret_offset(event.source)) self.utilities.updateCachedTextSelection(event.source) def presentKeyboardEvent(self, event): - if not event.isPrintableKey(): + if not event.is_printable_key(): return super().presentKeyboardEvent(event) - if event.isPressedKey(): + if event.is_pressed_key(): return False self._sayAllIsInterrupted = False @@ -129,14 +127,13 @@ class Script(default.Script): # We have no reliable way of knowing a password is being entered into # a terminal -- other than the fact that the text typed isn't there. - try: - text = event.getObject().queryText() - offset = text.caretOffset - prevChar = text.getText(offset - 1, offset) - char = text.getText(offset, offset + 1) - except Exception: + if not AXObject.supports_text(event.get_object()): return False + offset = AXText.get_caret_offset(event.get_object()) + prevChar = AXText.get_substring(event.get_object(), offset - 1, offset) + char = AXText.get_substring(event.get_object(), offset, offset + 1) + string = event.event_string if string not in [prevChar, "space", char]: return False diff --git a/src/cthulhu/scripts/terminal/script_utilities.py b/src/cthulhu/scripts/terminal/script_utilities.py index ed31327..1596c83 100644 --- a/src/cthulhu/scripts/terminal/script_utilities.py +++ b/src/cthulhu/scripts/terminal/script_utilities.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" @@ -40,6 +40,8 @@ from cthulhu import keybindings from cthulhu import cthulhu_state from cthulhu import script_utilities from cthulhu import settings_manager +from cthulhu.ax_object import AXObject +from cthulhu.ax_text import AXText from cthulhu.ax_utilities import AXUtilities _settingsManager = settings_manager.getManager() @@ -73,21 +75,15 @@ class Utilities(script_utilities.Utilities): if self.isClipboardTextChangedEvent(event): return event.any_data - try: - text = event.source.queryText() - except Exception: - tokens = ["ERROR: Exception querying text for", event.source] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + if not AXObject.supports_text(event.source): return event.any_data start, end = event.detail1, event.detail1 + len(event.any_data) - boundary = Atspi.TextBoundaryType.LINE_START - - firstLine = text.getTextAtOffset(start, boundary) + firstLine = AXText.get_line_at_offset(event.source, start) tokens = ["TERMINAL: First line of insertion:", firstLine] debug.printTokens(debug.LEVEL_INFO, tokens, True) - lastLine = text.getTextAtOffset(end - 1, boundary) + lastLine = AXText.get_line_at_offset(event.source, end - 1) tokens = ["TERMINAL: Last line of insertion:", firstLine] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -96,7 +92,7 @@ class Utilities(script_utilities.Utilities): debug.printMessage(debug.LEVEL_INFO, msg, True) return event.any_data - currentLine = text.getTextAtOffset(text.caretOffset, boundary) + currentLine = AXText.get_line_at_offset(event.source, AXText.get_caret_offset(event.source)) tokens = ["TERMINAL: Current line:", firstLine] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -111,7 +107,7 @@ class Utilities(script_utilities.Utilities): if lastLine[0].endswith("\n"): end -= 1 - adjusted = text.getText(start, end) + adjusted = AXText.get_substring(event.source, start, end) if adjusted: tokens = ["TERMINAL: Adjusted insertion: '", adjusted, "'"] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -123,14 +119,10 @@ class Utilities(script_utilities.Utilities): return adjusted def insertionEndsAtCaret(self, event): - try: - text = event.source.queryText() - except Exception: - tokens = ["ERROR: Exception querying text for", event.source] - debug.printTokens(debug.LEVEL_INFO, tokens, True) + if not AXObject.supports_text(event.source): return False - return text.caretOffset == event.detail1 + event.detail2 + return AXText.get_caret_offset(event.source) == event.detail1 + event.detail2 def isEditableTextArea(self, obj): if AXUtilities.is_terminal(obj): diff --git a/src/cthulhu/scripts/terminal/speech_generator.py b/src/cthulhu/scripts/terminal/speech_generator.py index 7f948d9..76b4708 100644 --- a/src/cthulhu/scripts/terminal/speech_generator.py +++ b/src/cthulhu/scripts/terminal/speech_generator.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" diff --git a/src/cthulhu/scripts/toolkits/Chromium/__init__.py b/src/cthulhu/scripts/toolkits/Chromium/__init__.py index 82fc251..44f2022 100644 --- a/src/cthulhu/scripts/toolkits/Chromium/__init__.py +++ b/src/cthulhu/scripts/toolkits/Chromium/__init__.py @@ -20,11 +20,11 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for Chromium.""" -# https://gitlab.gnome.org/GNOME/cthulhu/-/issues/358 +# https://git.stormux.org/storm/cthulhu # ruff: noqa: F401 from .script import Script diff --git a/src/cthulhu/scripts/toolkits/Chromium/braille_generator.py b/src/cthulhu/scripts/toolkits/Chromium/braille_generator.py index a8ba75f..fb1ddfa 100644 --- a/src/cthulhu/scripts/toolkits/Chromium/braille_generator.py +++ b/src/cthulhu/scripts/toolkits/Chromium/braille_generator.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom braille generator for Chromium.""" diff --git a/src/cthulhu/scripts/toolkits/Chromium/script.py b/src/cthulhu/scripts/toolkits/Chromium/script.py index 45ed7d1..b98de3d 100644 --- a/src/cthulhu/scripts/toolkits/Chromium/script.py +++ b/src/cthulhu/scripts/toolkits/Chromium/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for Chromium.""" @@ -73,15 +73,15 @@ class Script(web.Script): return super().isActivatableEvent(event) - def locusOfFocusChanged(self, event, oldFocus, newFocus): + def locus_of_focus_changed(self, event, oldFocus, newFocus): """Handles changes of focus of interest to the script.""" - if super().locusOfFocusChanged(event, oldFocus, newFocus): + if super().locus_of_focus_changed(event, oldFocus, newFocus): return msg = "CHROMIUM: Passing along event to default script" debug.printMessage(debug.LEVEL_INFO, msg, True) - default.Script.locusOfFocusChanged(self, event, oldFocus, newFocus) + default.Script.locus_of_focus_changed(self, event, oldFocus, newFocus) def onActiveChanged(self, event): """Callback for object:state-changed:active accessibility events.""" @@ -320,7 +320,7 @@ class Script(web.Script): if event.detail1 and not self.utilities.inDocumentContent(event.source): # The popup for an input with autocomplete on is a listbox child of a nameless frame. # It lives outside of the document and also doesn't fire selection-changed events. - if listbox := AXObject.get_parent(event.source, AXUtilities.is_list_box): + if listbox := AXObject.find_ancestor_inclusive(event.source, AXUtilities.is_list_box): parent = AXObject.get_parent(listbox) if AXUtilities.is_frame(parent) and not AXObject.get_name(parent): msg = "CHROMIUM: Event source believed to be in autocomplete popup" diff --git a/src/cthulhu/scripts/toolkits/Chromium/script_utilities.py b/src/cthulhu/scripts/toolkits/Chromium/script_utilities.py index 5c8793b..a3110be 100644 --- a/src/cthulhu/scripts/toolkits/Chromium/script_utilities.py +++ b/src/cthulhu/scripts/toolkits/Chromium/script_utilities.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script utilities for Chromium""" @@ -41,6 +41,7 @@ from cthulhu import cthulhu_state from cthulhu.scripts import web from cthulhu.ax_object import AXObject from cthulhu.ax_utilities import AXUtilities +from cthulhu.ax_utilities_relation import AXUtilitiesRelation class Utilities(web.Utilities): @@ -220,11 +221,11 @@ class Utilities(web.Utilities): return result def autocompleteForPopup(self, obj): - relation = AXObject.get_relation(obj, Atspi.RelationType.POPUP_FOR) - if not relation: + targets = AXUtilitiesRelation.get_is_popup_for(obj) + if not targets: return None - target = relation.get_target(0) + target = targets[0] if AXUtilities.is_autocomplete(target): return target diff --git a/src/cthulhu/scripts/toolkits/Chromium/speech_generator.py b/src/cthulhu/scripts/toolkits/Chromium/speech_generator.py index f5b4e97..5ee0e38 100644 --- a/src/cthulhu/scripts/toolkits/Chromium/speech_generator.py +++ b/src/cthulhu/scripts/toolkits/Chromium/speech_generator.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom speech generator for Chromium.""" diff --git a/src/cthulhu/scripts/toolkits/GAIL/__init__.py b/src/cthulhu/scripts/toolkits/GAIL/__init__.py index 91372dc..b006f6c 100644 --- a/src/cthulhu/scripts/toolkits/GAIL/__init__.py +++ b/src/cthulhu/scripts/toolkits/GAIL/__init__.py @@ -20,7 +20,7 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from .script import Script diff --git a/src/cthulhu/scripts/toolkits/GAIL/script.py b/src/cthulhu/scripts/toolkits/GAIL/script.py index 15d2226..30b9dcd 100644 --- a/src/cthulhu/scripts/toolkits/GAIL/script.py +++ b/src/cthulhu/scripts/toolkits/GAIL/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" @@ -46,7 +46,7 @@ class Script(default.Script): def getUtilities(self): return Utilities(self) - def locusOfFocusChanged(self, event, oldFocus, newFocus): + def locus_of_focus_changed(self, event, oldFocus, newFocus): """Handles changes of focus of interest to the script.""" if self.utilities.isInOpenMenuBarMenu(newFocus): @@ -55,7 +55,7 @@ class Script(default.Script): if windowChanged: cthulhu.setActiveWindow(window) - super().locusOfFocusChanged(event, oldFocus, newFocus) + super().locus_of_focus_changed(event, oldFocus, newFocus) def onActiveDescendantChanged(self, event): """Callback for object:active-descendant-changed accessibility events.""" diff --git a/src/cthulhu/scripts/toolkits/GAIL/script_utilities.py b/src/cthulhu/scripts/toolkits/GAIL/script_utilities.py index c04a533..d1ff789 100644 --- a/src/cthulhu/scripts/toolkits/GAIL/script_utilities.py +++ b/src/cthulhu/scripts/toolkits/GAIL/script_utilities.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" diff --git a/src/cthulhu/scripts/toolkits/Gecko/__init__.py b/src/cthulhu/scripts/toolkits/Gecko/__init__.py index 1cc055f..fec8f65 100644 --- a/src/cthulhu/scripts/toolkits/Gecko/__init__.py +++ b/src/cthulhu/scripts/toolkits/Gecko/__init__.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from .script import Script from .script_utilities import Utilities diff --git a/src/cthulhu/scripts/toolkits/Gecko/script.py b/src/cthulhu/scripts/toolkits/Gecko/script.py index 50325d5..c840234 100644 --- a/src/cthulhu/scripts/toolkits/Gecko/script.py +++ b/src/cthulhu/scripts/toolkits/Gecko/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" @@ -62,15 +62,15 @@ class Script(web.Script): return super().isActivatableEvent(event) - def locusOfFocusChanged(self, event, oldFocus, newFocus): + def locus_of_focus_changed(self, event, oldFocus, newFocus): """Handles changes of focus of interest to the script.""" - if super().locusOfFocusChanged(event, oldFocus, newFocus): + if super().locus_of_focus_changed(event, oldFocus, newFocus): return msg = "GECKO: Passing along event to default script" debug.printMessage(debug.LEVEL_INFO, msg, True) - default.Script.locusOfFocusChanged(self, event, oldFocus, newFocus) + default.Script.locus_of_focus_changed(self, event, oldFocus, newFocus) def onActiveChanged(self, event): """Callback for object:state-changed:active accessibility events.""" diff --git a/src/cthulhu/scripts/toolkits/Gecko/script_utilities.py b/src/cthulhu/scripts/toolkits/Gecko/script_utilities.py index 826b74b..c2c6e37 100644 --- a/src/cthulhu/scripts/toolkits/Gecko/script_utilities.py +++ b/src/cthulhu/scripts/toolkits/Gecko/script_utilities.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Commonly-required utility methods needed by -- and potentially customized by -- application and toolkit scripts. They have diff --git a/src/cthulhu/scripts/toolkits/J2SE-access-bridge/__init__.py b/src/cthulhu/scripts/toolkits/J2SE-access-bridge/__init__.py index 9a70c54..26d05ee 100644 --- a/src/cthulhu/scripts/toolkits/J2SE-access-bridge/__init__.py +++ b/src/cthulhu/scripts/toolkits/J2SE-access-bridge/__init__.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from .script import Script from .speech_generator import SpeechGenerator diff --git a/src/cthulhu/scripts/toolkits/J2SE-access-bridge/formatting.py b/src/cthulhu/scripts/toolkits/J2SE-access-bridge/formatting.py index d17a42d..5af62ff 100644 --- a/src/cthulhu/scripts/toolkits/J2SE-access-bridge/formatting.py +++ b/src/cthulhu/scripts/toolkits/J2SE-access-bridge/formatting.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom formatting for Java Swing.""" diff --git a/src/cthulhu/scripts/toolkits/J2SE-access-bridge/script.py b/src/cthulhu/scripts/toolkits/J2SE-access-bridge/script.py index 49fca20..6b21134 100644 --- a/src/cthulhu/scripts/toolkits/J2SE-access-bridge/script.py +++ b/src/cthulhu/scripts/toolkits/J2SE-access-bridge/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" diff --git a/src/cthulhu/scripts/toolkits/J2SE-access-bridge/script_utilities.py b/src/cthulhu/scripts/toolkits/J2SE-access-bridge/script_utilities.py index 0bdd0bb..1773dad 100644 --- a/src/cthulhu/scripts/toolkits/J2SE-access-bridge/script_utilities.py +++ b/src/cthulhu/scripts/toolkits/J2SE-access-bridge/script_utilities.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Commonly-required utility methods needed by -- and potentially customized by -- application and toolkit scripts. They have @@ -34,6 +34,10 @@ __date__ = "$Date$" __copyright__ = "Copyright (c) 2010 Joanmarie Diggs." __license__ = "LGPL" +import gi +gi.require_version("Atspi", "2.0") +from gi.repository import Atspi + import cthulhu.script_utilities as script_utilities from cthulhu.ax_object import AXObject from cthulhu.ax_utilities import AXUtilities @@ -81,15 +85,15 @@ class Utilities(script_utilities.Utilities): # negatives. # if AXUtilities.is_label(obj1) and AXUtilities.is_label(obj2): - try: - ext1 = obj1.queryComponent().getExtents(0) - ext2 = obj2.queryComponent().getExtents(0) - except Exception: - pass - else: - if ext1.x == ext2.x and ext1.y == ext2.y \ - and ext1.width == ext2.width and ext1.height == ext2.height: - return True + if AXObject.supports_component(obj1) and AXObject.supports_component(obj2): + try: + ext1 = Atspi.Component.get_extents(obj1, Atspi.CoordType.SCREEN) + ext2 = Atspi.Component.get_extents(obj2, Atspi.CoordType.SCREEN) + if ext1.x == ext2.x and ext1.y == ext2.y \ + and ext1.width == ext2.width and ext1.height == ext2.height: + return True + except Exception: + pass # In java applications, TRANSIENT state is missing for tree items # (fix for bug #352250) diff --git a/src/cthulhu/scripts/toolkits/J2SE-access-bridge/speech_generator.py b/src/cthulhu/scripts/toolkits/J2SE-access-bridge/speech_generator.py index f3ca517..0ed6fdc 100644 --- a/src/cthulhu/scripts/toolkits/J2SE-access-bridge/speech_generator.py +++ b/src/cthulhu/scripts/toolkits/J2SE-access-bridge/speech_generator.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" diff --git a/src/cthulhu/scripts/toolkits/Qt/__init__.py b/src/cthulhu/scripts/toolkits/Qt/__init__.py index 91372dc..b006f6c 100644 --- a/src/cthulhu/scripts/toolkits/Qt/__init__.py +++ b/src/cthulhu/scripts/toolkits/Qt/__init__.py @@ -20,7 +20,7 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from .script import Script diff --git a/src/cthulhu/scripts/toolkits/Qt/script.py b/src/cthulhu/scripts/toolkits/Qt/script.py index 490fd25..e61c975 100644 --- a/src/cthulhu/scripts/toolkits/Qt/script.py +++ b/src/cthulhu/scripts/toolkits/Qt/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" diff --git a/src/cthulhu/scripts/toolkits/Qt/script_utilities.py b/src/cthulhu/scripts/toolkits/Qt/script_utilities.py index 37c0569..adc84b1 100644 --- a/src/cthulhu/scripts/toolkits/Qt/script_utilities.py +++ b/src/cthulhu/scripts/toolkits/Qt/script_utilities.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" diff --git a/src/cthulhu/scripts/toolkits/VCL.py b/src/cthulhu/scripts/toolkits/VCL.py index ec98218..11f7e16 100644 --- a/src/cthulhu/scripts/toolkits/VCL.py +++ b/src/cthulhu/scripts/toolkits/VCL.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Custom script for VCL toolkit (OpenOffice) applications""" diff --git a/src/cthulhu/scripts/toolkits/WebKitGtk/__init__.py b/src/cthulhu/scripts/toolkits/WebKitGtk/__init__.py index 923e857..0ecdbf7 100644 --- a/src/cthulhu/scripts/toolkits/WebKitGtk/__init__.py +++ b/src/cthulhu/scripts/toolkits/WebKitGtk/__init__.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from .script import Script from .speech_generator import SpeechGenerator diff --git a/src/cthulhu/scripts/toolkits/WebKitGtk/braille_generator.py b/src/cthulhu/scripts/toolkits/WebKitGtk/braille_generator.py index d8af82c..53804cc 100644 --- a/src/cthulhu/scripts/toolkits/WebKitGtk/braille_generator.py +++ b/src/cthulhu/scripts/toolkits/WebKitGtk/braille_generator.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" diff --git a/src/cthulhu/scripts/toolkits/WebKitGtk/script.py b/src/cthulhu/scripts/toolkits/WebKitGtk/script.py index 7e017e5..748d8b6 100644 --- a/src/cthulhu/scripts/toolkits/WebKitGtk/script.py +++ b/src/cthulhu/scripts/toolkits/WebKitGtk/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" @@ -48,6 +48,8 @@ import cthulhu.cthulhu_state as cthulhu_state import cthulhu.speech as speech import cthulhu.structural_navigation as structural_navigation from cthulhu.ax_object import AXObject +from cthulhu.ax_hypertext import AXHypertext +from cthulhu.ax_text import AXText from cthulhu.ax_utilities import AXUtilities from .braille_generator import BrailleGenerator @@ -288,7 +290,7 @@ class Script(default.Script): return boundary = Atspi.TextBoundaryType.CHAR - objects = self.utilities.getObjectsFromEOCs(obj, boundary=boundary) + objects = self.utilities.get_objectsFromEOCs(obj, boundary=boundary) for (obj, start, end, string) in objects: if string: self.speakCharacter(string) @@ -309,7 +311,7 @@ class Script(default.Script): return boundary = Atspi.TextBoundaryType.WORD_START - objects = self.utilities.getObjectsFromEOCs(obj, boundary=boundary) + objects = self.utilities.get_objectsFromEOCs(obj, boundary=boundary) for (obj, start, end, string) in objects: self.sayPhrase(obj, start, end) @@ -327,7 +329,7 @@ class Script(default.Script): return boundary = Atspi.TextBoundaryType.LINE_START - objects = self.utilities.getObjectsFromEOCs(obj, boundary=boundary) + objects = self.utilities.get_objectsFromEOCs(obj, boundary=boundary) for (obj, start, end, string) in objects: self.sayPhrase(obj, start, end) @@ -480,9 +482,22 @@ class Script(default.Script): def getTextSegments(self, obj, boundary, offset=0): segments = [] - text = obj.queryText() - length = text.characterCount - string, start, end = text.getTextAtOffset(offset, boundary) + if not AXObject.supports_text(obj): + return segments + + length = AXText.get_character_count(obj) + if boundary == Atspi.TextBoundaryType.CHAR: + string, start, end = AXText.get_character_at_offset(obj, offset) + elif boundary == Atspi.TextBoundaryType.WORD_START: + string, start, end = AXText.get_word_at_offset(obj, offset) + elif boundary == Atspi.TextBoundaryType.LINE_START: + string, start, end = AXText.get_line_at_offset(obj, offset) + elif boundary == Atspi.TextBoundaryType.SENTENCE_START: + string, start, end = AXText.get_sentence_at_offset(obj, offset) + elif boundary == Atspi.TextBoundaryType.PARAGRAPH_START: + string, start, end = AXText.get_paragraph_at_offset(obj, offset) + else: + string, start, end = "", 0, 0 while string and offset < length: string = self.utilities.adjustForRepeats(string) voice = self.speechGenerator.getVoiceForString(obj, string) @@ -496,7 +511,18 @@ class Script(default.Script): segments.append([string, start, end, voice]) offset = end + 1 - string, start, end = text.getTextAtOffset(offset, boundary) + if boundary == Atspi.TextBoundaryType.CHAR: + string, start, end = AXText.get_character_at_offset(obj, offset) + elif boundary == Atspi.TextBoundaryType.WORD_START: + string, start, end = AXText.get_word_at_offset(obj, offset) + elif boundary == Atspi.TextBoundaryType.LINE_START: + string, start, end = AXText.get_line_at_offset(obj, offset) + elif boundary == Atspi.TextBoundaryType.SENTENCE_START: + string, start, end = AXText.get_sentence_at_offset(obj, offset) + elif boundary == Atspi.TextBoundaryType.PARAGRAPH_START: + string, start, end = AXText.get_paragraph_at_offset(obj, offset) + else: + string, start, end = "", 0, 0 return segments def textLines(self, obj, offset=None): @@ -539,7 +565,7 @@ class Script(default.Script): systemVoice = voices.get(settings.SYSTEM_VOICE) self._inSayAll = True - offset = textObjs[0].queryText().caretOffset + offset = AXText.get_caret_offset(textObjs[0]) for textObj in textObjs: textSegments = self.getTextSegments(textObj, boundary, offset) roleName = self.speechGenerator.getRoleName(textObj) @@ -567,8 +593,6 @@ class Script(default.Script): cthulhu.setLocusOfFocus(None, obj, notifyScript=False) offset = context.currentOffset - text = obj.queryText() - if progressType == speechserver.SayAllContext.INTERRUPTED: self._sayAllIsInterrupted = True if isinstance(cthulhu_state.lastInputEvent, input_event.KeyboardEvent): @@ -581,7 +605,8 @@ class Script(default.Script): self._inSayAll = False self._sayAllContexts = [] if not self._lastCommandWasStructNav: - text.setCaretOffset(offset) + if AXObject.supports_text(obj): + AXText.set_caret_offset(obj, offset) cthulhu.emitRegionChanged(obj, offset) return @@ -589,18 +614,16 @@ class Script(default.Script): # just done with the current object. If we're still in SayAll, we do # not want to set the caret (and hence set focus) in a link we just # passed by. - try: - hypertext = obj.queryHypertext() - except NotImplementedError: - pass - else: - linkCount = hypertext.getNLinks() - links = [hypertext.getLink(x) for x in range(linkCount)] - if [link for link in links if link.startIndex <= offset <= link.endIndex]: + if AXObject.supports_hypertext(obj): + links = AXHypertext.get_all_links(obj) + if [link for link in links + if AXHypertext.get_link_start_offset(link) + <= offset <= AXHypertext.get_link_end_offset(link)]: return cthulhu.emitRegionChanged(obj, offset, mode=cthulhu.SAY_ALL) - text.setCaretOffset(offset) + if AXObject.supports_text(obj): + AXText.set_caret_offset(obj, offset) def getTextLineAtCaret(self, obj, offset=None, startOffset=None, endOffset=None): """To-be-removed. Returns the string, caretOffset, startOffset.""" @@ -612,12 +635,8 @@ class Script(default.Script): return textLine textLine[0] = self.utilities.displayedText(obj) - try: - text = obj.queryText() - except Exception: - pass - else: - textLine[1] = min(textLine[1], text.characterCount) + if AXObject.supports_text(obj): + textLine[1] = min(textLine[1], AXText.get_character_count(obj)) return textLine diff --git a/src/cthulhu/scripts/toolkits/WebKitGtk/script_utilities.py b/src/cthulhu/scripts/toolkits/WebKitGtk/script_utilities.py index 4020c1f..7efa861 100644 --- a/src/cthulhu/scripts/toolkits/WebKitGtk/script_utilities.py +++ b/src/cthulhu/scripts/toolkits/WebKitGtk/script_utilities.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" @@ -40,6 +40,8 @@ import cthulhu.keybindings as keybindings import cthulhu.cthulhu as cthulhu import cthulhu.cthulhu_state as cthulhu_state from cthulhu.ax_object import AXObject +from cthulhu.ax_hypertext import AXHypertext +from cthulhu.ax_text import AXText from cthulhu.ax_utilities import AXUtilities ############################################################################# @@ -120,13 +122,13 @@ class Utilities(script_utilities.Utilities): return text def getLineContentsAtOffset(self, obj, offset, layoutMode=True, useCache=True): - return self.getObjectsFromEOCs( + return self.get_objectsFromEOCs( obj, offset, Atspi.TextBoundaryType.LINE_START) - def getObjectContentsAtOffset(self, obj, offset=0, useCache=True): - return self.getObjectsFromEOCs(obj, offset) + def get_objectContentsAtOffset(self, obj, offset=0, useCache=True): + return self.get_objectsFromEOCs(obj, offset) - def getObjectsFromEOCs(self, obj, offset=None, boundary=None): + def get_objectsFromEOCs(self, obj, offset=None, boundary=None): """Breaks the string containing a mixture of text and embedded object characters into a list of (obj, startOffset, endOffset, string) tuples. @@ -138,37 +140,49 @@ class Utilities(script_utilities.Utilities): Returns a list of (obj, startOffset, endOffset, string) tuples. """ - try: - text = obj.queryText() - htext = obj.queryHypertext() - except (AttributeError, NotImplementedError): + if not AXObject.supports_text(obj): return [(obj, 0, 1, '')] - string = text.getText(0, -1) + if not AXObject.supports_hypertext(obj): + return [(obj, 0, 1, '')] + + string = AXText.get_all_text(obj) if not string: return [(obj, 0, 1, '')] if offset is None: - offset = text.caretOffset + offset = AXText.get_caret_offset(obj) if boundary is None: start = 0 - end = text.characterCount + end = AXText.get_character_count(obj) else: if boundary == Atspi.TextBoundaryType.CHAR: key, mods = self.lastKeyAndModifiers() if (mods & keybindings.SHIFT_MODIFIER_MASK) and key == 'Right': offset -= 1 - segment, start, end = text.getTextAtOffset(offset, boundary) + if boundary == Atspi.TextBoundaryType.CHAR: + segment, start, end = AXText.get_character_at_offset(obj, offset) + elif boundary == Atspi.TextBoundaryType.WORD_START: + segment, start, end = AXText.get_word_at_offset(obj, offset) + elif boundary == Atspi.TextBoundaryType.LINE_START: + segment, start, end = AXText.get_line_at_offset(obj, offset) + elif boundary == Atspi.TextBoundaryType.SENTENCE_START: + segment, start, end = AXText.get_sentence_at_offset(obj, offset) + elif boundary == Atspi.TextBoundaryType.PARAGRAPH_START: + segment, start, end = AXText.get_paragraph_at_offset(obj, offset) + else: + segment, start, end = "", 0, 0 pattern = re.compile(self.EMBEDDED_OBJECT_CHARACTER) offsets = [m.start(0) for m in re.finditer(pattern, string)] offsets = [x for x in offsets if start <= x < end] objects = [] - try: - objs = [obj[htext.getLinkIndex(offset)] for offset in offsets] - except Exception: - objs = [] + objs = [] + for offset in offsets: + if child := AXHypertext.get_child_at_offset(obj, offset): + objs.append(child) + ranges = [self.getHyperlinkRange(x) for x in objs] for i, (first, last) in enumerate(ranges): objects.append((obj, start, first, string[start:first])) @@ -188,7 +202,7 @@ class Utilities(script_utilities.Utilities): if AXUtilities.is_link(obj): obj = AXObject.get_parent(obj) - prevObj = AXObject.get_previous_object(obj) + prevObj = AXUtilities.get_previous_object(obj) if AXUtilities.is_list(prevObj) and AXObject.get_child_count(prevObj): child = AXObject.get_child(prevObj, -1) if self.isTextListItem(child): @@ -205,7 +219,7 @@ class Utilities(script_utilities.Utilities): if AXUtilities.is_link(obj): obj = AXObject.get_parent(obj) - nextObj = AXObject.get_next_object(obj) + nextObj = AXUtilities.get_next_object(obj) if AXUtilities.is_list(nextObj) and AXObject.get_child_count(nextObj): child = AXObject.get_child(nextObj, 0) if self.isTextListItem(child): @@ -273,9 +287,9 @@ class Utilities(script_utilities.Utilities): return None, -1 index = -1 - text = child.queryText() - for i in range(text.characterCount): - if text.setCaretOffset(i): + char_count = AXText.get_character_count(child) + for i in range(char_count): + if AXText.set_caret_offset(child, i): index = i break diff --git a/src/cthulhu/scripts/toolkits/WebKitGtk/speech_generator.py b/src/cthulhu/scripts/toolkits/WebKitGtk/speech_generator.py index 6f47e37..3ffb46b 100644 --- a/src/cthulhu/scripts/toolkits/WebKitGtk/speech_generator.py +++ b/src/cthulhu/scripts/toolkits/WebKitGtk/speech_generator.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" diff --git a/src/cthulhu/scripts/toolkits/__init__.py b/src/cthulhu/scripts/toolkits/__init__.py index 182fd90..0b6f488 100644 --- a/src/cthulhu/scripts/toolkits/__init__.py +++ b/src/cthulhu/scripts/toolkits/__init__.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __all__ = ['clutter', 'Chromium', diff --git a/src/cthulhu/scripts/toolkits/clutter/__init__.py b/src/cthulhu/scripts/toolkits/clutter/__init__.py index 91372dc..b006f6c 100644 --- a/src/cthulhu/scripts/toolkits/clutter/__init__.py +++ b/src/cthulhu/scripts/toolkits/clutter/__init__.py @@ -20,7 +20,7 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from .script import Script diff --git a/src/cthulhu/scripts/toolkits/clutter/script.py b/src/cthulhu/scripts/toolkits/clutter/script.py index 2393eec..20d6331 100644 --- a/src/cthulhu/scripts/toolkits/clutter/script.py +++ b/src/cthulhu/scripts/toolkits/clutter/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" diff --git a/src/cthulhu/scripts/toolkits/clutter/script_utilities.py b/src/cthulhu/scripts/toolkits/clutter/script_utilities.py index f2807b1..cdf87d5 100644 --- a/src/cthulhu/scripts/toolkits/clutter/script_utilities.py +++ b/src/cthulhu/scripts/toolkits/clutter/script_utilities.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" diff --git a/src/cthulhu/scripts/toolkits/gtk/__init__.py b/src/cthulhu/scripts/toolkits/gtk/__init__.py index 1cc055f..fec8f65 100644 --- a/src/cthulhu/scripts/toolkits/gtk/__init__.py +++ b/src/cthulhu/scripts/toolkits/gtk/__init__.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from .script import Script from .script_utilities import Utilities diff --git a/src/cthulhu/scripts/toolkits/gtk/script.py b/src/cthulhu/scripts/toolkits/gtk/script.py index 26f25ec..8234918 100644 --- a/src/cthulhu/scripts/toolkits/gtk/script.py +++ b/src/cthulhu/scripts/toolkits/gtk/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" @@ -52,7 +52,7 @@ class Script(default.Script): self.utilities.clearCachedObjects() super().deactivate() - def locusOfFocusChanged(self, event, oldFocus, newFocus): + def locus_of_focus_changed(self, event, oldFocus, newFocus): """Handles changes of focus of interest to the script.""" if self.utilities.isToggleDescendantOfComboBox(newFocus): @@ -64,7 +64,7 @@ class Script(default.Script): if windowChanged: cthulhu.setActiveWindow(window) - super().locusOfFocusChanged(event, oldFocus, newFocus) + super().locus_of_focus_changed(event, oldFocus, newFocus) def onActiveDescendantChanged(self, event): """Callback for object:active-descendant-changed accessibility events.""" diff --git a/src/cthulhu/scripts/toolkits/gtk/script_utilities.py b/src/cthulhu/scripts/toolkits/gtk/script_utilities.py index 4ffb7d4..449c22f 100644 --- a/src/cthulhu/scripts/toolkits/gtk/script_utilities.py +++ b/src/cthulhu/scripts/toolkits/gtk/script_utilities.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" @@ -38,7 +38,9 @@ import cthulhu.debug as debug import cthulhu.script_utilities as script_utilities import cthulhu.cthulhu_state as cthulhu_state from cthulhu.ax_object import AXObject +from cthulhu.ax_text import AXText from cthulhu.ax_utilities import AXUtilities +from cthulhu.ax_utilities_relation import AXUtilitiesRelation class Utilities(script_utilities.Utilities): @@ -148,7 +150,7 @@ class Utilities(script_utilities.Utilities): and AXObject.find_ancestor(obj, AXUtilities.is_window) is not None def isPopOver(self, obj): - return AXObject.has_relation(obj, Atspi.RelationType.POPUP_FOR) + return bool(AXUtilitiesRelation.get_is_popup_for(obj)) def isSameObject(self, obj1, obj2, comparePaths=False, ignoreNames=False, ignoreDescriptions=True): @@ -210,8 +212,12 @@ class Utilities(script_utilities.Utilities): if not text: return x, y - objBox = obj.queryComponent().getExtents(coordType) - stringBox = text.getRangeExtents(0, text.characterCount, coordType) + if not AXObject.supports_component(obj): + return x, y + + objBox = Atspi.Component.get_extents(obj, coordType) + stringBox = Atspi.Text.get_range_extents( + obj, 0, AXText.get_character_count(obj), coordType) if self.intersection(objBox, stringBox) != (0, 0, 0, 0): return x, y @@ -224,7 +230,7 @@ class Utilities(script_utilities.Utilities): # Window Coordinates should be relative to the window; not the widget. # But broken interface is broken, and this appears to be what is being # exposed. And we need this information to get the widget's x and y. - charExtents = text.getCharacterExtents(0, Atspi.CoordType.WINDOW) + charExtents = Atspi.Text.get_character_extents(obj, 0, Atspi.CoordType.WINDOW) if 0 < charExtents[0] < charExtents[2]: boxX -= charExtents[0] if 0 < charExtents[1] < charExtents[3]: diff --git a/src/cthulhu/scripts/web/__init__.py b/src/cthulhu/scripts/web/__init__.py index be2a0f6..2fc8955 100644 --- a/src/cthulhu/scripts/web/__init__.py +++ b/src/cthulhu/scripts/web/__init__.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from .script import Script from .speech_generator import SpeechGenerator diff --git a/src/cthulhu/scripts/web/bookmarks.py b/src/cthulhu/scripts/web/bookmarks.py index 5610f75..9fb790f 100644 --- a/src/cthulhu/scripts/web/bookmarks.py +++ b/src/cthulhu/scripts/web/bookmarks.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" @@ -64,7 +64,7 @@ class Bookmarks(bookmarks.Bookmarks): return self._script.utilities.setCaretPosition(obj, offset) - contents = self._script.utilities.getObjectContentsAtOffset(obj, offset) + contents = self._script.utilities.get_objectContentsAtOffset(obj, offset) self._script.speakContents(contents) self._script.displayContents(contents) self._currentbookmarkindex[index[1]] = index[0] diff --git a/src/cthulhu/scripts/web/braille_generator.py b/src/cthulhu/scripts/web/braille_generator.py index deec6b1..af91208 100644 --- a/src/cthulhu/scripts/web/braille_generator.py +++ b/src/cthulhu/scripts/web/braille_generator.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" diff --git a/src/cthulhu/scripts/web/script.py b/src/cthulhu/scripts/web/script.py index 4cf2aad..d526e98 100644 --- a/src/cthulhu/scripts/web/script.py +++ b/src/cthulhu/scripts/web/script.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" @@ -32,6 +32,10 @@ __copyright__ = "Copyright (c) 2005-2009 Sun Microsystems Inc." \ __license__ = "LGPL" import time + +import gi +gi.require_version("Atspi", "2.0") +from gi.repository import Atspi from gi.repository import Gtk from cthulhu import caret_navigation @@ -40,6 +44,7 @@ from cthulhu import keybindings from cthulhu import debug from cthulhu import guilabels from cthulhu import input_event +from cthulhu import input_event_manager from cthulhu import liveregions from cthulhu import messages from cthulhu import cthulhu @@ -49,9 +54,11 @@ from cthulhu import settings_manager from cthulhu import speech from cthulhu import speechserver from cthulhu import structural_navigation +from cthulhu import sound_theme_manager from cthulhu.acss import ACSS from cthulhu.scripts import default from cthulhu.ax_object import AXObject +from cthulhu.ax_text import AXText from cthulhu.ax_utilities import AXUtilities from .bookmarks import Bookmarks @@ -607,7 +614,7 @@ class Script(default.Script): self._lastCommandWasMouseButton = False return consumes - if not keyboardEvent.isModifierKey(): + if not keyboardEvent.is_modifier_key(): self._lastCommandWasCaretNav = False self._lastCommandWasStructNav = False self._lastCommandWasMouseButton = False @@ -751,7 +758,8 @@ class Script(default.Script): """Updates the context and presents the find results if appropriate.""" text = self.utilities.queryNonEmptyText(obj) - if not (text and text.getNSelections() > 0): + selections = AXText.get_selected_ranges(obj) if text else [] + if not selections: return document = self.utilities.getDocumentForObject(obj) @@ -759,7 +767,7 @@ class Script(default.Script): return context = self.utilities.getCaretContext(documentFrame=document) - start, end = text.getSelection(0) + start, end = selections[0] offset = max(offset, start) self.utilities.setCaretContext(obj, offset, documentFrame=document) if end - start < _settingsManager.getSetting('findResultsMinimumLength'): @@ -968,7 +976,7 @@ class Script(default.Script): contents = None if self.utilities.treatAsEndOfLine(obj, offset) and AXObject.supports_text(obj): - char = obj.queryText().getText(offset, offset + 1) + char = AXText.get_substring(obj, offset, offset + 1) if char == self.EMBEDDED_OBJECT_CHARACTER: char = "" contents = [[obj, offset, offset + 1, char]] @@ -1000,8 +1008,7 @@ class Script(default.Script): document = self.utilities.getTopLevelDocumentForObject(obj) obj, offset = self.utilities.getCaretContext(documentFrame=document) - keyString, mods = self.utilities.lastKeyAndModifiers() - if keyString == "Right": + if input_event_manager.get_manager().last_event_was_right(): offset -= 1 wordContents = self.utilities.getWordContentsAtOffset(obj, offset, useCache=True) @@ -1056,7 +1063,7 @@ class Script(default.Script): # danger of presented irrelevant context. useCache = False offset = args.get("offset", 0) - contents = self.utilities.getObjectContentsAtOffset(obj, offset, useCache) + contents = self.utilities.get_objectContentsAtOffset(obj, offset, useCache) self.displayContents(contents) self.speakContents(contents, **args) @@ -1064,7 +1071,7 @@ class Script(default.Script): """Try to reposition the cursor without having to do a full update.""" text = self.utilities.queryNonEmptyText(obj) - if text and self.EMBEDDED_OBJECT_CHARACTER in text.getText(0, -1): + if text and self.EMBEDDED_OBJECT_CHARACTER in AXText.get_all_text(obj): self.updateBraille(obj) return @@ -1108,7 +1115,7 @@ class Script(default.Script): if offset > 0 and isContentEditable: text = self.utilities.queryNonEmptyText(obj) if text: - offset = min(offset, text.characterCount) + offset = min(offset, AXText.get_character_count(obj)) contents = self.utilities.getLineContentsAtOffset(obj, offset) self.displayContents(contents, documentFrame=document) @@ -1272,13 +1279,10 @@ class Script(default.Script): text = self.utilities.queryNonEmptyText(obj) if offset is None: - try: - offset = max(0, text.caretOffset) - except Exception: - offset = 0 + offset = max(0, AXText.get_caret_offset(obj)) if text and startOffset is not None and endOffset is not None: - return text.getText(startOffset, endOffset), offset, startOffset + return AXText.get_substring(obj, startOffset, endOffset), offset, startOffset contextObj, contextOffset = self.utilities.getCaretContext(documentFrame=None) if contextObj == obj: @@ -1318,10 +1322,10 @@ class Script(default.Script): if not obj: return - if AXUtilities.is_focusable(obj): - obj.queryComponent().grabFocus() + if AXUtilities.is_focusable(obj) and AXObject.supports_component(obj): + Atspi.Component.grab_focus(obj) - contents = self.utilities.getObjectContentsAtOffset(obj, offset) + contents = self.utilities.get_objectContentsAtOffset(obj, offset) self.utilities.setCaretPosition(obj, offset) self.speakContents(contents) self.updateBraille(obj) @@ -1332,7 +1336,7 @@ class Script(default.Script): obj, offset = self._preMouseOverContext self.utilities.setCaretPosition(obj, offset) - self.speakContents(self.utilities.getObjectContentsAtOffset(obj, offset)) + self.speakContents(self.utilities.get_objectContentsAtOffset(obj, offset)) self.updateBraille(obj) self._inMouseOverObject = False self._lastMouseOverObject = None @@ -1373,6 +1377,7 @@ class Script(default.Script): self.utilities.setCaretContext(AXObject.get_parent(parent), -1) if not self._loadingDocumentContent: self.presentMessage(messages.MODE_BROWSE) + sound_theme_manager.getManager().playBrowseModeSound() else: if not self.utilities.grabFocusWhenSettingCaret(obj) \ and (self._lastCommandWasCaretNav \ @@ -1381,6 +1386,7 @@ class Script(default.Script): self.utilities.grabFocus(obj) self.presentMessage(messages.MODE_FOCUS) + sound_theme_manager.getManager().playFocusModeSound() self._inFocusMode = not self._inFocusMode self._focusModeIsSticky = False self._browseModeIsSticky = False @@ -1518,7 +1524,7 @@ class Script(default.Script): """Activates clickable element at current focus via Return key.""" return self._tryClickableActivation(inputEvent) - def locusOfFocusChanged(self, event, oldFocus, newFocus): + def locus_of_focus_changed(self, event, oldFocus, newFocus): """Handles changes of focus of interest to the script.""" if newFocus and self.utilities.isZombie(newFocus): @@ -1564,8 +1570,10 @@ class Script(default.Script): newFocus, offset = self.utilities.findFirstCaretContext(newFocus, 0) text = self.utilities.queryNonEmptyText(newFocus) - if text and (0 <= text.caretOffset <= text.characterCount): - caretOffset = text.caretOffset + if text: + textOffset = AXText.get_caret_offset(newFocus) + if 0 <= textOffset <= AXText.get_character_count(newFocus): + caretOffset = textOffset self.utilities.setCaretContext(newFocus, caretOffset, document) self.updateBraille(newFocus, documentFrame=document) @@ -1602,7 +1610,7 @@ class Script(default.Script): elif AXUtilities.is_heading(newFocus): tokens = ["WEB: New focus", newFocus, "is heading. Generating object."] debug.printTokens(debug.LEVEL_INFO, tokens, True) - contents = self.utilities.getObjectContentsAtOffset(newFocus, 0) + contents = self.utilities.get_objectContentsAtOffset(newFocus, 0) elif self.utilities.caretMovedToSamePageFragment(event, oldFocus): tokens = ["WEB: Source", event.source, "is same page fragment. Generating line."] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -1642,11 +1650,6 @@ class Script(default.Script): self._saveFocusedObjectInfo(newFocus) - if self.utilities.inTopLevelWebApp(newFocus) and not self._browseModeIsSticky: - announce = not self.utilities.inDocumentContent(oldFocus) - self.enableStickyFocusMode(None, announce) - return True - if not self._focusModeIsSticky \ and not self._browseModeIsSticky \ and self.useFocusMode(newFocus, oldFocus) != self._inFocusMode: @@ -1771,14 +1774,6 @@ class Script(default.Script): self.presentMessage(summary) obj, offset = self.utilities.getCaretContext() - if not AXUtilities.is_busy(event.source) \ - and self.utilities.isTopLevelWebApp(event.source): - tokens = ["WEB: Setting locusOfFocus to", obj, "with sticky focus mode"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - cthulhu.setLocusOfFocus(event, obj) - self.enableStickyFocusMode(None, True) - return True - if self.useFocusMode(obj) != self._inFocusMode: self.togglePresentationMode(None) @@ -2257,8 +2252,14 @@ class Script(default.Script): # We should get proper state-changed events for these. if self.utilities.inDocumentContent(event.source): - msg = "WEB: Ignoring because object:state-changed-focused expected." + if self._getQueuedEvent("object:state-changed:focused", True): + msg = "WEB: Ignoring because object:state-changed-focused expected." + debug.printMessage(debug.LEVEL_INFO, msg, True) + return True + + msg = "WEB: Handling focus event in document content; focused event missing." debug.printMessage(debug.LEVEL_INFO, msg, True) + cthulhu.setLocusOfFocus(event, event.source) return True return False @@ -2448,6 +2449,10 @@ class Script(default.Script): msg = "WEB: Event believed to be browser UI page switch" debug.printMessage(debug.LEVEL_INFO, msg, True) if event.detail1: + # Work around stale cache when switching tabs. + AXObject.clear_cache(event.source, False, "Work around Chromium page switch.") + AXUtilities.clear_all_cache_now(reason=msg) + self.utilities.clearCaretContext() self.presentObject(event.source, priorObj=cthulhu_state.locusOfFocus, interrupt=True) return True @@ -2790,8 +2795,8 @@ class Script(default.Script): debug.printMessage(debug.LEVEL_INFO, msg, True) return False - offset = text.caretOffset - char = text.getText(offset, offset+1) + offset = AXText.get_caret_offset(event.source) + char = AXText.get_substring(event.source, offset, offset + 1) if char == self.EMBEDDED_OBJECT_CHARACTER \ and not self.utilities.lastInputEventWasCaretNavWithSelection() \ and not self.utilities.lastInputEventWasCommand(): diff --git a/src/cthulhu/scripts/web/script_utilities.py b/src/cthulhu/scripts/web/script_utilities.py index e6ab008..d1993f6 100644 --- a/src/cthulhu/scripts/web/script_utilities.py +++ b/src/cthulhu/scripts/web/script_utilities.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" @@ -41,6 +41,7 @@ import urllib from cthulhu import debug from cthulhu import input_event +from cthulhu import input_event_manager from cthulhu import messages from cthulhu import cthulhu from cthulhu import cthulhu_state @@ -48,10 +49,16 @@ from cthulhu import script_utilities from cthulhu import script_manager from cthulhu import settings_manager from cthulhu.ax_collection import AXCollection +from cthulhu.ax_component import AXComponent +from cthulhu.ax_document import AXDocument +from cthulhu.ax_hypertext import AXHypertext from cthulhu.ax_object import AXObject +from cthulhu.ax_text import AXText from cthulhu.ax_utilities import AXUtilities +from cthulhu.ax_utilities_relation import AXUtilitiesRelation +from cthulhu import speech_and_verbosity_manager -_scriptManager = script_manager.getManager() +_scriptManager = script_manager.get_manager() _settingsManager = settings_manager.getManager() @@ -265,7 +272,7 @@ class Utilities(script_utilities.Utilities): return rv def _getDocumentsEmbeddedBy(self, frame): - return AXObject.get_relation_targets(frame, Atspi.RelationType.EMBEDS, self.isDocument) + return [x for x in AXUtilitiesRelation.get_embeds(frame) if self.isDocument(x)] def sanityCheckActiveWindow(self): app = self._script.app @@ -277,7 +284,7 @@ class Utilities(script_utilities.Utilities): # TODO - JD: Is this exception handling still needed? try: - script = _scriptManager.getScript(app, cthulhu_state.activeWindow) + script = _scriptManager.get_script(app, cthulhu_state.activeWindow) tokens = ["WEB: Script for active Window is", script] debug.printTokens(debug.LEVEL_INFO, tokens, True) except Exception: @@ -300,10 +307,69 @@ class Utilities(script_utilities.Utilities): return True def activeDocument(self, window=None): - documents = self._getDocumentsEmbeddedBy(window or cthulhu_state.activeWindow) + window = window or cthulhu_state.activeWindow + documents = self._getDocumentsEmbeddedBy(window) documents = list(filter(AXUtilities.is_showing, documents)) + + def documentHasUri(document): + return bool(AXDocument.get_uri(document)) + + def findDocumentWithUri(searchRoot): + if not searchRoot: + return None + return AXObject.find_descendant( + searchRoot, + lambda obj: AXUtilities.is_document_web(obj) + and AXUtilities.is_showing(obj) + and AXDocument.get_uri(obj) + ) + if len(documents) == 1: - return documents[0] + document = documents[0] + if documentHasUri(document): + return document + + fallback = findDocumentWithUri(window) + if fallback and fallback != document: + tokens = ["WEB: Using fallback active document with URI:", fallback] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + return fallback + + return document + + # If multiple documents are showing (e.g., multi-tab browser), use the + # locus of focus to determine which document is currently active. + if documents: + focusDoc = self.getTopLevelDocumentForObject(cthulhu_state.locusOfFocus) + if focusDoc in documents and documentHasUri(focusDoc): + tokens = ["WEB: Multiple showing documents, using focus-based document:", focusDoc] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + return focusDoc + + uriDoc = next((doc for doc in documents if documentHasUri(doc)), None) + if uriDoc and uriDoc != focusDoc: + tokens = ["WEB: Multiple showing documents, using URI-based document:", uriDoc] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + return uriDoc + + if focusDoc in documents: + tokens = ["WEB: Multiple showing documents, using focus-based document:", focusDoc] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + return focusDoc + + fallback = findDocumentWithUri(window) + if fallback: + tokens = ["WEB: Multiple showing documents, using fallback document:", fallback] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + return fallback + + if window: + fallback = findDocumentWithUri(window) + if fallback: + tokens = ["WEB: Using fallback document:", fallback] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + return fallback + return None def documentFrame(self, obj=None): @@ -317,16 +383,9 @@ class Utilities(script_utilities.Utilities): def documentFrameURI(self, documentFrame=None): documentFrame = documentFrame or self.documentFrame() if documentFrame: - try: - document = documentFrame.queryDocument() - except NotImplementedError: - tokens = ["WEB:", documentFrame, "does not implement document interface"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - except Exception: - tokens = ["ERROR: Exception querying document interface of", documentFrame] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - else: - return document.getAttributeValue('DocURL') or document.getAttributeValue('URI') + uri = AXDocument.get_uri(documentFrame) + if uri: + return uri return "" @@ -339,20 +398,10 @@ class Utilities(script_utilities.Utilities): if rv is not None: return rv - try: - document = documentFrame.queryDocument() - attrs = dict([attr.split(":", 1) for attr in document.getAttributes()]) - except NotImplementedError: - tokens = ["WEB:", documentFrame, "does not implement document interface"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - except Exception: - tokens = ["ERROR: Exception getting document attributes of", documentFrame] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - else: - rv = attrs.get("MimeType") - tokens = ["WEB: MimeType of", documentFrame, "is '", rv, "'"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - self._mimeType[hash(documentFrame)] = rv + rv = AXDocument.get_mime_type(documentFrame) + tokens = ["WEB: MimeType of", documentFrame, "is '", rv, "'"] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + self._mimeType[hash(documentFrame)] = rv return rv @@ -370,11 +419,13 @@ class Utilities(script_utilities.Utilities): return AXUtilities.is_focusable(obj) def grabFocus(self, obj): - try: - obj.queryComponent().grabFocus() - except NotImplementedError: - tokens = ["WEB:", obj, "does not implement the component interface"] + if not AXObject.supports_component(obj): + tokens = ["WEB:", obj, "does not support the component interface"] debug.printTokens(debug.LEVEL_INFO, tokens, True) + return + + try: + Atspi.Component.grab_focus(obj) except Exception: tokens = ["WEB: Exception grabbing focus on", obj] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -397,13 +448,11 @@ class Utilities(script_utilities.Utilities): # Don't use queryNonEmptyText() because we need to try to force-update focus. if AXObject.supports_text(obj): - try: - obj.queryText().setCaretOffset(offset) - except Exception as error: - tokens = ["WEB: Exception setting caret to", offset, "in", obj, ":", error] + if AXText.set_caret_offset(obj, offset): + tokens = ["WEB: Caret set to", offset, "in", obj] debug.printTokens(debug.LEVEL_INFO, tokens, True) else: - tokens = ["WEB: Caret set to", offset, "in", obj] + tokens = ["WEB: Exception setting caret to", offset, "in", obj] debug.printTokens(debug.LEVEL_INFO, tokens, True) if self._script.useFocusMode(obj, oldFocus) != self._script.inFocusMode(): @@ -419,9 +468,9 @@ class Utilities(script_utilities.Utilities): if not obj: return None - relation = AXObject.get_relation(obj, Atspi.RelationType.FLOWS_TO) - if relation: - return relation.get_target(0) + flowsTo = AXUtilitiesRelation.get_flows_to(obj) + if flowsTo: + return flowsTo[0] if obj == documentFrame: obj, offset = self.getCaretContext(documentFrame) @@ -627,10 +676,8 @@ class Utilities(script_utilities.Utilities): nextobj, nextoffset = self.findNextCaretInOrder(obj, offset) if skipSpace: - text = self.queryNonEmptyText(nextobj) - while text and text.getText(nextoffset, nextoffset + 1) in [" ", "\xa0"]: + while nextobj and AXText.get_character_at_offset(nextobj, nextoffset)[0].isspace(): nextobj, nextoffset = self.findNextCaretInOrder(nextobj, nextoffset) - text = self.queryNonEmptyText(nextobj) return nextobj, nextoffset @@ -640,10 +687,8 @@ class Utilities(script_utilities.Utilities): prevobj, prevoffset = self.findPreviousCaretInOrder(obj, offset) if skipSpace: - text = self.queryNonEmptyText(prevobj) - while text and text.getText(prevoffset, prevoffset + 1) in [" ", "\xa0"]: + while prevobj and AXText.get_character_at_offset(prevobj, prevoffset)[0].isspace(): prevobj, prevoffset = self.findPreviousCaretInOrder(prevobj, prevoffset) - text = self.queryNonEmptyText(prevobj) return prevobj, prevoffset @@ -651,7 +696,7 @@ class Utilities(script_utilities.Utilities): offset = 0 text = self.queryNonEmptyText(root) if text: - offset = text.characterCount - 1 + offset = AXText.get_character_count(root) - 1 def _isInRoot(o): return o == root or AXObject.find_ancestor(o, lambda x: x == root) @@ -705,52 +750,25 @@ class Utilities(script_utilities.Utilities): if not obj: return [0, 0, 0, 0] - result = [0, 0, 0, 0] - try: - text = obj.queryText() - if text.characterCount and 0 <= startOffset < endOffset: - result = list(text.getRangeExtents(startOffset, endOffset, 0)) - except NotImplementedError: - pass - except Exception: - tokens = ["WEB: Exception getting range extents for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return [0, 0, 0, 0] - else: - if result[0] and result[1] and result[2] == 0 and result[3] == 0 \ - and text.getText(startOffset, endOffset).strip(): - tokens = ["WEB: Suspected bogus range extents for", - obj, "(chars:", startOffset, ",", endOffset, "):", result] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - elif text.characterCount: + if AXObject.supports_text(obj) and 0 <= startOffset < endOffset: + rect = AXText.get_range_rect(obj, startOffset, endOffset) + result = [rect.x, rect.y, rect.width, rect.height] + if not (result[0] and result[1] and result[2] == 0 and result[3] == 0 + and AXText.get_substring(obj, startOffset, endOffset).strip()): return result + tokens = ["WEB: Suspected bogus range extents for", + obj, "(chars:", startOffset, ",", endOffset, "):", result] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + parent = AXObject.get_parent(obj) if (AXUtilities.is_menu(obj) or AXUtilities.is_list_item(obj)) \ - and (AXUtilities.is_combo_box(parent) or AXUtilities.is_list_box(parent)): - try: - ext = parent.queryComponent().getExtents(0) - except NotImplementedError: - tokens = ["WEB:", parent, "does not implement the component interface"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return [0, 0, 0, 0] - except Exception: - tokens = ["WEB: Exception getting extents for", parent] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return [0, 0, 0, 0] + and (AXUtilities.is_combo_box(parent) or AXUtilities.is_list_box(parent)): + extents = AXComponent.get_rect(parent) else: - try: - ext = obj.queryComponent().getExtents(0) - except NotImplementedError: - tokens = ["WEB:", obj, "does not implement the component interface"] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return [0, 0, 0, 0] - except Exception: - tokens = ["WEB: Exception getting extents for", obj] - debug.printTokens(debug.LEVEL_INFO, tokens, True) - return [0, 0, 0, 0] + extents = AXComponent.get_rect(obj) - return [ext.x, ext.y, ext.width, ext.height] + return [extents.x, extents.y, extents.width, extents.height] def descendantAtPoint(self, root, x, y, coordType=None): if coordType is None: @@ -802,7 +820,7 @@ class Utilities(script_utilities.Utilities): text = self.queryNonEmptyText(obj) if text: - return text.getText(startOffset, endOffset) + return AXText.get_substring(obj, startOffset, endOffset) return "" @@ -870,7 +888,7 @@ class Utilities(script_utilities.Utilities): if not self.isTextBlockElement(obj): return -1 - child = self.findChildAtOffset(obj, offset) + child = AXHypertext.find_child_at_offset(obj, offset) if child and not self.isTextBlockElement(child): matches = [x for x in contents if x[0] == child] if len(matches) == 1: @@ -1112,11 +1130,11 @@ class Utilities(script_utilities.Utilities): return False - def __findRange(self, text, offset, start, end, boundary): + def __findRange(self, obj, offset, start, end, boundary): # We should not have to do any of this. Seriously. This is why # We can't have nice things. - allText = text.getText(0, -1) + allText = AXText.get_all_text(obj) if boundary == Atspi.TextBoundaryType.CHAR: try: string = allText[offset] @@ -1125,15 +1143,18 @@ class Utilities(script_utilities.Utilities): return string, offset, offset + 1 - extents = list(text.getRangeExtents(offset, offset + 1, 0)) + extents = list(Atspi.Text.get_range_extents( + obj, offset, offset + 1, Atspi.CoordType.SCREEN)) def _inThisSpan(span): return span[0] <= offset <= span[1] def _onThisLine(span): start, end = span - startExtents = list(text.getRangeExtents(start, start + 1, 0)) - endExtents = list(text.getRangeExtents(end - 1, end, 0)) + startExtents = list(Atspi.Text.get_range_extents( + obj, start, start + 1, Atspi.CoordType.SCREEN)) + endExtents = list(Atspi.Text.get_range_extents( + obj, end - 1, end, Atspi.CoordType.SCREEN)) delta = max(startExtents[3], endExtents[3]) if not self.extentsAreOnSameLine(startExtents, endExtents, delta): tokens = ["FAIL: Start", startExtents, "and end", endExtents, @@ -1144,7 +1165,7 @@ class Utilities(script_utilities.Utilities): return self.extentsAreOnSameLine(extents, startExtents) spans = [] - charCount = text.characterCount + charCount = AXText.get_character_count(obj) if boundary == Atspi.TextBoundaryType.SENTENCE_START: spans = [m.span() for m in re.finditer( r"\S*[^\.\?\!]+((? 0.3: return False AXObject.clear_cache(obj) - tokens = list(filter(lambda x: x, re.split(r"[\s\ufffc]", text.getText(0, -1)))) + tokens = list(filter(lambda x: x, re.split(r"[\s\ufffc]", AXText.get_all_text(obj)))) # Note: We cannot check for the editable-text interface, because Gecko # seems to be exposing that for non-editable things. Thanks Gecko. @@ -3235,7 +3260,7 @@ class Utilities(script_utilities.Utilities): boundary = Atspi.TextBoundaryType.LINE_START i = 0 while i < nChars: - string, start, end = text.getTextAtOffset(i, boundary) + string, start, end = AXText.get_line_at_offset(obj, i) if len(string.split()) != 1: rv = False break @@ -3256,10 +3281,7 @@ class Utilities(script_utilities.Utilities): if not text: return False - try: - nChars = text.characterCount - except Exception: - return False + nChars = AXText.get_character_count(obj) if not nChars: return False @@ -3270,7 +3292,7 @@ class Utilities(script_utilities.Utilities): # CSSified text we're trying to detect can have embedded object characters. So # if we have more than 30% EOCs, don't use this workaround. (The 30% is based on # testing with problematic text.) - eocs = re.findall(self.EMBEDDED_OBJECT_CHARACTER, text.getText(0, -1)) + eocs = re.findall(self.EMBEDDED_OBJECT_CHARACTER, AXText.get_all_text(obj)) if len(eocs)/nChars > 0.3: return False @@ -3280,13 +3302,12 @@ class Utilities(script_utilities.Utilities): # seems to be exposing that for non-editable things. Thanks Gecko. rv = not AXUtilities.is_editable(obj) if rv: - boundary = Atspi.TextBoundaryType.LINE_START for i in range(nChars): - char = text.getText(i, i + 1) + char = AXText.get_substring(obj, i, i + 1) if char.isspace() or char in ["\ufffc", "\ufffd"]: continue - string, start, end = text.getTextAtOffset(i, boundary) + string, start, end = AXText.get_line_at_offset(obj, i) if len(string.strip()) > 1: rv = False break @@ -3325,11 +3346,7 @@ class Utilities(script_utilities.Utilities): rv = False targets = self.labelTargets(obj) if targets: - try: - text = obj.queryText() - end = text.characterCount - except Exception: - end = 1 + end = AXText.get_character_count(obj) if AXObject.supports_text(obj) else 1 x, y, width, height = self.getExtents(obj, 0, end) if x < 0 or y < 0: rv = True @@ -3364,9 +3381,13 @@ class Utilities(script_utilities.Utilities): if not AXUtilities.is_link(obj) or not AXObject.supports_text(obj): return False - text = obj.queryText() - start = list(text.getRangeExtents(0, 1, 0)) - end = list(text.getRangeExtents(text.characterCount - 1, text.characterCount, 0)) + try: + char_count = AXText.get_character_count(obj) + start = list(Atspi.Text.get_range_extents(obj, 0, 1, Atspi.CoordType.SCREEN)) + end = list(Atspi.Text.get_range_extents( + obj, max(0, char_count - 1), char_count, Atspi.CoordType.SCREEN)) + except Exception: + return False if self.extentsAreOnSameLine(start, end): return False @@ -3378,7 +3399,7 @@ class Utilities(script_utilities.Utilities): return True def targetsForLabel(self, obj): - return AXObject.get_relation_targets(obj, Atspi.RelationType.LABEL_FOR) + return AXUtilitiesRelation.get_is_label_for(obj) def labelTargets(self, obj): if not (obj and self.inDocumentContent(obj)): @@ -3525,8 +3546,12 @@ class Utilities(script_utilities.Utilities): rv = AXObject.has_action(obj, "click-ancestor") if rv and not AXObject.get_name(obj) and AXObject.supports_text(obj): - string = obj.queryText().getText(0, -1) - if not string.strip(): + string = AXText.get_all_text(obj) + if not string.replace("\ufffc", ""): + tokens = ["WEB:", obj, "is not clickable: its text is just EOCs"] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + rv = False + elif not string.strip(): rv = not (AXUtilities.is_static(obj) or AXUtilities.is_link(obj)) self._isClickableElement[hash(obj)] = rv @@ -3625,9 +3650,8 @@ class Utilities(script_utilities.Utilities): if listbox is None: return None - targets = AXObject.get_relation_targets(listbox, - Atspi.RelationType.CONTROLLED_BY, - self.isEditableComboBox) + targets = [x for x in AXUtilitiesRelation.get_is_controlled_by(listbox) + if self.isEditableComboBox(x)] if len(targets) == 1: return targets[0] @@ -3768,7 +3792,7 @@ class Utilities(script_utilities.Utilities): if rv is not None: return rv - rv = AXObject.has_relation(obj, Atspi.RelationType.ERROR_FOR) + rv = bool(AXUtilitiesRelation.get_is_error_for(obj)) self._isErrorMessage[hash(obj)] = rv return rv @@ -3784,10 +3808,9 @@ class Utilities(script_utilities.Utilities): return False def _isMatch(x): - try: - string = x.queryText().getText(0, -1).strip() - except Exception: + if not AXObject.supports_text(x): return False + string = AXText.get_all_text(x).strip() if entryName != string: return False return AXUtilities.is_section(x) or AXUtilities.is_static(x) @@ -4030,7 +4053,7 @@ class Utilities(script_utilities.Utilities): if self.isCustomElement(obj) and self.hasExplicitName(obj) \ and AXUtilities.is_section(obj) \ and AXObject.supports_text(obj) \ - and not re.search(r'[^\s\ufffc]', obj.queryText().getText(0, -1)): + and not re.search(r'[^\s\ufffc]', AXText.get_all_text(obj)): for child in AXObject.iter_children(obj): if not (AXUtilities.is_image_or_canvas(child) or self.isSVG(child)): break @@ -4064,11 +4087,10 @@ class Utilities(script_utilities.Utilities): if uri and not uri.startswith('javascript'): rv = False if rv and AXObject.supports_image(obj): - image = obj.queryImage() - if image.imageDescription: + if AXObject.get_image_description(obj): rv = False elif not self.hasExplicitName(obj) and not self.isRedundantSVG(obj): - width, height = image.getImageSize() + width, height = AXObject.get_image_size(obj) if width > 25 and height > 25: rv = False if rv and AXObject.supports_text(obj): @@ -4125,8 +4147,8 @@ class Utilities(script_utilities.Utilities): elif self.hasValidName(obj) \ or AXObject.get_description(obj) or AXObject.get_child_count(obj): rv = False - elif AXObject.supports_text(obj) and obj.queryText().characterCount \ - and obj.queryText().getText(0, -1) != AXObject.get_name(obj): + elif AXObject.supports_text(obj) and AXText.get_character_count(obj) \ + and AXText.get_all_text(obj) != AXObject.get_name(obj): rv = False elif AXObject.supports_action(obj): names = AXObject.get_action_names(obj) @@ -4205,8 +4227,7 @@ class Utilities(script_utilities.Utilities): if rv is not None: return rv - relation = AXObject.get_relation(obj, Atspi.RelationType.DETAILS) - rv = relation and relation.get_n_targets() > 0 + rv = bool(AXUtilitiesRelation.get_details(obj)) self._hasDetails[hash(obj)] = rv return rv @@ -4214,7 +4235,7 @@ class Utilities(script_utilities.Utilities): if not self.hasDetails(obj): return [] - return AXObject.get_relation_targets(obj, Atspi.RelationType.DETAILS) + return AXUtilitiesRelation.get_details(obj) def isDetails(self, obj): if not (obj and self.inDocumentContent(obj)): @@ -4224,8 +4245,7 @@ class Utilities(script_utilities.Utilities): if rv is not None: return rv - relation = AXObject.get_relation(obj, Atspi.RelationType.DETAILS_FOR) - rv = relation and relation.get_n_targets() > 0 + rv = bool(AXUtilitiesRelation.get_is_details_for(obj)) self._isDetails[hash(obj)] = rv return rv @@ -4233,7 +4253,7 @@ class Utilities(script_utilities.Utilities): if not self.isDetails(obj): return [] - return AXObject.get_relation_targets(obj, Atspi.RelationType.DETAILS_FOR) + return AXUtilitiesRelation.get_is_details_for(obj) def popupType(self, obj): if not (obj and self.inDocumentContent(obj)): @@ -4344,16 +4364,14 @@ class Utilities(script_utilities.Utilities): if event.type.startswith("object:text-changed") \ or event.type.startswith("object:text-selection-changed"): - lastKey, mods = self.lastKeyAndModifiers() - if lastKey in ["Down", "Up"]: + if input_event_manager.get_manager().last_event_was_up_or_down(): return True return False def treatEventAsSpinnerValueChange(self, event): if event.type.startswith("object:text-caret-moved") and self.isSpinnerEntry(event.source): - lastKey, mods = self.lastKeyAndModifiers() - if lastKey in ["Down", "Up"]: + if input_event_manager.get_manager().last_event_was_up_or_down(): obj, offset = self.getCaretContext() return event.source == obj @@ -4365,8 +4383,7 @@ class Utilities(script_utilities.Utilities): if event.type.startswith("object:text-") \ and self.isSingleLineAutocompleteEntry(event.source): - lastKey, mods = self.lastKeyAndModifiers() - return lastKey == "Return" + return input_event_manager.get_manager().last_event_was_return() if event.type.startswith("object:text-") or event.type.endswith("accessible-name"): return AXUtilities.is_status_bar(event.source) or AXUtilities.is_label(event.source) if event.type.startswith("object:children-changed"): @@ -4395,8 +4412,7 @@ class Utilities(script_utilities.Utilities): return True if obj == event.source and isComboBoxItem(obj): - lastKey, mods = self.lastKeyAndModifiers() - if lastKey in ["Down", "Up"]: + if input_event_manager.get_manager().last_event_was_up_or_down(): return True return False @@ -4418,8 +4434,7 @@ class Utilities(script_utilities.Utilities): if AXUtilities.is_menu_related(event.source) \ and AXUtilities.is_entry(cthulhu_state.locusOfFocus) \ and AXUtilities.is_focused(cthulhu_state.locusOfFocus): - lastKey, mods = self.lastKeyAndModifiers() - if lastKey not in ["Down", "Up"]: + if not input_event_manager.get_manager().last_event_was_up_or_down(): return True return False @@ -4435,8 +4450,7 @@ class Utilities(script_utilities.Utilities): if AXUtilities.is_menu_item_of_any_kind(cthulhu_state.locusOfFocus) \ or AXUtilities.is_list_item(cthulhu_state.locusOfFocus): - lastKey, mods = self.lastKeyAndModifiers() - return lastKey in ["Down", "Up"] + return input_event_manager.get_manager().last_event_was_up_or_down() return False @@ -4530,7 +4544,7 @@ class Utilities(script_utilities.Utilities): if isinstance(cthulhu_state.lastInputEvent, input_event.KeyboardEvent): inputEvent = cthulhu_state.lastNonModifierKeyEvent - return inputEvent and inputEvent.isPrintableKey() and not inputEvent.modifiers + return inputEvent and inputEvent.is_printable_key() and not inputEvent.modifiers return False @@ -4637,7 +4651,7 @@ class Utilities(script_utilities.Utilities): return -1, -1, 0 start, end = self.getHyperlinkRange(obj) - return start, end, text.characterCount + return start, end, AXText.get_character_count(AXObject.get_parent(obj)) def getError(self, obj): if not (obj and self.inDocumentContent(obj)): @@ -4667,9 +4681,9 @@ class Utilities(script_utilities.Utilities): if not self.getError(obj): return None - relation = AXObject.get_relation(obj, Atspi.RelationType.ERROR_MESSAGE) - if relation: - return relation.get_target(0) + errorMessages = AXUtilitiesRelation.get_error_message(obj) + if errorMessages: + return errorMessages[0] return None @@ -4803,19 +4817,19 @@ class Utilities(script_utilities.Utilities): container = obj contextObj, contextOffset = None, -1 while obj: - try: - offset = obj.queryText().caretOffset - except Exception: + offset = AXText.get_caret_offset(obj) + if offset < 0: tokens = ["WEB: Exception getting caret offset of", obj] debug.printTokens(debug.LEVEL_INFO, tokens, True) obj = None + continue + + contextObj, contextOffset = obj, offset + child = AXHypertext.find_child_at_offset(obj, offset) + if child: + obj = child else: - contextObj, contextOffset = obj, offset - child = self.findChildAtOffset(obj, offset) - if child: - obj = child - else: - break + break if contextObj and not self.isHidden(contextObj): return self.findNextCaretInOrder(contextObj, max(-1, contextOffset - 1)) @@ -4832,12 +4846,13 @@ class Utilities(script_utilities.Utilities): if not self.inDocumentContent(obj): return None, -1 - try: - offset = obj.queryText().caretOffset - except NotImplementedError: + if AXObject.supports_text(obj): + offset = AXText.get_caret_offset(obj) + else: + offset = 0 + + if offset < 0: offset = 0 - except Exception: - offset = -1 return obj, offset @@ -5007,9 +5022,9 @@ class Utilities(script_utilities.Utilities): obj, offset = None, -1 notify = True - keyString, mods = self.lastKeyAndModifiers() + lastWasUp = input_event_manager.get_manager().last_event_was_up() childCount = AXObject.get_child_count(event.source) - if keyString == "Up": + if lastWasUp: if event.detail1 >= childCount: msg = "WEB: Last child removed. Getting new location from end of parent." debug.printMessage(debug.LEVEL_INFO, msg, True) @@ -5071,7 +5086,7 @@ class Utilities(script_utilities.Utilities): def findContextReplicant(self, documentFrame=None, matchRole=True, matchName=True): path, oldRole, oldName = self.getCaretContextPathRoleAndName(documentFrame) - obj = self.getObjectFromPath(path) + obj = self.get_objectFromPath(path) if obj and matchRole: if AXObject.get_role(obj) != oldRole: obj = None @@ -5149,9 +5164,14 @@ class Utilities(script_utilities.Utilities): debug.printTokens(debug.LEVEL_INFO, tokens, True) return obj, 0 - if text and offset >= text.characterCount: + if text: + char_count = AXText.get_character_count(obj) + else: + char_count = 0 + + if text and offset >= char_count: if self.isContentEditableWithEmbeddedObjects(obj) and self.lastInputEventWasCharNav(): - nextObj, nextOffset = self.nextContext(obj, text.characterCount) + nextObj, nextOffset = self.nextContext(obj, char_count) if not nextObj: tokens = ["WEB: No next object found at end of contenteditable", obj] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -5166,13 +5186,13 @@ class Utilities(script_utilities.Utilities): return nextObj, nextOffset tokens = ["WEB: First caret context at end of", obj, ", ", offset, "is", - obj, ", ", text.characterCount] + obj, ", ", char_count] debug.printTokens(debug.LEVEL_INFO, tokens, True) - return obj, text.characterCount + return obj, char_count offset = max(0, offset) if text: - allText = text.getText(0, -1) + allText = AXText.get_all_text(obj) if allText[offset] != self.EMBEDDED_OBJECT_CHARACTER or role == Atspi.Role.ENTRY: msg = "WEB: First caret context is unchanged" debug.printMessage(debug.LEVEL_INFO, msg, True) @@ -5184,7 +5204,7 @@ class Utilities(script_utilities.Utilities): debug.printMessage(debug.LEVEL_INFO, msg, True) return obj, offset - child = self.findChildAtOffset(obj, offset) + child = AXHypertext.find_child_at_offset(obj, offset) if not child: msg = "WEB: Child at offset is null. Returning context unchanged." debug.printMessage(debug.LEVEL_INFO, msg, True) @@ -5195,7 +5215,7 @@ class Utilities(script_utilities.Utilities): tokens = ["WEB: Child", child, "of", obj, "at offset", offset, "cannot be context."] debug.printTokens(debug.LEVEL_INFO, tokens, True) offset += 1 - child = self.findChildAtOffset(obj, offset) + child = AXHypertext.find_child_at_offset(obj, offset) if self.isListItemMarker(child): tokens = ["WEB: First caret context is next offset in", obj, ":", @@ -5238,9 +5258,9 @@ class Utilities(script_utilities.Utilities): if self._canHaveCaretContext(obj): text = self.queryNonEmptyText(obj) if text: - allText = text.getText(0, -1) + allText = AXText.get_all_text(obj) for i in range(offset + 1, len(allText)): - child = self.findChildAtOffset(obj, i) + child = AXHypertext.find_child_at_offset(obj, i) if child and allText[i] != self.EMBEDDED_OBJECT_CHARACTER: tokens = ["ERROR: Child", child, "found at offset with char '", allText[i].replace("\n", "\\n"), "'"] @@ -5312,11 +5332,11 @@ class Utilities(script_utilities.Utilities): if self._canHaveCaretContext(obj): text = self.queryNonEmptyText(obj) if text: - allText = text.getText(0, -1) + allText = AXText.get_all_text(obj) if offset == -1 or offset > len(allText): offset = len(allText) for i in range(offset - 1, -1, -1): - child = self.findChildAtOffset(obj, i) + child = AXHypertext.find_child_at_offset(obj, i) if child and allText[i] != self.EMBEDDED_OBJECT_CHARACTER: tokens = ["ERROR: Child", child, "found at offset with char '", allText[i].replace("\n", "\\n"), "'"] diff --git a/src/cthulhu/scripts/web/sound_generator.py b/src/cthulhu/scripts/web/sound_generator.py index 987a854..8c34de3 100644 --- a/src/cthulhu/scripts/web/sound_generator.py +++ b/src/cthulhu/scripts/web/sound_generator.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Utilities for obtaining sounds to be presented for objects.""" diff --git a/src/cthulhu/scripts/web/speech_generator.py b/src/cthulhu/scripts/web/speech_generator.py index 66d418b..d21c176 100644 --- a/src/cthulhu/scripts/web/speech_generator.py +++ b/src/cthulhu/scripts/web/speech_generator.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" @@ -37,6 +37,7 @@ from gi.repository import Atspi import urllib from cthulhu import debug +from cthulhu import input_event_manager from cthulhu import messages from cthulhu import object_properties from cthulhu import cthulhu_state @@ -44,6 +45,7 @@ from cthulhu import settings from cthulhu import settings_manager from cthulhu import speech_generator from cthulhu.ax_object import AXObject +from cthulhu.ax_text import AXText from cthulhu.ax_utilities import AXUtilities _settingsManager = settings_manager.getManager() @@ -301,12 +303,13 @@ class SpeechGenerator(speech_generator.SpeechGenerator): if args.get('leaving'): return [] - lastKey, mods = self._script.utilities.lastKeyAndModifiers() - if (lastKey in ['Down', 'Right'] or self._script.inSayAll()) and args.get('startOffset'): + manager = input_event_manager.get_manager() + if (manager.last_event_was_forward_caret_navigation() or self._script.inSayAll()) \ + and args.get('startOffset'): return [] - if lastKey in ['Up', 'Left']: - text = self._script.utilities.queryNonEmptyText(obj) - if text and args.get('endOffset') not in [None, text.characterCount]: + if manager.last_event_was_backward_caret_navigation(): + if self._script.utilities.treatAsTextObject(obj) \ + and args.get('endOffset') not in [None, AXText.get_character_count(obj)]: return [] result = [] @@ -357,8 +360,7 @@ class SpeechGenerator(speech_generator.SpeechGenerator): if self._script.utilities.isContentEditableWithEmbeddedObjects(obj) \ or self._script.utilities.isDocument(obj): - lastKey, mods = self._script.utilities.lastKeyAndModifiers() - if lastKey in ["Home", "End", "Up", "Down", "Left", "Right", "Page_Up", "Page_Down"]: + if input_event_manager.get_manager().last_event_was_caret_navigation(): return [] if AXUtilities.is_page_tab(priorObj) and AXObject.get_name(priorObj) == objName: @@ -593,15 +595,16 @@ class SpeechGenerator(speech_generator.SpeechGenerator): if self._script.utilities.isMenuInCollapsedSelectElement(obj): doNotSpeak.append(Atspi.Role.MENU) - lastKey, mods = self._script.utilities.lastKeyAndModifiers() isEditable = AXUtilities.is_editable(obj) if isEditable and not self._script.utilities.isContentEditableWithEmbeddedObjects(obj): - if ((lastKey in ["Down", "Right"] and not mods) or self._script.inSayAll()) and start: + manager = input_event_manager.get_manager() + if (manager.last_event_was_forward_caret_navigation() or self._script.inSayAll()) \ + and start: return [] - if lastKey in ["Up", "Left"] and not mods: + if manager.last_event_was_backward_caret_navigation(): text = self._script.utilities.queryNonEmptyText(obj) - if text and end not in [None, text.characterCount]: + if text and end not in [None, AXText.get_character_count(obj)]: return [] if role not in doNotSpeak: result.append(self.getLocalizedRoleName(obj, **args)) @@ -610,8 +613,7 @@ class SpeechGenerator(speech_generator.SpeechGenerator): elif isEditable and self._script.utilities.isDocument(obj): parent = AXObject.get_parent(obj) if parent and not AXUtilities.is_editable(parent) \ - and lastKey not in \ - ["Home", "End", "Up", "Down", "Left", "Right", "Page_Up", "Page_Down"]: + and not input_event_manager.get_manager().last_event_was_caret_navigation(): result.append(object_properties.ROLE_EDITABLE_CONTENT) result.extend(self.voice(speech_generator.SYSTEM, obj=obj, **args)) diff --git a/src/cthulhu/scripts/web/tutorial_generator.py b/src/cthulhu/scripts/web/tutorial_generator.py index b9dfb42..bf17e0a 100644 --- a/src/cthulhu/scripts/web/tutorial_generator.py +++ b/src/cthulhu/scripts/web/tutorial_generator.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu __id__ = "$Id$" __version__ = "$Revision$" diff --git a/src/cthulhu/settings.py b/src/cthulhu/settings.py index fd92e25..5c7d32b 100644 --- a/src/cthulhu/settings.py +++ b/src/cthulhu/settings.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Manages the settings for Cthulhu. This will defer to user settings first, but fallback to local settings if the user settings doesn't exist (e.g., in the @@ -78,6 +78,8 @@ userCustomizableSettings = [ "playSoundForState", "playSoundForPositionInSet", "playSoundForValue", + "soundTheme", + "enableModeChangeSound", "verbalizePunctuationStyle", "presentToolTips", "sayAllStyle", @@ -312,6 +314,8 @@ playSoundForRole = False playSoundForState = False playSoundForPositionInSet = False playSoundForValue = False +soundTheme = "default" +enableModeChangeSound = True # Keyboard and Echo keyboardLayout = GENERAL_KEYBOARD_LAYOUT_DESKTOP diff --git a/src/cthulhu/settings_manager.py b/src/cthulhu/settings_manager.py index 46ab9f7..b2e3eb9 100644 --- a/src/cthulhu/settings_manager.py +++ b/src/cthulhu/settings_manager.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Settings manager module. This will load/save user settings from a defined settings backend.""" @@ -57,7 +57,7 @@ try: except Exception: _proxy = None -_scriptManager = script_manager.getManager() +_scriptManager = script_manager.get_manager() class SettingsManager(object): """Settings backend manager. This class manages cthulhu user's settings diff --git a/src/cthulhu/signal_manager.py b/src/cthulhu/signal_manager.py index 72e3dce..22ad931 100644 --- a/src/cthulhu/signal_manager.py +++ b/src/cthulhu/signal_manager.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu import gi from gi.repository import GObject diff --git a/src/cthulhu/sleep_mode_manager.py b/src/cthulhu/sleep_mode_manager.py index 0bc28c3..6e3e268 100644 --- a/src/cthulhu/sleep_mode_manager.py +++ b/src/cthulhu/sleep_mode_manager.py @@ -19,8 +19,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Module for sleep mode management.""" @@ -133,16 +133,16 @@ class SleepModeManager: return True from . import cthulhu_state - scriptManager = script_manager.getManager() + scriptManager = script_manager.get_manager() if self.isActiveForApp(script.app): # Turning OFF sleep mode self._apps.remove(hash(script.app)) - newScript = scriptManager.getScript(script.app) + newScript = scriptManager.get_script(script.app) if notifyUser: newScript.presentMessage( messages.SLEEP_MODE_DISABLED_FOR % AXObject.get_name(script.app)) - scriptManager.setActiveScript(newScript, "Sleep mode toggled off") + scriptManager.set_active_script(newScript, "Sleep mode toggled off") debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Disabled for {AXObject.get_name(script.app)}", True) # Reset debounce timer after successful toggle self._lastToggleTime = 0 @@ -169,11 +169,11 @@ class SleepModeManager: try: debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Getting sleep script for {AXObject.get_name(script.app)}", True) - sleepScript = scriptManager.getOrCreateSleepModeScript(script.app) + sleepScript = scriptManager.get_or_create_sleep_mode_script(script.app) debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Got sleep script: {sleepScript}", True) debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Setting active script", True) - scriptManager.setActiveScript(sleepScript, "Sleep mode toggled on") + scriptManager.set_active_script(sleepScript, "Sleep mode toggled on") debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Active script set successfully", True) debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Adding app to sleep list", True) @@ -193,4 +193,4 @@ _manager = SleepModeManager() def getManager(): """Returns the Sleep Mode Manager singleton.""" - return _manager \ No newline at end of file + return _manager diff --git a/src/cthulhu/sound.py b/src/cthulhu/sound.py index c435aa2..575a32a 100644 --- a/src/cthulhu/sound.py +++ b/src/cthulhu/sound.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Utilities for playing sounds.""" diff --git a/src/cthulhu/sound_generator.py b/src/cthulhu/sound_generator.py index 5c57af0..0b0246b 100644 --- a/src/cthulhu/sound_generator.py +++ b/src/cthulhu/sound_generator.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Utilities for obtaining sounds to be presented for objects.""" diff --git a/src/cthulhu/sound_theme_manager.py b/src/cthulhu/sound_theme_manager.py new file mode 100644 index 0000000..3fe5b32 --- /dev/null +++ b/src/cthulhu/sound_theme_manager.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2024 Stormux +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., Franklin Street, Fifth Floor, +# Boston MA 02110-1301 USA. +# +# Cthulhu project: https://git.stormux.org/storm/cthulhu + +"""Sound Theme Manager for Cthulhu screen reader. + +Handles discovery and playback of sound theme files from: +- System: /usr/share/cthulhu/sounds/{theme_name}/ +- Local: ~/.local/share/cthulhu/sounds/{theme_name}/ +- User themes: Same as local, user can add custom folders + +Themes are simply folders containing sound files. The folder name is the theme name. +""" + +__id__ = "$Id$" +__version__ = "$Revision$" +__date__ = "$Date$" +__copyright__ = "Copyright (c) 2024 Stormux" +__license__ = "LGPL" + +import os + +from gi.repository import GLib + +from . import debug +from . import settings_manager +from . import sound +from .sound_generator import Icon + +_settingsManager = settings_manager.getManager() + +# Sound event constants - add new events here for easy extensibility +SOUND_FOCUS_MODE = "editbox" +SOUND_BROWSE_MODE = "browse_mode" +SOUND_BUTTON = "button" + +# Special theme name for no sounds +THEME_NONE = "none" + + +class SoundThemeManager: + """Manages sound themes for Cthulhu.""" + + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance._initialized = False + return cls._instance + + def __init__(self): + if self._initialized: + return + self._initialized = True + self._systemSoundsDir = None + self._userSoundsDir = None + + def getSystemSoundsDir(self): + """Get system sounds directory from platform settings.""" + if self._systemSoundsDir is None: + try: + from . import cthulhu_platform + datadir = getattr(cthulhu_platform, 'datadir', '/usr/share') + except ImportError: + datadir = '/usr/share' + self._systemSoundsDir = os.path.join(datadir, 'cthulhu', 'sounds') + return self._systemSoundsDir + + def getUserSoundsDir(self): + """Get user sounds directory (XDG_DATA_HOME/cthulhu/sounds).""" + if self._userSoundsDir is None: + self._userSoundsDir = os.path.join( + GLib.get_user_data_dir(), 'cthulhu', 'sounds' + ) + return self._userSoundsDir + + def getAvailableThemes(self): + """Discover all available sound themes. + + Returns list of theme names (folder names) from both system and user dirs. + User themes with same name as system themes will override system themes. + """ + themes = set() + + for baseDir in [self.getSystemSoundsDir(), self.getUserSoundsDir()]: + if os.path.isdir(baseDir): + try: + for entry in os.listdir(baseDir): + entryPath = os.path.join(baseDir, entry) + if os.path.isdir(entryPath): + themes.add(entry) + except OSError as e: + tokens = ["SOUND THEME: Error listing directory:", baseDir, str(e)] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + + return sorted(list(themes)) + + def getThemePath(self, themeName): + """Get the path to a theme directory. + + User themes take precedence over system themes. + """ + userPath = os.path.join(self.getUserSoundsDir(), themeName) + if os.path.isdir(userPath): + return userPath + + systemPath = os.path.join(self.getSystemSoundsDir(), themeName) + if os.path.isdir(systemPath): + return systemPath + + return None + + def getSoundPath(self, themeName, soundName): + """Get path to a specific sound file. + + Checks for common audio file extensions: .wav, .ogg, .mp3, .flac + """ + themePath = self.getThemePath(themeName) + if not themePath: + return None + + for ext in ['.wav', '.ogg', '.mp3', '.flac']: + soundPath = os.path.join(themePath, soundName + ext) + if os.path.isfile(soundPath): + return soundPath + + return None + + def playSound(self, soundName, interrupt=True): + """Play a sound from the current theme if enabled. + + Args: + soundName: The name of the sound file (without extension) + interrupt: Whether to interrupt currently playing sounds + + Returns: + True if sound was played, False otherwise + """ + if not _settingsManager.getSetting('enableModeChangeSound'): + return False + + themeName = _settingsManager.getSetting('soundTheme') + if not themeName: + themeName = 'default' + + # "none" theme means no sounds + if themeName == THEME_NONE: + return False + + soundPath = self.getSoundPath(themeName, soundName) + if not soundPath: + tokens = ["SOUND THEME: Sound not found:", soundName, "in theme:", themeName] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + return False + + try: + icon = Icon(os.path.dirname(soundPath), os.path.basename(soundPath)) + if icon.isValid(): + player = sound.getPlayer() + player.play(icon, interrupt=interrupt) + return True + except Exception as e: + tokens = ["SOUND THEME: Error playing sound:", str(e)] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + + return False + + def playFocusModeSound(self): + """Play sound for entering focus mode.""" + return self.playSound(SOUND_FOCUS_MODE) + + def playBrowseModeSound(self): + """Play sound for entering browse mode.""" + return self.playSound(SOUND_BROWSE_MODE) + + def playButtonSound(self): + """Play sound for button focus (future use).""" + return self.playSound(SOUND_BUTTON) + + +_manager = None + + +def getManager(): + """Get the singleton SoundThemeManager instance.""" + global _manager + if _manager is None: + _manager = SoundThemeManager() + return _manager diff --git a/src/cthulhu/speech.py b/src/cthulhu/speech.py index 6cd8b5b..3b394a0 100644 --- a/src/cthulhu/speech.py +++ b/src/cthulhu/speech.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Manages the default speech server for cthulhu. A script can use this as its speech server, or it can feel free to create one of its own.""" diff --git a/src/cthulhu/speech_and_verbosity_manager.py b/src/cthulhu/speech_and_verbosity_manager.py index ddf71da..c8cf216 100644 --- a/src/cthulhu/speech_and_verbosity_manager.py +++ b/src/cthulhu/speech_and_verbosity_manager.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Module for configuring speech and verbosity settings.""" diff --git a/src/cthulhu/speech_generator.py b/src/cthulhu/speech_generator.py index b77143d..50495a1 100644 --- a/src/cthulhu/speech_generator.py +++ b/src/cthulhu/speech_generator.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Utilities for obtaining speech utterances for objects.""" @@ -52,7 +52,10 @@ from . import settings_manager from . import speech from . import text_attribute_names from .ax_object import AXObject +from .ax_table import AXTable +from .ax_text import AXText from .ax_utilities import AXUtilities +from .ax_utilities_relation import AXUtilitiesRelation class Pause: """A dummy class to indicate we want to insert a pause into an @@ -391,8 +394,8 @@ class SpeechGenerator(generator.Generator): endOffset = args.get('endOffset') if endOffset is not None: - text = self._script.utilities.queryNonEmptyText(obj) - if text and text.characterCount != endOffset: + if self._script.utilities.queryNonEmptyText(obj) \ + and AXText.get_character_count(obj) != endOffset: return [] result = [messages.CONTENT_DELETION_END] @@ -434,8 +437,8 @@ class SpeechGenerator(generator.Generator): endOffset = args.get('endOffset') if endOffset is not None: - text = self._script.utilities.queryNonEmptyText(obj) - if text and text.characterCount != endOffset: + if self._script.utilities.queryNonEmptyText(obj) \ + and AXText.get_character_count(obj) != endOffset: return [] result = [messages.CONTENT_INSERTION_END] @@ -478,8 +481,8 @@ class SpeechGenerator(generator.Generator): endOffset = args.get('endOffset') if endOffset is not None: - text = self._script.utilities.queryNonEmptyText(obj) - if text and text.characterCount != endOffset: + if self._script.utilities.queryNonEmptyText(obj) \ + and AXText.get_character_count(obj) != endOffset: return [] result = [messages.CONTENT_MARK_END] @@ -929,11 +932,7 @@ class SpeechGenerator(generator.Generator): it exists. Otherwise, an empty array is returned. """ result = [] - try: - obj.queryImage() - except Exception: - pass - else: + if AXObject.supports_image(obj): args['role'] = Atspi.Role.IMAGE result.extend(self.generate(obj, **args)) result.extend(self.voice(DEFAULT, obj=obj, **args)) @@ -1141,15 +1140,9 @@ class SpeechGenerator(generator.Generator): parent = AXObject.get_parent(obj) if AXUtilities.is_table_cell(parent): obj = parent - parent = self._script.utilities.getTable(obj) - try: - table = parent.queryTable() - except Exception: - if args.get('guessCoordinates', False): - col = self._script.pointOfReference.get('lastColumn', -1) - else: - index = self._script.utilities.cellIndex(obj) - col = table.getColumnAtIndex(index) + row, col = self._script.utilities.coordinatesForCell(obj, False) + if col < 0 and args.get('guessCoordinates', False): + col = self._script.pointOfReference.get('lastColumn', -1) if col >= 0: result.append(messages.TABLE_COLUMN % (col + 1)) if result: @@ -1180,15 +1173,9 @@ class SpeechGenerator(generator.Generator): parent = AXObject.get_parent(obj) if AXUtilities.is_table_cell(parent): obj = parent - parent = self._script.utilities.getTable(obj) - try: - table = parent.queryTable() - except Exception: - if args.get('guessCoordinates', False): - row = self._script.pointOfReference.get('lastRow', -1) - else: - index = self._script.utilities.cellIndex(obj) - row = table.getRowAtIndex(index) + row, col = self._script.utilities.coordinatesForCell(obj, False) + if row < 0 and args.get('guessCoordinates', False): + row = self._script.pointOfReference.get('lastRow', -1) if row >= 0: result.append(messages.TABLE_ROW % (row + 1)) if result: @@ -1208,21 +1195,17 @@ class SpeechGenerator(generator.Generator): parent = AXObject.get_parent(obj) if AXUtilities.is_table_cell(parent): obj = parent - parent = self._script.utilities.getTable(obj) - try: - table = parent.queryTable() - except Exception: - table = None - else: - index = self._script.utilities.cellIndex(obj) - col = table.getColumnAtIndex(index) - row = table.getRowAtIndex(index) - result.append(messages.TABLE_COLUMN_DETAILED \ - % {"index" : (col + 1), - "total" : table.nColumns}) - result.append(messages.TABLE_ROW_DETAILED \ - % {"index" : (row + 1), - "total" : table.nRows}) + table = self._script.utilities.getTable(obj) + if table: + row, col = self._script.utilities.coordinatesForCell(obj, False) + rows, cols = self._script.utilities.rowAndColumnCount(table, False) + if row >= 0 and col >= 0 and rows > 0 and cols > 0: + result.append(messages.TABLE_COLUMN_DETAILED \ + % {"index" : (col + 1), + "total" : cols}) + result.append(messages.TABLE_ROW_DETAILED \ + % {"index" : (row + 1), + "total" : rows}) if result: result.extend(self.voice(SYSTEM, obj=obj, **args)) return result @@ -1330,16 +1313,7 @@ class SpeechGenerator(generator.Generator): """ attribStr = "" - defaultAttributes = text.getDefaultAttributes() - keyList, attributesDictionary = \ - self._script.utilities.stringToKeysAndDict(defaultAttributes) - - charAttributes = text.getAttributes(textOffset) - if charAttributes[0]: - keyList, charDict = \ - self._script.utilities.stringToKeysAndDict(charAttributes[0]) - for key in keyList: - attributesDictionary[key] = charDict[key] + attributesDictionary, _, _ = AXText.get_text_attributes_at_offset(obj, textOffset) if attributesDictionary: for key in keys: @@ -1391,8 +1365,11 @@ class SpeechGenerator(generator.Generator): except Exception: pass - textObj = obj.queryText() - caretOffset = textObj.caretOffset + if not AXObject.supports_text(obj): + self._script.generatorCache['textInformation'] = ["", 0, 0, False] + return self._script.generatorCache['textInformation'] + + caretOffset = AXText.get_caret_offset(obj) textContents, startOffset, endOffset = self._script.utilities.allSelectedText(obj) selected = textContents != "" @@ -1400,17 +1377,14 @@ class SpeechGenerator(generator.Generator): if not selected: # Get the line containing the caret # - [line, startOffset, endOffset] = textObj.getTextAtOffset( - textObj.caretOffset, - Atspi.TextBoundaryType.LINE_START) + [line, startOffset, endOffset] = AXText.get_line_at_offset(obj, caretOffset) if len(line): line = self._script.utilities.adjustForRepeats(line) textContents = line else: - char = textObj.getTextAtOffset(caretOffset, - Atspi.TextBoundaryType.CHAR) - if char[0] == "\n" and startOffset == caretOffset: - textContents = char[0] + char, charStart, charEnd = AXText.get_character_at_offset(obj, caretOffset) + if char == "\n" and startOffset == caretOffset: + textContents = char if self._script.utilities.shouldVerbalizeAllPunctuation(obj): textContents = self._script.utilities.verbalizeAllPunctuation(textContents) @@ -1430,9 +1404,7 @@ class SpeechGenerator(generator.Generator): if result: return result - try: - obj.queryText() - except NotImplementedError: + if not AXObject.supports_text(obj): return [] result = [] @@ -1455,9 +1427,7 @@ class SpeechGenerator(generator.Generator): called prior to this method. """ - try: - text = obj.queryText() - except NotImplementedError: + if not AXObject.supports_text(obj): return [] [line, startOffset, endOffset, selected] = self._getTextInformation(obj) @@ -1466,7 +1436,7 @@ class SpeechGenerator(generator.Generator): lastAttribs = None textOffset = startOffset for i in range(0, len(line)): - attribs = self._getCharacterAttributes(obj, text, textOffset, i) + attribs = self._getCharacterAttributes(obj, None, textOffset, i) if attribs and attribs != lastAttribs: if newLine: newLine += " ; " @@ -1477,7 +1447,7 @@ class SpeechGenerator(generator.Generator): textOffset += 1 attribs = self._getCharacterAttributes(obj, - text, + None, startOffset, 0, ["paragraph-style"]) @@ -1500,9 +1470,7 @@ class SpeechGenerator(generator.Generator): if _settingsManager.getSetting('onlySpeakDisplayedText'): return [] - try: - obj.queryText() - except NotImplementedError: + if not AXObject.supports_text(obj): return [] result = [] @@ -1523,16 +1491,11 @@ class SpeechGenerator(generator.Generator): return [] result = [] - try: - textObj = obj.queryText() - except Exception: - pass - else: - noOfSelections = textObj.getNSelections() - if noOfSelections == 1: - [string, startOffset, endOffset] = \ - textObj.getTextAtOffset(0, Atspi.TextBoundaryType.LINE_START) - if startOffset == 0 and endOffset == len(string): + if AXObject.supports_text(obj): + selections = AXText.get_selected_ranges(obj) + if len(selections) == 1: + startOffset, endOffset = selections[0] + if startOffset == 0 and endOffset >= AXText.get_character_count(obj): result = [messages.TEXT_SELECTED] result.extend(self.voice(SYSTEM, obj=obj, **args)) return result @@ -1666,7 +1629,7 @@ class SpeechGenerator(generator.Generator): # TODO - JD: We need other ways to determine group membership. Not all # implementations expose the member-of relation. Gtk3 does. Others are TBD. - members = AXObject.get_relation_targets(obj, Atspi.RelationType.MEMBER_OF) + members = AXUtilitiesRelation.get_is_member_of(obj) if priorObj not in members: return result @@ -2290,8 +2253,7 @@ class SpeechGenerator(generator.Generator): # TODO - JD: We need other ways to determine group membership. Not all # implementations expose the member-of relation. Gtk3 does. Others are TBD. - members = AXObject.get_relation_targets( - obj, Atspi.RelationType.MEMBER_OF, AXUtilities.is_showing) + members = [m for m in AXUtilitiesRelation.get_is_member_of(obj) if AXUtilities.is_showing(m)] if obj not in members: return [] @@ -2836,16 +2798,15 @@ class SpeechGenerator(generator.Generator): return result def _generateMathTableStart(self, obj, **args): - try: - table = obj.queryTable() - except Exception: - return [] - nestingLevel = self._script.utilities.getMathNestingLevel(obj) + rows = AXTable.get_row_count(obj, prefer_attribute=False) + cols = AXTable.get_column_count(obj, prefer_attribute=False) + if rows < 0 or cols < 0: + return [] if nestingLevel > 0: - result = [messages.mathNestedTableSize(table.nRows, table.nColumns)] + result = [messages.mathNestedTableSize(rows, cols)] else: - result = [messages.mathTableSize(table.nRows, table.nColumns)] + result = [messages.mathTableSize(rows, cols)] result.extend(self.voice(SYSTEM, obj=obj, **args)) return result diff --git a/src/cthulhu/speechdispatcherfactory.py b/src/cthulhu/speechdispatcherfactory.py index efb6297..5c88e2d 100644 --- a/src/cthulhu/speechdispatcherfactory.py +++ b/src/cthulhu/speechdispatcherfactory.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Provides an Cthulhu speech server for Speech Dispatcher backend.""" diff --git a/src/cthulhu/speechserver.py b/src/cthulhu/speechserver.py index 3901b67..eb95d82 100644 --- a/src/cthulhu/speechserver.py +++ b/src/cthulhu/speechserver.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Provides an abtract class for working with speech servers. diff --git a/src/cthulhu/spellcheck.py b/src/cthulhu/spellcheck.py index 348cb93..22bccc5 100644 --- a/src/cthulhu/spellcheck.py +++ b/src/cthulhu/spellcheck.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Script-customizable support for application spellcheckers.""" @@ -44,6 +44,7 @@ from cthulhu import object_properties from cthulhu import cthulhu_state from cthulhu import settings_manager from cthulhu.ax_object import AXObject +from cthulhu.ax_text import AXText from cthulhu.ax_utilities import AXUtilities _settingsManager = settings_manager.getManager() @@ -156,18 +157,14 @@ class SpellCheck: if not (obj and offset >= 0): return False - try: - text = obj.queryText() - except Exception: + # This should work, but some toolkits are broken. + if not AXObject.supports_text(obj): return False - # This should work, but some toolkits are broken. - boundary = Atspi.TextBoundaryType.SENTENCE_START - string, start, end = text.getTextAtOffset(offset, boundary) + string, start, end = AXText.get_sentence_at_offset(obj, offset) if not string: - boundary = Atspi.TextBoundaryType.LINE_START - string, start, end = text.getTextAtOffset(offset, boundary) + string, start, end = AXText.get_line_at_offset(obj, offset) sentences = re.split(r'(?:\.|\!|\?)', string) word = self.getMisspelledWord() if string.count(word) == 1: diff --git a/src/cthulhu/structural_navigation.py b/src/cthulhu/structural_navigation.py index c7890db..3abe229 100644 --- a/src/cthulhu/structural_navigation.py +++ b/src/cthulhu/structural_navigation.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Implements structural navigation.""" @@ -51,6 +51,8 @@ from . import settings_manager from .ax_collection import AXCollection from .ax_event_synthesizer import AXEventSynthesizer from .ax_object import AXObject +from .ax_table import AXTable +from .ax_text import AXText from .ax_selection import AXSelection from .ax_utilities import AXUtilities @@ -974,13 +976,11 @@ class StructuralNavigation: - obj: the accessible table whose caption we want. """ - caption = obj.queryTable().caption - try: - caption.queryText() - except Exception: + caption = AXTable.get_caption(obj) + if not caption: return None - else: - return self._script.utilities.displayedText(caption) + + return AXText.get_all_text(caption) def _getTableDescription(self, obj): """Returns a string which describes the table.""" @@ -1056,12 +1056,8 @@ class StructuralNavigation: if obj and (AXObject.get_name(obj) or AXObject.get_child_count(obj)): return False - try: - text = obj.queryText() - except Exception: - pass - else: - if text.getText(0, -1).strip(): + if AXObject.supports_text(obj): + if AXText.get_all_text(obj).strip(): return False return True @@ -1174,7 +1170,7 @@ class StructuralNavigation: tokens = ["STRUCTURAL NAVIGATION:", obj, "became defunct after setting caret position"] debug.printTokens(debug.LEVEL_INFO, tokens, True) - replicant = self._script.utilities.getObjectFromPath(objPath) + replicant = self._script.utilities.get_objectFromPath(objPath) if replicant and AXObject.get_role(replicant) == objRole: tokens = ["STRUCTURAL NAVIGATION: Updating obj to replicant", replicant] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -1251,12 +1247,7 @@ class StructuralNavigation: if item: text = AXObject.get_name(item) if not text and AXUtilities.is_image(obj): - try: - image = obj.queryImage() - except Exception: - text = AXObject.get_description(obj) - else: - text = image.imageDescription or AXObject.get_description(obj) + text = AXObject.get_image_description(obj) or AXObject.get_description(obj) if not text: parent = AXObject.get_parent(obj) if AXUtilities.is_link(parent): @@ -1491,12 +1482,16 @@ class StructuralNavigation: return True text = self._script.utilities.queryNonEmptyText(obj) - if not (text and text.characterCount > settings.largeObjectTextLength): + if not text: return False - string = text.getText(0, -1) + char_count = AXText.get_character_count(obj) + if char_count <= settings.largeObjectTextLength: + return False + + string = AXText.get_all_text(obj) eocs = string.count(self._script.EMBEDDED_OBJECT_CHARACTER) - if eocs/text.characterCount < 0.05: + if char_count and eocs/char_count < 0.05: return True return False @@ -1991,16 +1986,15 @@ class StructuralNavigation: if AXUtilities.is_heading(obj): return True - try: - text = obj.queryText() - # We're choosing 3 characters as the minimum because some - # paragraphs contain a single image or link and a text - # of length 2: An embedded object character and a space. - # We want to skip these. - return text.characterCount > 2 - except Exception: + if not AXObject.supports_text(obj): return False + # We're choosing 3 characters as the minimum because some + # paragraphs contain a single image or link and a text + # of length 2: An embedded object character and a space. + # We want to skip these. + return AXText.get_character_count(obj) > 2 + return AXUtilities.find_all_paragraphs(document, True, has_at_least_three_characters) def _paragraphPresentation(self, obj, arg=None): @@ -2117,11 +2111,11 @@ class StructuralNavigation: if attrs.get('layout-guess') == 'true': return False - try: - return obj.queryTable().nRows > 0 - except Exception: + if not AXObject.supports_table(obj): return False + return AXTable.get_row_count(obj, prefer_attribute=False) > 0 + return AXUtilities.find_all_tables(document, is_not_layout_or_empty) def _tablePresentation(self, obj, arg=None): @@ -2130,7 +2124,7 @@ class StructuralNavigation: if caption: self._script.presentMessage(caption) self._script.presentMessage(self._getTableDescription(obj)) - cell = obj.queryTable().getAccessibleAt(0, 0) + cell = AXTable.get_cell_at(obj, 0, 0) if not cell: tokens = ["STRUCTURAL NAVIGATION: Broken table interface for", obj] debug.printTokens(debug.LEVEL_INFO, tokens, True) diff --git a/src/cthulhu/text_attribute_names.py b/src/cthulhu/text_attribute_names.py index 2c19cf7..78ecfbe 100644 --- a/src/cthulhu/text_attribute_names.py +++ b/src/cthulhu/text_attribute_names.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Provides getTextAttributeName method that maps each text attribute into its localized equivalent.""" diff --git a/src/cthulhu/translation_context.py b/src/cthulhu/translation_context.py index aac71ac..6e6fe9c 100644 --- a/src/cthulhu/translation_context.py +++ b/src/cthulhu/translation_context.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu import gi, os, locale, gettext from gi.repository import GObject diff --git a/src/cthulhu/translation_manager.py b/src/cthulhu/translation_manager.py index 8265581..b5394ec 100644 --- a/src/cthulhu/translation_manager.py +++ b/src/cthulhu/translation_manager.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu import gi, os, locale, gettext from gi.repository import GObject diff --git a/src/cthulhu/tutorialgenerator.py b/src/cthulhu/tutorialgenerator.py index 8b18e50..670ee71 100644 --- a/src/cthulhu/tutorialgenerator.py +++ b/src/cthulhu/tutorialgenerator.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Utilities for obtaining tutorial utterances for objects. In general, there probably should be a singleton instance of the TutorialGenerator @@ -44,6 +44,7 @@ from . import cthulhu_state from . import settings from .ax_object import AXObject +from .ax_table import AXTable from .ax_utilities import AXUtilities from .cthulhu_i18n import _ # for gettext support @@ -613,10 +614,7 @@ class TutorialGenerator: if (not alreadyFocused): parent = AXObject.get_parent(obj) - try: - parent_table = parent.queryTable() - except Exception: - parent_table = None + parent_table = AXTable.get_table(parent) readFullRow = self._script.utilities.shouldReadFullRow(obj) if readFullRow and parent_table and not self._script.utilities.isLayoutOnly(parent): utterances.extend(self._getTutorialForTableCell(obj, diff --git a/src/cthulhu/where_am_i_presenter.py b/src/cthulhu/where_am_i_presenter.py index bbcac10..0ac9a73 100644 --- a/src/cthulhu/where_am_i_presenter.py +++ b/src/cthulhu/where_am_i_presenter.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Module for commands related to the current accessible object.""" diff --git a/test/harness/__init__.py b/test/harness/__init__.py index 782103c..301e5ea 100644 --- a/test/harness/__init__.py +++ b/test/harness/__init__.py @@ -20,6 +20,6 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu diff --git a/test/harness/runcthulhu.py b/test/harness/runcthulhu.py index fb527d9..2163e50 100644 --- a/test/harness/runcthulhu.py +++ b/test/harness/runcthulhu.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu import argparse import dbus diff --git a/test/harness/runprofiler.py b/test/harness/runprofiler.py index a0cfdee..13ccb69 100644 --- a/test/harness/runprofiler.py +++ b/test/harness/runprofiler.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu try: import cProfile as myprofiler diff --git a/test/harness/settings_test.py b/test/harness/settings_test.py index e60a326..56cd0db 100644 --- a/test/harness/settings_test.py +++ b/test/harness/settings_test.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from cthulhu import settings_manager from json import load, dump diff --git a/test/harness/utils.py b/test/harness/utils.py index 2f24609..0ef540a 100644 --- a/test/harness/utils.py +++ b/test/harness/utils.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Utilities that can be used by tests.""" diff --git a/test/html/cthulhu-wiki.html b/test/html/cthulhu-wiki.html index 170f5cd..c810ccc 100644 --- a/test/html/cthulhu-wiki.html +++ b/test/html/cthulhu-wiki.html @@ -151,7 +151,7 @@ searchBlur(e);

How Can I Help?

There's a bunch you can do! Please refer to the How Can I Help page for detailed information.

More Information

-


+


The information on this page and the other Cthulhu-related pages on this site are distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.


CategoryAccessibility

Cthulhu (last edited 2007-12-07 22:09:22 by WillieWalker)

diff --git a/test/keystrokes/firefox/aria_alert.py b/test/keystrokes/firefox/aria_alert.py index d8a4d0e..40248a9 100644 --- a/test/keystrokes/firefox/aria_alert.py +++ b/test/keystrokes/firefox/aria_alert.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of ARIA alert presentation.""" diff --git a/test/keystrokes/firefox/aria_alert_dialog.py b/test/keystrokes/firefox/aria_alert_dialog.py index 531bf3f..3e9b120 100644 --- a/test/keystrokes/firefox/aria_alert_dialog.py +++ b/test/keystrokes/firefox/aria_alert_dialog.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of UIUC button presentation using Firefox.""" diff --git a/test/keystrokes/firefox/aria_button.py b/test/keystrokes/firefox/aria_button.py index 8933b10..e6561e8 100644 --- a/test/keystrokes/firefox/aria_button.py +++ b/test/keystrokes/firefox/aria_button.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of ARIA button presentation.""" diff --git a/test/keystrokes/firefox/aria_button_dojo.py b/test/keystrokes/firefox/aria_button_dojo.py index 3c9c305..2433001 100644 --- a/test/keystrokes/firefox/aria_button_dojo.py +++ b/test/keystrokes/firefox/aria_button_dojo.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/aria_button_toggle.py b/test/keystrokes/firefox/aria_button_toggle.py index fadd757..9ffb799 100644 --- a/test/keystrokes/firefox/aria_button_toggle.py +++ b/test/keystrokes/firefox/aria_button_toggle.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/aria_checkbox.py b/test/keystrokes/firefox/aria_checkbox.py index f1a5cc2..a6a13a0 100644 --- a/test/keystrokes/firefox/aria_checkbox.py +++ b/test/keystrokes/firefox/aria_checkbox.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of ARIA checkbox presentation.""" diff --git a/test/keystrokes/firefox/aria_checkbox_dojo.py b/test/keystrokes/firefox/aria_checkbox_dojo.py index c094cf9..b1eff64 100644 --- a/test/keystrokes/firefox/aria_checkbox_dojo.py +++ b/test/keystrokes/firefox/aria_checkbox_dojo.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of Dojo checkbox presentation.""" diff --git a/test/keystrokes/firefox/aria_combobox_dojo.py b/test/keystrokes/firefox/aria_combobox_dojo.py index 4517d84..d65f324 100644 --- a/test/keystrokes/firefox/aria_combobox_dojo.py +++ b/test/keystrokes/firefox/aria_combobox_dojo.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of Dojo combo box presentation.""" diff --git a/test/keystrokes/firefox/aria_dialog_dismissed.py b/test/keystrokes/firefox/aria_dialog_dismissed.py index 68b369f..97955e5 100644 --- a/test/keystrokes/firefox/aria_dialog_dismissed.py +++ b/test/keystrokes/firefox/aria_dialog_dismissed.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/aria_dialog_dojo.py b/test/keystrokes/firefox/aria_dialog_dojo.py index a1f36eb..281eb54 100644 --- a/test/keystrokes/firefox/aria_dialog_dojo.py +++ b/test/keystrokes/firefox/aria_dialog_dojo.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of Dojo dialog presentation.""" diff --git a/test/keystrokes/firefox/aria_editor_navigation_dojo.py b/test/keystrokes/firefox/aria_editor_navigation_dojo.py index 595db16..e732f82 100644 --- a/test/keystrokes/firefox/aria_editor_navigation_dojo.py +++ b/test/keystrokes/firefox/aria_editor_navigation_dojo.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test navigation out of the Dojo editor.""" diff --git a/test/keystrokes/firefox/aria_invalid.py b/test/keystrokes/firefox/aria_invalid.py index 65acb05..0da62e1 100644 --- a/test/keystrokes/firefox/aria_invalid.py +++ b/test/keystrokes/firefox/aria_invalid.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/aria_landmarks.py b/test/keystrokes/firefox/aria_landmarks.py index 40fea41..b33b012 100644 --- a/test/keystrokes/firefox/aria_landmarks.py +++ b/test/keystrokes/firefox/aria_landmarks.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of structural navigation amongst landmarks.""" diff --git a/test/keystrokes/firefox/aria_list.py b/test/keystrokes/firefox/aria_list.py index 7755bb1..bd1664e 100644 --- a/test/keystrokes/firefox/aria_list.py +++ b/test/keystrokes/firefox/aria_list.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of presentation of ARIA role list.""" diff --git a/test/keystrokes/firefox/aria_menu.py b/test/keystrokes/firefox/aria_menu.py index 8f887bc..5c8035e 100644 --- a/test/keystrokes/firefox/aria_menu.py +++ b/test/keystrokes/firefox/aria_menu.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of ARIA menu presentation.""" diff --git a/test/keystrokes/firefox/aria_progressbar.py b/test/keystrokes/firefox/aria_progressbar.py index 003b2c6..a161643 100644 --- a/test/keystrokes/firefox/aria_progressbar.py +++ b/test/keystrokes/firefox/aria_progressbar.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of ARIA progressbar presentation.""" diff --git a/test/keystrokes/firefox/aria_radiobutton.py b/test/keystrokes/firefox/aria_radiobutton.py index 32acef2..42c4941 100644 --- a/test/keystrokes/firefox/aria_radiobutton.py +++ b/test/keystrokes/firefox/aria_radiobutton.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/aria_roledescription_where_am_i.py b/test/keystrokes/firefox/aria_roledescription_where_am_i.py index 51a9ae2..494ba7d 100644 --- a/test/keystrokes/firefox/aria_roledescription_where_am_i.py +++ b/test/keystrokes/firefox/aria_roledescription_where_am_i.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/aria_slider.py b/test/keystrokes/firefox/aria_slider.py index 2bbfe2f..cdc98d7 100644 --- a/test/keystrokes/firefox/aria_slider.py +++ b/test/keystrokes/firefox/aria_slider.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of ARIA slider presentation.""" diff --git a/test/keystrokes/firefox/aria_slider_dojo.py b/test/keystrokes/firefox/aria_slider_dojo.py index 86f1d54..89fcb00 100644 --- a/test/keystrokes/firefox/aria_slider_dojo.py +++ b/test/keystrokes/firefox/aria_slider_dojo.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of Dojo slider presentation.""" diff --git a/test/keystrokes/firefox/aria_slider_tpg.py b/test/keystrokes/firefox/aria_slider_tpg.py index 6393664..b3e802e 100644 --- a/test/keystrokes/firefox/aria_slider_tpg.py +++ b/test/keystrokes/firefox/aria_slider_tpg.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of ARIA horizontal sliders using Firefox.""" diff --git a/test/keystrokes/firefox/aria_sliders.py b/test/keystrokes/firefox/aria_sliders.py index 3c20c2e..20a5045 100644 --- a/test/keystrokes/firefox/aria_sliders.py +++ b/test/keystrokes/firefox/aria_sliders.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/aria_spinner_dojo.py b/test/keystrokes/firefox/aria_spinner_dojo.py index cdc3d25..561815b 100644 --- a/test/keystrokes/firefox/aria_spinner_dojo.py +++ b/test/keystrokes/firefox/aria_spinner_dojo.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of Dojo spinner presentation.""" diff --git a/test/keystrokes/firefox/aria_switch.py b/test/keystrokes/firefox/aria_switch.py index caebb56..de74322 100644 --- a/test/keystrokes/firefox/aria_switch.py +++ b/test/keystrokes/firefox/aria_switch.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of ARIA switch presentation.""" diff --git a/test/keystrokes/firefox/aria_tabcontainer_dojo.py b/test/keystrokes/firefox/aria_tabcontainer_dojo.py index dbc298c..d02248c 100644 --- a/test/keystrokes/firefox/aria_tabcontainer_dojo.py +++ b/test/keystrokes/firefox/aria_tabcontainer_dojo.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of Dojo tab container presentation.""" diff --git a/test/keystrokes/firefox/aria_tabpanel.py b/test/keystrokes/firefox/aria_tabpanel.py index c07ba7d..d1b359a 100644 --- a/test/keystrokes/firefox/aria_tabpanel.py +++ b/test/keystrokes/firefox/aria_tabpanel.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of ARIA tabpanel presentation.""" diff --git a/test/keystrokes/firefox/aria_tabpanel2.py b/test/keystrokes/firefox/aria_tabpanel2.py index 128c429..4172a61 100644 --- a/test/keystrokes/firefox/aria_tabpanel2.py +++ b/test/keystrokes/firefox/aria_tabpanel2.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/aria_tabpanel_text_dojo.py b/test/keystrokes/firefox/aria_tabpanel_text_dojo.py index d3d8593..acd3fe4 100644 --- a/test/keystrokes/firefox/aria_tabpanel_text_dojo.py +++ b/test/keystrokes/firefox/aria_tabpanel_text_dojo.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of presentation of Dojo's panel text.""" diff --git a/test/keystrokes/firefox/aria_toolbar_dojo.py b/test/keystrokes/firefox/aria_toolbar_dojo.py index 7edbbb1..26c1d13 100644 --- a/test/keystrokes/firefox/aria_toolbar_dojo.py +++ b/test/keystrokes/firefox/aria_toolbar_dojo.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of Dojo toolbar presentation.""" diff --git a/test/keystrokes/firefox/aria_tree.py b/test/keystrokes/firefox/aria_tree.py index 8c3ca03..2aaf167 100644 --- a/test/keystrokes/firefox/aria_tree.py +++ b/test/keystrokes/firefox/aria_tree.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/aria_tree_dojo.py b/test/keystrokes/firefox/aria_tree_dojo.py index a618ccf..170a2d3 100644 --- a/test/keystrokes/firefox/aria_tree_dojo.py +++ b/test/keystrokes/firefox/aria_tree_dojo.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of Dojo tree presentation.""" diff --git a/test/keystrokes/firefox/aria_treegrid.py b/test/keystrokes/firefox/aria_treegrid.py index cf0996b..f7030fd 100644 --- a/test/keystrokes/firefox/aria_treegrid.py +++ b/test/keystrokes/firefox/aria_treegrid.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of ARIA treegrid presentation.""" diff --git a/test/keystrokes/firefox/find_wiki.py b/test/keystrokes/firefox/find_wiki.py index e374f2b..b1477f3 100644 --- a/test/keystrokes/firefox/find_wiki.py +++ b/test/keystrokes/firefox/find_wiki.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of find result presentation.""" diff --git a/test/keystrokes/firefox/flat_review_combo_box.py b/test/keystrokes/firefox/flat_review_combo_box.py index a83deac..79281d6 100644 --- a/test/keystrokes/firefox/flat_review_combo_box.py +++ b/test/keystrokes/firefox/flat_review_combo_box.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of flat reviewing HTML.""" diff --git a/test/keystrokes/firefox/flat_review_hidden_elements.py b/test/keystrokes/firefox/flat_review_hidden_elements.py index dcd36a2..59d85c6 100644 --- a/test/keystrokes/firefox/flat_review_hidden_elements.py +++ b/test/keystrokes/firefox/flat_review_hidden_elements.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of flat review in content with hidden elements.""" diff --git a/test/keystrokes/firefox/flat_review_text_by_line.py b/test/keystrokes/firefox/flat_review_text_by_line.py index d859372..089d602 100644 --- a/test/keystrokes/firefox/flat_review_text_by_line.py +++ b/test/keystrokes/firefox/flat_review_text_by_line.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of flat review by line in a simple text document.""" diff --git a/test/keystrokes/firefox/flat_review_text_by_word_and_char.py b/test/keystrokes/firefox/flat_review_text_by_word_and_char.py index df49214..b118fc3 100644 --- a/test/keystrokes/firefox/flat_review_text_by_word_and_char.py +++ b/test/keystrokes/firefox/flat_review_text_by_word_and_char.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of flat review by word and char in a simple text document.""" diff --git a/test/keystrokes/firefox/focus_tracking_descriptions.py b/test/keystrokes/firefox/focus_tracking_descriptions.py index f7538b7..3210c9d 100644 --- a/test/keystrokes/firefox/focus_tracking_descriptions.py +++ b/test/keystrokes/firefox/focus_tracking_descriptions.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of the fix for bug 511389.""" diff --git a/test/keystrokes/firefox/focus_tracking_imagemap.py b/test/keystrokes/firefox/focus_tracking_imagemap.py index be235b8..f2fad36 100644 --- a/test/keystrokes/firefox/focus_tracking_imagemap.py +++ b/test/keystrokes/firefox/focus_tracking_imagemap.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of Cthulhu output when tabbing on a page with imagemaps.""" diff --git a/test/keystrokes/firefox/focus_tracking_input_type_number.py b/test/keystrokes/firefox/focus_tracking_input_type_number.py index 784ceef..b1a9055 100644 --- a/test/keystrokes/firefox/focus_tracking_input_type_number.py +++ b/test/keystrokes/firefox/focus_tracking_input_type_number.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/focus_tracking_link_child_of_body.py b/test/keystrokes/firefox/focus_tracking_link_child_of_body.py index 75c90f0..9f38324 100644 --- a/test/keystrokes/firefox/focus_tracking_link_child_of_body.py +++ b/test/keystrokes/firefox/focus_tracking_link_child_of_body.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/focus_tracking_links.py b/test/keystrokes/firefox/focus_tracking_links.py index 17592a9..46df663 100644 --- a/test/keystrokes/firefox/focus_tracking_links.py +++ b/test/keystrokes/firefox/focus_tracking_links.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of the fix for bug 511389.""" diff --git a/test/keystrokes/firefox/focus_tracking_radios_with_label_and_name.py b/test/keystrokes/firefox/focus_tracking_radios_with_label_and_name.py index 905ff8f..8271daf 100644 --- a/test/keystrokes/firefox/focus_tracking_radios_with_label_and_name.py +++ b/test/keystrokes/firefox/focus_tracking_radios_with_label_and_name.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/focus_tracking_roledescriptions.py b/test/keystrokes/firefox/focus_tracking_roledescriptions.py index ac17c13..2414d42 100644 --- a/test/keystrokes/firefox/focus_tracking_roledescriptions.py +++ b/test/keystrokes/firefox/focus_tracking_roledescriptions.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/html_access_keys.py b/test/keystrokes/firefox/html_access_keys.py index e1542c1..afce238 100644 --- a/test/keystrokes/firefox/html_access_keys.py +++ b/test/keystrokes/firefox/html_access_keys.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/html_link_where_am_i.py b/test/keystrokes/firefox/html_link_where_am_i.py index 7d63f5f..63c80b1 100644 --- a/test/keystrokes/firefox/html_link_where_am_i.py +++ b/test/keystrokes/firefox/html_link_where_am_i.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of Where am I for links.""" diff --git a/test/keystrokes/firefox/html_page_summary.py b/test/keystrokes/firefox/html_page_summary.py index 1cfa8e5..f2c7478 100644 --- a/test/keystrokes/firefox/html_page_summary.py +++ b/test/keystrokes/firefox/html_page_summary.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of page summary""" diff --git a/test/keystrokes/firefox/html_role_combo_box.py b/test/keystrokes/firefox/html_role_combo_box.py index 417f2d6..75576a6 100644 --- a/test/keystrokes/firefox/html_role_combo_box.py +++ b/test/keystrokes/firefox/html_role_combo_box.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of HTML combo box presentation.""" diff --git a/test/keystrokes/firefox/html_role_links.py b/test/keystrokes/firefox/html_role_links.py index 43b0276..0315d29 100644 --- a/test/keystrokes/firefox/html_role_links.py +++ b/test/keystrokes/firefox/html_role_links.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of HTML links presentation.""" diff --git a/test/keystrokes/firefox/html_role_list_item_where_am_i.py b/test/keystrokes/firefox/html_role_list_item_where_am_i.py index 466ec23..7bbe17f 100644 --- a/test/keystrokes/firefox/html_role_list_item_where_am_i.py +++ b/test/keystrokes/firefox/html_role_list_item_where_am_i.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of HTML list item whereAmI presentation.""" diff --git a/test/keystrokes/firefox/html_struct_nav_activate_link.py b/test/keystrokes/firefox/html_struct_nav_activate_link.py index 501886a..0b246bb 100644 --- a/test/keystrokes/firefox/html_struct_nav_activate_link.py +++ b/test/keystrokes/firefox/html_struct_nav_activate_link.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test for activation of a link after using structural navigation""" diff --git a/test/keystrokes/firefox/html_struct_nav_blockquote.py b/test/keystrokes/firefox/html_struct_nav_blockquote.py index f63d04b..962c0e7 100644 --- a/test/keystrokes/firefox/html_struct_nav_blockquote.py +++ b/test/keystrokes/firefox/html_struct_nav_blockquote.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of structural navigation by blockquote.""" diff --git a/test/keystrokes/firefox/html_struct_nav_bug_554616.py b/test/keystrokes/firefox/html_struct_nav_bug_554616.py index b34a278..c99fabc 100644 --- a/test/keystrokes/firefox/html_struct_nav_bug_554616.py +++ b/test/keystrokes/firefox/html_struct_nav_bug_554616.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of table cell structural navigation.""" diff --git a/test/keystrokes/firefox/html_struct_nav_bug_556470.py b/test/keystrokes/firefox/html_struct_nav_bug_556470.py index 0ef3f4a..92adaa8 100644 --- a/test/keystrokes/firefox/html_struct_nav_bug_556470.py +++ b/test/keystrokes/firefox/html_struct_nav_bug_556470.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of table structural navigation with empty tables.""" diff --git a/test/keystrokes/firefox/html_struct_nav_bug_567984.py b/test/keystrokes/firefox/html_struct_nav_bug_567984.py index 732763d..f586995 100644 --- a/test/keystrokes/firefox/html_struct_nav_bug_567984.py +++ b/test/keystrokes/firefox/html_struct_nav_bug_567984.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of structural navigation by heading.""" diff --git a/test/keystrokes/firefox/html_struct_nav_bug_591592.py b/test/keystrokes/firefox/html_struct_nav_bug_591592.py index e23f9d8..43b1297 100644 --- a/test/keystrokes/firefox/html_struct_nav_bug_591592.py +++ b/test/keystrokes/firefox/html_struct_nav_bug_591592.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of table structural navigation with headings which contain anchors.""" diff --git a/test/keystrokes/firefox/html_struct_nav_clickable_text_change.py b/test/keystrokes/firefox/html_struct_nav_clickable_text_change.py index 1bb5363..f27f0a3 100644 --- a/test/keystrokes/firefox/html_struct_nav_clickable_text_change.py +++ b/test/keystrokes/firefox/html_struct_nav_clickable_text_change.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/html_struct_nav_containers.py b/test/keystrokes/firefox/html_struct_nav_containers.py index 22e2c0b..518a892 100644 --- a/test/keystrokes/firefox/html_struct_nav_containers.py +++ b/test/keystrokes/firefox/html_struct_nav_containers.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/html_struct_nav_descriptions.py b/test/keystrokes/firefox/html_struct_nav_descriptions.py index 35fb9d3..2c7df0d 100644 --- a/test/keystrokes/firefox/html_struct_nav_descriptions.py +++ b/test/keystrokes/firefox/html_struct_nav_descriptions.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of structural navigation.""" diff --git a/test/keystrokes/firefox/html_struct_nav_heading_empty.py b/test/keystrokes/firefox/html_struct_nav_heading_empty.py index d920e8b..ecb20cb 100644 --- a/test/keystrokes/firefox/html_struct_nav_heading_empty.py +++ b/test/keystrokes/firefox/html_struct_nav_heading_empty.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of structural navigation by heading.""" diff --git a/test/keystrokes/firefox/html_struct_nav_heading_in_div_with_text.py b/test/keystrokes/firefox/html_struct_nav_heading_in_div_with_text.py index bfdd049..49e7abb 100644 --- a/test/keystrokes/firefox/html_struct_nav_heading_in_div_with_text.py +++ b/test/keystrokes/firefox/html_struct_nav_heading_in_div_with_text.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of structural navigation by heading.""" diff --git a/test/keystrokes/firefox/html_struct_nav_heading_with_child_text.py b/test/keystrokes/firefox/html_struct_nav_heading_with_child_text.py index d53227a..225ca12 100644 --- a/test/keystrokes/firefox/html_struct_nav_heading_with_child_text.py +++ b/test/keystrokes/firefox/html_struct_nav_heading_with_child_text.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of structural navigation by heading.""" diff --git a/test/keystrokes/firefox/html_struct_nav_heading_with_clickable.py b/test/keystrokes/firefox/html_struct_nav_heading_with_clickable.py index e243dc8..52d7ac6 100644 --- a/test/keystrokes/firefox/html_struct_nav_heading_with_clickable.py +++ b/test/keystrokes/firefox/html_struct_nav_heading_with_clickable.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of structural navigation by heading.""" diff --git a/test/keystrokes/firefox/html_struct_nav_headings_buried_deep.py b/test/keystrokes/firefox/html_struct_nav_headings_buried_deep.py index 831fe22..226d75f 100644 --- a/test/keystrokes/firefox/html_struct_nav_headings_buried_deep.py +++ b/test/keystrokes/firefox/html_struct_nav_headings_buried_deep.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of structural navigation by heading.""" diff --git a/test/keystrokes/firefox/html_struct_nav_headings_with_hidden_anchors.py b/test/keystrokes/firefox/html_struct_nav_headings_with_hidden_anchors.py index 413bbfe..fbf2bd9 100644 --- a/test/keystrokes/firefox/html_struct_nav_headings_with_hidden_anchors.py +++ b/test/keystrokes/firefox/html_struct_nav_headings_with_hidden_anchors.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of structural navigation by heading.""" diff --git a/test/keystrokes/firefox/html_struct_nav_hidden_paragraphs.py b/test/keystrokes/firefox/html_struct_nav_hidden_paragraphs.py index 5bffe1b..4e62e57 100644 --- a/test/keystrokes/firefox/html_struct_nav_hidden_paragraphs.py +++ b/test/keystrokes/firefox/html_struct_nav_hidden_paragraphs.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of structural navigation by paragraph with some paragraphs hidden.""" diff --git a/test/keystrokes/firefox/html_struct_nav_large_obj.py b/test/keystrokes/firefox/html_struct_nav_large_obj.py index 8f086f6..203122d 100644 --- a/test/keystrokes/firefox/html_struct_nav_large_obj.py +++ b/test/keystrokes/firefox/html_struct_nav_large_obj.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of structural navigation amongst 'large objects'.""" diff --git a/test/keystrokes/firefox/html_struct_nav_link_with_child_text.py b/test/keystrokes/firefox/html_struct_nav_link_with_child_text.py index a7bc718..2f45c34 100644 --- a/test/keystrokes/firefox/html_struct_nav_link_with_child_text.py +++ b/test/keystrokes/firefox/html_struct_nav_link_with_child_text.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/html_struct_nav_links.py b/test/keystrokes/firefox/html_struct_nav_links.py index f33fcfa..cd36de8 100644 --- a/test/keystrokes/firefox/html_struct_nav_links.py +++ b/test/keystrokes/firefox/html_struct_nav_links.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of structural navigation amongst links.""" diff --git a/test/keystrokes/firefox/html_struct_nav_list_item.py b/test/keystrokes/firefox/html_struct_nav_list_item.py index d20fb56..6d0d7ba 100644 --- a/test/keystrokes/firefox/html_struct_nav_list_item.py +++ b/test/keystrokes/firefox/html_struct_nav_list_item.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of structural navigation amongst list items.""" diff --git a/test/keystrokes/firefox/html_struct_nav_lists.py b/test/keystrokes/firefox/html_struct_nav_lists.py index bd92b17..cbc5d7a 100644 --- a/test/keystrokes/firefox/html_struct_nav_lists.py +++ b/test/keystrokes/firefox/html_struct_nav_lists.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of structural navigation amongst lists.""" diff --git a/test/keystrokes/firefox/label_inference_bug_546815.py b/test/keystrokes/firefox/label_inference_bug_546815.py index d86b2a5..17b7c39 100644 --- a/test/keystrokes/firefox/label_inference_bug_546815.py +++ b/test/keystrokes/firefox/label_inference_bug_546815.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of label guess functionality.""" diff --git a/test/keystrokes/firefox/label_inference_bugzilla_search.py b/test/keystrokes/firefox/label_inference_bugzilla_search.py index 1f5b7b5..4cfc3d8 100644 --- a/test/keystrokes/firefox/label_inference_bugzilla_search.py +++ b/test/keystrokes/firefox/label_inference_bugzilla_search.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of label guess for bugzilla's advanced search page.""" diff --git a/test/keystrokes/firefox/label_inference_entries.py b/test/keystrokes/firefox/label_inference_entries.py index c1484a9..56c64cb 100644 --- a/test/keystrokes/firefox/label_inference_entries.py +++ b/test/keystrokes/firefox/label_inference_entries.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/label_inference_labels_without_for_far_away.py b/test/keystrokes/firefox/label_inference_labels_without_for_far_away.py index 37d993e..76f7cd1 100644 --- a/test/keystrokes/firefox/label_inference_labels_without_for_far_away.py +++ b/test/keystrokes/firefox/label_inference_labels_without_for_far_away.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/label_inference_mailman.py b/test/keystrokes/firefox/label_inference_mailman.py index 21655a4..7aeafb7 100644 --- a/test/keystrokes/firefox/label_inference_mailman.py +++ b/test/keystrokes/firefox/label_inference_mailman.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of label guess functionality.""" diff --git a/test/keystrokes/firefox/line_nav_aria_landmarks.py b/test/keystrokes/firefox/line_nav_aria_landmarks.py index fb6342e..6118983 100644 --- a/test/keystrokes/firefox/line_nav_aria_landmarks.py +++ b/test/keystrokes/firefox/line_nav_aria_landmarks.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_aria_landmarks_no_context.py b/test/keystrokes/firefox/line_nav_aria_landmarks_no_context.py index 53443d3..5ca9f42 100644 --- a/test/keystrokes/firefox/line_nav_aria_landmarks_no_context.py +++ b/test/keystrokes/firefox/line_nav_aria_landmarks_no_context.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_broken_list.py b/test/keystrokes/firefox/line_nav_broken_list.py index 06d6ebb..ad5a799 100644 --- a/test/keystrokes/firefox/line_nav_broken_list.py +++ b/test/keystrokes/firefox/line_nav_broken_list.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/line_nav_bug_546815.py b/test/keystrokes/firefox/line_nav_bug_546815.py index cacfcb1..543e1af 100644 --- a/test/keystrokes/firefox/line_nav_bug_546815.py +++ b/test/keystrokes/firefox/line_nav_bug_546815.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_bug_549128.py b/test/keystrokes/firefox/line_nav_bug_549128.py index 43364b4..eaf7438 100644 --- a/test/keystrokes/firefox/line_nav_bug_549128.py +++ b/test/keystrokes/firefox/line_nav_bug_549128.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_bug_552887a.py b/test/keystrokes/firefox/line_nav_bug_552887a.py index f30383d..765d595 100644 --- a/test/keystrokes/firefox/line_nav_bug_552887a.py +++ b/test/keystrokes/firefox/line_nav_bug_552887a.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of the fix for one of the two issues in bug 552887.""" diff --git a/test/keystrokes/firefox/line_nav_bug_554616.py b/test/keystrokes/firefox/line_nav_bug_554616.py index 5fccc81..428d09c 100644 --- a/test/keystrokes/firefox/line_nav_bug_554616.py +++ b/test/keystrokes/firefox/line_nav_bug_554616.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output.""" diff --git a/test/keystrokes/firefox/line_nav_bug_555055.py b/test/keystrokes/firefox/line_nav_bug_555055.py index cf1d6f6..73fb916 100644 --- a/test/keystrokes/firefox/line_nav_bug_555055.py +++ b/test/keystrokes/firefox/line_nav_bug_555055.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox. """ diff --git a/test/keystrokes/firefox/line_nav_bug_570757.py b/test/keystrokes/firefox/line_nav_bug_570757.py index c9b684a..ee2efdc 100644 --- a/test/keystrokes/firefox/line_nav_bug_570757.py +++ b/test/keystrokes/firefox/line_nav_bug_570757.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_bug_570757_no_context.py b/test/keystrokes/firefox/line_nav_bug_570757_no_context.py index a48a797..2b0fd17 100644 --- a/test/keystrokes/firefox/line_nav_bug_570757_no_context.py +++ b/test/keystrokes/firefox/line_nav_bug_570757_no_context.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_bug_577239.py b/test/keystrokes/firefox/line_nav_bug_577239.py index ae0e58b..0785032 100644 --- a/test/keystrokes/firefox/line_nav_bug_577239.py +++ b/test/keystrokes/firefox/line_nav_bug_577239.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_bug_577239_no_context.py b/test/keystrokes/firefox/line_nav_bug_577239_no_context.py index 511923b..3a204e9 100644 --- a/test/keystrokes/firefox/line_nav_bug_577239_no_context.py +++ b/test/keystrokes/firefox/line_nav_bug_577239_no_context.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_bug_592383.py b/test/keystrokes/firefox/line_nav_bug_592383.py index b14c344..1920e34 100644 --- a/test/keystrokes/firefox/line_nav_bug_592383.py +++ b/test/keystrokes/firefox/line_nav_bug_592383.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of navigation given a paragraph with a multi-line-high initial char.""" diff --git a/test/keystrokes/firefox/line_nav_bugzilla_search_down.py b/test/keystrokes/firefox/line_nav_bugzilla_search_down.py index 49f1a41..8365acd 100644 --- a/test/keystrokes/firefox/line_nav_bugzilla_search_down.py +++ b/test/keystrokes/firefox/line_nav_bugzilla_search_down.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation.""" diff --git a/test/keystrokes/firefox/line_nav_bugzilla_search_up.py b/test/keystrokes/firefox/line_nav_bugzilla_search_up.py index 0a2ef80..0b0fa00 100644 --- a/test/keystrokes/firefox/line_nav_bugzilla_search_up.py +++ b/test/keystrokes/firefox/line_nav_bugzilla_search_up.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation.""" diff --git a/test/keystrokes/firefox/line_nav_button_in_link_position_relative_on_focus.py b/test/keystrokes/firefox/line_nav_button_in_link_position_relative_on_focus.py index 25b8608..31c5356 100644 --- a/test/keystrokes/firefox/line_nav_button_in_link_position_relative_on_focus.py +++ b/test/keystrokes/firefox/line_nav_button_in_link_position_relative_on_focus.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/line_nav_canvas.py b/test/keystrokes/firefox/line_nav_canvas.py index 53b5c6b..773db87 100644 --- a/test/keystrokes/firefox/line_nav_canvas.py +++ b/test/keystrokes/firefox/line_nav_canvas.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/line_nav_clickables.py b/test/keystrokes/firefox/line_nav_clickables.py index c6ecb77..b05bb18 100644 --- a/test/keystrokes/firefox/line_nav_clickables.py +++ b/test/keystrokes/firefox/line_nav_clickables.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_descriptions.py b/test/keystrokes/firefox/line_nav_descriptions.py index cade940..759a686 100644 --- a/test/keystrokes/firefox/line_nav_descriptions.py +++ b/test/keystrokes/firefox/line_nav_descriptions.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_display_table_cell.py b/test/keystrokes/firefox/line_nav_display_table_cell.py index 34f96a7..f99863d 100644 --- a/test/keystrokes/firefox/line_nav_display_table_cell.py +++ b/test/keystrokes/firefox/line_nav_display_table_cell.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/line_nav_emoji.py b/test/keystrokes/firefox/line_nav_emoji.py index 824aa18..8f1dfbe 100644 --- a/test/keystrokes/firefox/line_nav_emoji.py +++ b/test/keystrokes/firefox/line_nav_emoji.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_empty_anchor.py b/test/keystrokes/firefox/line_nav_empty_anchor.py index f440eac..cbe9aab 100644 --- a/test/keystrokes/firefox/line_nav_empty_anchor.py +++ b/test/keystrokes/firefox/line_nav_empty_anchor.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation on a page with empty anchors.""" diff --git a/test/keystrokes/firefox/line_nav_empty_block_link.py b/test/keystrokes/firefox/line_nav_empty_block_link.py index 34f96a7..f99863d 100644 --- a/test/keystrokes/firefox/line_nav_empty_block_link.py +++ b/test/keystrokes/firefox/line_nav_empty_block_link.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/line_nav_empty_link_with_line_break.py b/test/keystrokes/firefox/line_nav_empty_link_with_line_break.py index 01afb14..b02ac2a 100644 --- a/test/keystrokes/firefox/line_nav_empty_link_with_line_break.py +++ b/test/keystrokes/firefox/line_nav_empty_link_with_line_break.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/line_nav_empty_textarea.py b/test/keystrokes/firefox/line_nav_empty_textarea.py index 9a5dc16..a2dcbbf 100644 --- a/test/keystrokes/firefox/line_nav_empty_textarea.py +++ b/test/keystrokes/firefox/line_nav_empty_textarea.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation.""" diff --git a/test/keystrokes/firefox/line_nav_enter_bug.py b/test/keystrokes/firefox/line_nav_enter_bug.py index 89a697b..4637e98 100644 --- a/test/keystrokes/firefox/line_nav_enter_bug.py +++ b/test/keystrokes/firefox/line_nav_enter_bug.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation.""" diff --git a/test/keystrokes/firefox/line_nav_entries.py b/test/keystrokes/firefox/line_nav_entries.py index 8c1e307..752aa3c 100644 --- a/test/keystrokes/firefox/line_nav_entries.py +++ b/test/keystrokes/firefox/line_nav_entries.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation.""" diff --git a/test/keystrokes/firefox/line_nav_focused_link.py b/test/keystrokes/firefox/line_nav_focused_link.py index a7152a7..d24243f 100644 --- a/test/keystrokes/firefox/line_nav_focused_link.py +++ b/test/keystrokes/firefox/line_nav_focused_link.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation.""" diff --git a/test/keystrokes/firefox/line_nav_follow_same_page_link.py b/test/keystrokes/firefox/line_nav_follow_same_page_link.py index 410516f..2968f12 100644 --- a/test/keystrokes/firefox/line_nav_follow_same_page_link.py +++ b/test/keystrokes/firefox/line_nav_follow_same_page_link.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of navigation to same page links.""" diff --git a/test/keystrokes/firefox/line_nav_follow_same_page_link_2.py b/test/keystrokes/firefox/line_nav_follow_same_page_link_2.py index 10708b8..de13732 100644 --- a/test/keystrokes/firefox/line_nav_follow_same_page_link_2.py +++ b/test/keystrokes/firefox/line_nav_follow_same_page_link_2.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of navigation by same-page links on the Cthulhu wiki.""" diff --git a/test/keystrokes/firefox/line_nav_follow_same_page_link_3.py b/test/keystrokes/firefox/line_nav_follow_same_page_link_3.py index aed3e19..1a2e02a 100644 --- a/test/keystrokes/firefox/line_nav_follow_same_page_link_3.py +++ b/test/keystrokes/firefox/line_nav_follow_same_page_link_3.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line nav after loading a same-page link.""" diff --git a/test/keystrokes/firefox/line_nav_fontawesome_link.py b/test/keystrokes/firefox/line_nav_fontawesome_link.py index 8e64acc..f91b00e 100644 --- a/test/keystrokes/firefox/line_nav_fontawesome_link.py +++ b/test/keystrokes/firefox/line_nav_fontawesome_link.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/line_nav_heading_section.py b/test/keystrokes/firefox/line_nav_heading_section.py index 61ca1af..ed9978f 100644 --- a/test/keystrokes/firefox/line_nav_heading_section.py +++ b/test/keystrokes/firefox/line_nav_heading_section.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation on a page with headings in sections.""" diff --git a/test/keystrokes/firefox/line_nav_hidden_buttons.py b/test/keystrokes/firefox/line_nav_hidden_buttons.py index 8830b94..d396860 100644 --- a/test/keystrokes/firefox/line_nav_hidden_buttons.py +++ b/test/keystrokes/firefox/line_nav_hidden_buttons.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_hidden_elements.py b/test/keystrokes/firefox/line_nav_hidden_elements.py index d5552f1..996bde0 100644 --- a/test/keystrokes/firefox/line_nav_hidden_elements.py +++ b/test/keystrokes/firefox/line_nav_hidden_elements.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_hidden_float.py b/test/keystrokes/firefox/line_nav_hidden_float.py index 2cacdfb..7262ca5 100644 --- a/test/keystrokes/firefox/line_nav_hidden_float.py +++ b/test/keystrokes/firefox/line_nav_hidden_float.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_hidden_label.py b/test/keystrokes/firefox/line_nav_hidden_label.py index b6a9e49..184b206 100644 --- a/test/keystrokes/firefox/line_nav_hidden_label.py +++ b/test/keystrokes/firefox/line_nav_hidden_label.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_hidden_links.py b/test/keystrokes/firefox/line_nav_hidden_links.py index 3357651..518fa6d 100644 --- a/test/keystrokes/firefox/line_nav_hidden_links.py +++ b/test/keystrokes/firefox/line_nav_hidden_links.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_iframes_blogger.py b/test/keystrokes/firefox/line_nav_iframes_blogger.py index 0da3c2c..c72cb6b 100644 --- a/test/keystrokes/firefox/line_nav_iframes_blogger.py +++ b/test/keystrokes/firefox/line_nav_iframes_blogger.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_iframes_in_inline_block.py b/test/keystrokes/firefox/line_nav_iframes_in_inline_block.py index e78fb52..b103277 100644 --- a/test/keystrokes/firefox/line_nav_iframes_in_inline_block.py +++ b/test/keystrokes/firefox/line_nav_iframes_in_inline_block.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_iframes_in_inline_block2.py b/test/keystrokes/firefox/line_nav_iframes_in_inline_block2.py index 4507d8a..a8c503c 100644 --- a/test/keystrokes/firefox/line_nav_iframes_in_inline_block2.py +++ b/test/keystrokes/firefox/line_nav_iframes_in_inline_block2.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_iframes_nested.py b/test/keystrokes/firefox/line_nav_iframes_nested.py index f045a01..00c8cad 100644 --- a/test/keystrokes/firefox/line_nav_iframes_nested.py +++ b/test/keystrokes/firefox/line_nav_iframes_nested.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_image_in_link.py b/test/keystrokes/firefox/line_nav_image_in_link.py index 491d433..87c3e6e 100644 --- a/test/keystrokes/firefox/line_nav_image_in_link.py +++ b/test/keystrokes/firefox/line_nav_image_in_link.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_imagemap.py b/test/keystrokes/firefox/line_nav_imagemap.py index 480db0e..100c8ef 100644 --- a/test/keystrokes/firefox/line_nav_imagemap.py +++ b/test/keystrokes/firefox/line_nav_imagemap.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation on a page with an imagemap.""" diff --git a/test/keystrokes/firefox/line_nav_images_in_links.py b/test/keystrokes/firefox/line_nav_images_in_links.py index 4e14bc9..fbecdc5 100644 --- a/test/keystrokes/firefox/line_nav_images_in_links.py +++ b/test/keystrokes/firefox/line_nav_images_in_links.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of presentation of links that contain images.""" diff --git a/test/keystrokes/firefox/line_nav_images_in_table_and_floating_div.py b/test/keystrokes/firefox/line_nav_images_in_table_and_floating_div.py index d2fad5e..70e01d3 100644 --- a/test/keystrokes/firefox/line_nav_images_in_table_and_floating_div.py +++ b/test/keystrokes/firefox/line_nav_images_in_table_and_floating_div.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of presentation of lines that contain images.""" diff --git a/test/keystrokes/firefox/line_nav_inline_block_spans.py b/test/keystrokes/firefox/line_nav_inline_block_spans.py index 096aabb..af8b1a0 100644 --- a/test/keystrokes/firefox/line_nav_inline_block_spans.py +++ b/test/keystrokes/firefox/line_nav_inline_block_spans.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_link_position_relative_on_focus.py b/test/keystrokes/firefox/line_nav_link_position_relative_on_focus.py index 783f164..51a675f 100644 --- a/test/keystrokes/firefox/line_nav_link_position_relative_on_focus.py +++ b/test/keystrokes/firefox/line_nav_link_position_relative_on_focus.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/line_nav_list_with_anchors_and_hyphens.py b/test/keystrokes/firefox/line_nav_list_with_anchors_and_hyphens.py index f356ca8..53c1b2a 100644 --- a/test/keystrokes/firefox/line_nav_list_with_anchors_and_hyphens.py +++ b/test/keystrokes/firefox/line_nav_list_with_anchors_and_hyphens.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation on a page with multi-line cells and sections.""" diff --git a/test/keystrokes/firefox/line_nav_lists.py b/test/keystrokes/firefox/line_nav_lists.py index 336b2b0..429f0bf 100644 --- a/test/keystrokes/firefox/line_nav_lists.py +++ b/test/keystrokes/firefox/line_nav_lists.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of HTML list presentation.""" diff --git a/test/keystrokes/firefox/line_nav_lists_broken.py b/test/keystrokes/firefox/line_nav_lists_broken.py index 38ebdb4..fbb5b21 100644 --- a/test/keystrokes/firefox/line_nav_lists_broken.py +++ b/test/keystrokes/firefox/line_nav_lists_broken.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/line_nav_lists_without_items.py b/test/keystrokes/firefox/line_nav_lists_without_items.py index a849f2f..fbd0861 100644 --- a/test/keystrokes/firefox/line_nav_lists_without_items.py +++ b/test/keystrokes/firefox/line_nav_lists_without_items.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/line_nav_multi_line_text.py b/test/keystrokes/firefox/line_nav_multi_line_text.py index a531805..553a92c 100644 --- a/test/keystrokes/firefox/line_nav_multi_line_text.py +++ b/test/keystrokes/firefox/line_nav_multi_line_text.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation on a page with multi-line cells and sections.""" diff --git a/test/keystrokes/firefox/line_nav_nested_items.py b/test/keystrokes/firefox/line_nav_nested_items.py index dfb83c8..a564626 100644 --- a/test/keystrokes/firefox/line_nav_nested_items.py +++ b/test/keystrokes/firefox/line_nav_nested_items.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_nested_items_no_context.py b/test/keystrokes/firefox/line_nav_nested_items_no_context.py index d066b3b..44b922d 100644 --- a/test/keystrokes/firefox/line_nav_nested_items_no_context.py +++ b/test/keystrokes/firefox/line_nav_nested_items_no_context.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_nested_tables.py b/test/keystrokes/firefox/line_nav_nested_tables.py index 59df6ff..1e5bf49 100644 --- a/test/keystrokes/firefox/line_nav_nested_tables.py +++ b/test/keystrokes/firefox/line_nav_nested_tables.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation on a page with nested layout tables. """ diff --git a/test/keystrokes/firefox/line_nav_offscreen_text_with_tiny_width.py b/test/keystrokes/firefox/line_nav_offscreen_text_with_tiny_width.py index 1c831ff..1da1ecf 100644 --- a/test/keystrokes/firefox/line_nav_offscreen_text_with_tiny_width.py +++ b/test/keystrokes/firefox/line_nav_offscreen_text_with_tiny_width.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_paragraphs_in_links.py b/test/keystrokes/firefox/line_nav_paragraphs_in_links.py index 437abc8..20c5b06 100644 --- a/test/keystrokes/firefox/line_nav_paragraphs_in_links.py +++ b/test/keystrokes/firefox/line_nav_paragraphs_in_links.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation.""" diff --git a/test/keystrokes/firefox/line_nav_pre_lines.py b/test/keystrokes/firefox/line_nav_pre_lines.py index f59fbe1..90eb909 100644 --- a/test/keystrokes/firefox/line_nav_pre_lines.py +++ b/test/keystrokes/firefox/line_nav_pre_lines.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_pre_links.py b/test/keystrokes/firefox/line_nav_pre_links.py index 6a21da9..e458302 100644 --- a/test/keystrokes/firefox/line_nav_pre_links.py +++ b/test/keystrokes/firefox/line_nav_pre_links.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_regions_and_fieldsets.py b/test/keystrokes/firefox/line_nav_regions_and_fieldsets.py index ddc8c45..e4f6e88 100644 --- a/test/keystrokes/firefox/line_nav_regions_and_fieldsets.py +++ b/test/keystrokes/firefox/line_nav_regions_and_fieldsets.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_regions_and_fieldsets_no_context.py b/test/keystrokes/firefox/line_nav_regions_and_fieldsets_no_context.py index cbbe8b3..461d08a 100644 --- a/test/keystrokes/firefox/line_nav_regions_and_fieldsets_no_context.py +++ b/test/keystrokes/firefox/line_nav_regions_and_fieldsets_no_context.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_role_application.py b/test/keystrokes/firefox/line_nav_role_application.py index c45779e..ec1ade0 100644 --- a/test/keystrokes/firefox/line_nav_role_application.py +++ b/test/keystrokes/firefox/line_nav_role_application.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_roledescriptions.py b/test/keystrokes/firefox/line_nav_roledescriptions.py index a7d2d61..041bc37 100644 --- a/test/keystrokes/firefox/line_nav_roledescriptions.py +++ b/test/keystrokes/firefox/line_nav_roledescriptions.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/line_nav_simple_form.py b/test/keystrokes/firefox/line_nav_simple_form.py index 7cb9fa9..cb033c2 100644 --- a/test/keystrokes/firefox/line_nav_simple_form.py +++ b/test/keystrokes/firefox/line_nav_simple_form.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation on a page with a simple form.""" diff --git a/test/keystrokes/firefox/line_nav_slash_test.py b/test/keystrokes/firefox/line_nav_slash_test.py index fcd79e8..adda97e 100644 --- a/test/keystrokes/firefox/line_nav_slash_test.py +++ b/test/keystrokes/firefox/line_nav_slash_test.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/line_nav_sun_java.py b/test/keystrokes/firefox/line_nav_sun_java.py index 592b203..d5eddf1 100644 --- a/test/keystrokes/firefox/line_nav_sun_java.py +++ b/test/keystrokes/firefox/line_nav_sun_java.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation.""" diff --git a/test/keystrokes/firefox/line_nav_table_captions.py b/test/keystrokes/firefox/line_nav_table_captions.py index c365228..c6d86eb 100644 --- a/test/keystrokes/firefox/line_nav_table_captions.py +++ b/test/keystrokes/firefox/line_nav_table_captions.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation in a table with a caption.""" diff --git a/test/keystrokes/firefox/line_nav_table_cell_links.py b/test/keystrokes/firefox/line_nav_table_cell_links.py index 4163476..1adbd3e 100644 --- a/test/keystrokes/firefox/line_nav_table_cell_links.py +++ b/test/keystrokes/firefox/line_nav_table_cell_links.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation with links in a cell with line breaks.""" diff --git a/test/keystrokes/firefox/line_nav_textarea_last_line.py b/test/keystrokes/firefox/line_nav_textarea_last_line.py index 802a29c..698cc06 100644 --- a/test/keystrokes/firefox/line_nav_textarea_last_line.py +++ b/test/keystrokes/firefox/line_nav_textarea_last_line.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation.""" diff --git a/test/keystrokes/firefox/line_nav_twitter_bug.py b/test/keystrokes/firefox/line_nav_twitter_bug.py index c50a951..c976980 100644 --- a/test/keystrokes/firefox/line_nav_twitter_bug.py +++ b/test/keystrokes/firefox/line_nav_twitter_bug.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation.""" diff --git a/test/keystrokes/firefox/line_nav_wiki_down.py b/test/keystrokes/firefox/line_nav_wiki_down.py index c5c0145..2f46695 100644 --- a/test/keystrokes/firefox/line_nav_wiki_down.py +++ b/test/keystrokes/firefox/line_nav_wiki_down.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation.""" @@ -815,11 +815,11 @@ sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) sequence.append(utils.AssertPresentationAction( "79. Line Down", - ["BRAILLE LINE: '• Mailing list: cthulhu-list@gnome.org (Archives)'", + ["BRAILLE LINE: '• Mailing list: https://groups.io/g/stormux (Archives)'", " VISIBLE: '• Mailing list: cthulhu-list@gnome.', cursor=1", "SPEECH OUTPUT: '•.'", "SPEECH OUTPUT: 'Mailing list:'", - "SPEECH OUTPUT: 'cthulhu-list@gnome.org'", + "SPEECH OUTPUT: 'https://groups.io/g/stormux'", "SPEECH OUTPUT: 'link.'", "SPEECH OUTPUT: '('", "SPEECH OUTPUT: 'Archives'", diff --git a/test/keystrokes/firefox/line_nav_wiki_up.py b/test/keystrokes/firefox/line_nav_wiki_up.py index 32aa417..2fda80d 100644 --- a/test/keystrokes/firefox/line_nav_wiki_up.py +++ b/test/keystrokes/firefox/line_nav_wiki_up.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox on the Cthulhu wiki.""" @@ -268,11 +268,11 @@ sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Up")) sequence.append(utils.AssertPresentationAction( "24. Line Up", - ["BRAILLE LINE: '• Mailing list: cthulhu-list@gnome.org (Archives)'", + ["BRAILLE LINE: '• Mailing list: https://groups.io/g/stormux (Archives)'", " VISIBLE: '• Mailing list: cthulhu-list@gnome.', cursor=1", "SPEECH OUTPUT: '•.'", "SPEECH OUTPUT: 'Mailing list:'", - "SPEECH OUTPUT: 'cthulhu-list@gnome.org'", + "SPEECH OUTPUT: 'https://groups.io/g/stormux'", "SPEECH OUTPUT: 'link.'", "SPEECH OUTPUT: '('", "SPEECH OUTPUT: 'Archives'", diff --git a/test/keystrokes/firefox/longdesc_1.py b/test/keystrokes/firefox/longdesc_1.py index dcc9afa..d58fb2b 100644 --- a/test/keystrokes/firefox/longdesc_1.py +++ b/test/keystrokes/firefox/longdesc_1.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/longdesc_10.py b/test/keystrokes/firefox/longdesc_10.py index cc25dfc..c16c65e 100644 --- a/test/keystrokes/firefox/longdesc_10.py +++ b/test/keystrokes/firefox/longdesc_10.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/longdesc_11.py b/test/keystrokes/firefox/longdesc_11.py index 7124087..df87c6d 100644 --- a/test/keystrokes/firefox/longdesc_11.py +++ b/test/keystrokes/firefox/longdesc_11.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/longdesc_12.py b/test/keystrokes/firefox/longdesc_12.py index c6802a1..b0dcdf8 100644 --- a/test/keystrokes/firefox/longdesc_12.py +++ b/test/keystrokes/firefox/longdesc_12.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/longdesc_13.py b/test/keystrokes/firefox/longdesc_13.py index cadedb7..1855016 100644 --- a/test/keystrokes/firefox/longdesc_13.py +++ b/test/keystrokes/firefox/longdesc_13.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/longdesc_14.py b/test/keystrokes/firefox/longdesc_14.py index dcc9afa..d58fb2b 100644 --- a/test/keystrokes/firefox/longdesc_14.py +++ b/test/keystrokes/firefox/longdesc_14.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/longdesc_15.py b/test/keystrokes/firefox/longdesc_15.py index dcc9afa..d58fb2b 100644 --- a/test/keystrokes/firefox/longdesc_15.py +++ b/test/keystrokes/firefox/longdesc_15.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/longdesc_2.py b/test/keystrokes/firefox/longdesc_2.py index dcc9afa..d58fb2b 100644 --- a/test/keystrokes/firefox/longdesc_2.py +++ b/test/keystrokes/firefox/longdesc_2.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/longdesc_3.py b/test/keystrokes/firefox/longdesc_3.py index 18e1e7f..1c037f3 100644 --- a/test/keystrokes/firefox/longdesc_3.py +++ b/test/keystrokes/firefox/longdesc_3.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/longdesc_4.py b/test/keystrokes/firefox/longdesc_4.py index e614fd0..613a789 100644 --- a/test/keystrokes/firefox/longdesc_4.py +++ b/test/keystrokes/firefox/longdesc_4.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/longdesc_5.py b/test/keystrokes/firefox/longdesc_5.py index e614fd0..613a789 100644 --- a/test/keystrokes/firefox/longdesc_5.py +++ b/test/keystrokes/firefox/longdesc_5.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/longdesc_6.py b/test/keystrokes/firefox/longdesc_6.py index e614fd0..613a789 100644 --- a/test/keystrokes/firefox/longdesc_6.py +++ b/test/keystrokes/firefox/longdesc_6.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/longdesc_7.py b/test/keystrokes/firefox/longdesc_7.py index 18e1e7f..1c037f3 100644 --- a/test/keystrokes/firefox/longdesc_7.py +++ b/test/keystrokes/firefox/longdesc_7.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/longdesc_8.py b/test/keystrokes/firefox/longdesc_8.py index 7124087..df87c6d 100644 --- a/test/keystrokes/firefox/longdesc_8.py +++ b/test/keystrokes/firefox/longdesc_8.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/longdesc_9.py b/test/keystrokes/firefox/longdesc_9.py index 18e1e7f..1c037f3 100644 --- a/test/keystrokes/firefox/longdesc_9.py +++ b/test/keystrokes/firefox/longdesc_9.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/math_line_nav_fraction.py b/test/keystrokes/firefox/math_line_nav_fraction.py index 4256e52..db6a468 100644 --- a/test/keystrokes/firefox/math_line_nav_fraction.py +++ b/test/keystrokes/firefox/math_line_nav_fraction.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/math_line_nav_math_in_dialog.py b/test/keystrokes/firefox/math_line_nav_math_in_dialog.py index fb541e7..ed3c5b2 100644 --- a/test/keystrokes/firefox/math_line_nav_math_in_dialog.py +++ b/test/keystrokes/firefox/math_line_nav_math_in_dialog.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/math_line_nav_mathvariant.py b/test/keystrokes/firefox/math_line_nav_mathvariant.py index 923aab0..eb49599 100644 --- a/test/keystrokes/firefox/math_line_nav_mathvariant.py +++ b/test/keystrokes/firefox/math_line_nav_mathvariant.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/math_line_nav_menclose.py b/test/keystrokes/firefox/math_line_nav_menclose.py index a61e121..535944b 100644 --- a/test/keystrokes/firefox/math_line_nav_menclose.py +++ b/test/keystrokes/firefox/math_line_nav_menclose.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/math_line_nav_mfenced.py b/test/keystrokes/firefox/math_line_nav_mfenced.py index c4b0f10..b6c54f2 100644 --- a/test/keystrokes/firefox/math_line_nav_mfenced.py +++ b/test/keystrokes/firefox/math_line_nav_mfenced.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/math_line_nav_mroot.py b/test/keystrokes/firefox/math_line_nav_mroot.py index 20dfaaa..c3aa172 100644 --- a/test/keystrokes/firefox/math_line_nav_mroot.py +++ b/test/keystrokes/firefox/math_line_nav_mroot.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/math_line_nav_mrow.py b/test/keystrokes/firefox/math_line_nav_mrow.py index e1a1b7d..6621a44 100644 --- a/test/keystrokes/firefox/math_line_nav_mrow.py +++ b/test/keystrokes/firefox/math_line_nav_mrow.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/math_line_nav_punctuation.py b/test/keystrokes/firefox/math_line_nav_punctuation.py index 2eb785c..0dd0703 100644 --- a/test/keystrokes/firefox/math_line_nav_punctuation.py +++ b/test/keystrokes/firefox/math_line_nav_punctuation.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/math_line_nav_scripts.py b/test/keystrokes/firefox/math_line_nav_scripts.py index 6eefe1c..3b5e4e1 100644 --- a/test/keystrokes/firefox/math_line_nav_scripts.py +++ b/test/keystrokes/firefox/math_line_nav_scripts.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/math_line_nav_table.py b/test/keystrokes/firefox/math_line_nav_table.py index b7d3d18..3e75388 100644 --- a/test/keystrokes/firefox/math_line_nav_table.py +++ b/test/keystrokes/firefox/math_line_nav_table.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/math_line_nav_tiny_mathml.py b/test/keystrokes/firefox/math_line_nav_tiny_mathml.py index 53ee20f..148c5a2 100644 --- a/test/keystrokes/firefox/math_line_nav_tiny_mathml.py +++ b/test/keystrokes/firefox/math_line_nav_tiny_mathml.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/math_line_nav_torture_test.py b/test/keystrokes/firefox/math_line_nav_torture_test.py index 5f2cf72..a816d23 100644 --- a/test/keystrokes/firefox/math_line_nav_torture_test.py +++ b/test/keystrokes/firefox/math_line_nav_torture_test.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/mouseover_javascript_alert.py b/test/keystrokes/firefox/mouseover_javascript_alert.py index 2aec6d9..935f4b4 100644 --- a/test/keystrokes/firefox/mouseover_javascript_alert.py +++ b/test/keystrokes/firefox/mouseover_javascript_alert.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of Cthulhu's support for mouseovers.""" diff --git a/test/keystrokes/firefox/object_nav_descriptions_down.py b/test/keystrokes/firefox/object_nav_descriptions_down.py index 9cbaaea..a58b5b5 100644 --- a/test/keystrokes/firefox/object_nav_descriptions_down.py +++ b/test/keystrokes/firefox/object_nav_descriptions_down.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/object_nav_descriptions_up.py b/test/keystrokes/firefox/object_nav_descriptions_up.py index 52d3105..5ba7cbd 100644 --- a/test/keystrokes/firefox/object_nav_descriptions_up.py +++ b/test/keystrokes/firefox/object_nav_descriptions_up.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/object_nav_link_in_quotes.py b/test/keystrokes/firefox/object_nav_link_in_quotes.py index 0d57b3f..75bc1ed 100644 --- a/test/keystrokes/firefox/object_nav_link_in_quotes.py +++ b/test/keystrokes/firefox/object_nav_link_in_quotes.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/object_nav_links_in_text.py b/test/keystrokes/firefox/object_nav_links_in_text.py index 4a41e33..f51e43b 100644 --- a/test/keystrokes/firefox/object_nav_links_in_text.py +++ b/test/keystrokes/firefox/object_nav_links_in_text.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of object navigation.""" diff --git a/test/keystrokes/firefox/object_nav_links_on_line.py b/test/keystrokes/firefox/object_nav_links_on_line.py index 71e8661..08cb06b 100644 --- a/test/keystrokes/firefox/object_nav_links_on_line.py +++ b/test/keystrokes/firefox/object_nav_links_on_line.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of object navigation.""" diff --git a/test/keystrokes/firefox/object_nav_simple_form_down.py b/test/keystrokes/firefox/object_nav_simple_form_down.py index e7fe3a3..a2a7c72 100644 --- a/test/keystrokes/firefox/object_nav_simple_form_down.py +++ b/test/keystrokes/firefox/object_nav_simple_form_down.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of object navigation.""" diff --git a/test/keystrokes/firefox/object_nav_simple_form_up.py b/test/keystrokes/firefox/object_nav_simple_form_up.py index c8052c5..6e47047 100644 --- a/test/keystrokes/firefox/object_nav_simple_form_up.py +++ b/test/keystrokes/firefox/object_nav_simple_form_up.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of object navigation.""" diff --git a/test/keystrokes/firefox/say_all_aria_landmarks_no_context.py b/test/keystrokes/firefox/say_all_aria_landmarks_no_context.py index d1fd80a..d33e658 100644 --- a/test/keystrokes/firefox/say_all_aria_landmarks_no_context.py +++ b/test/keystrokes/firefox/say_all_aria_landmarks_no_context.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" diff --git a/test/keystrokes/firefox/say_all_blockquote.py b/test/keystrokes/firefox/say_all_blockquote.py index 210f1da..200a3c8 100644 --- a/test/keystrokes/firefox/say_all_blockquote.py +++ b/test/keystrokes/firefox/say_all_blockquote.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" diff --git a/test/keystrokes/firefox/say_all_blockquote_no_context.py b/test/keystrokes/firefox/say_all_blockquote_no_context.py index 0faf4dd..ac7b635 100644 --- a/test/keystrokes/firefox/say_all_blockquote_no_context.py +++ b/test/keystrokes/firefox/say_all_blockquote_no_context.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" diff --git a/test/keystrokes/firefox/say_all_bug_511389.py b/test/keystrokes/firefox/say_all_bug_511389.py index 4ed539b..3a29dd5 100644 --- a/test/keystrokes/firefox/say_all_bug_511389.py +++ b/test/keystrokes/firefox/say_all_bug_511389.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" diff --git a/test/keystrokes/firefox/say_all_bug_591351_1.py b/test/keystrokes/firefox/say_all_bug_591351_1.py index 7f5156b..8dfb00f 100644 --- a/test/keystrokes/firefox/say_all_bug_591351_1.py +++ b/test/keystrokes/firefox/say_all_bug_591351_1.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll output.""" diff --git a/test/keystrokes/firefox/say_all_bugzilla_search.py b/test/keystrokes/firefox/say_all_bugzilla_search.py index 4d0506e..b06b446 100644 --- a/test/keystrokes/firefox/say_all_bugzilla_search.py +++ b/test/keystrokes/firefox/say_all_bugzilla_search.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" diff --git a/test/keystrokes/firefox/say_all_bugzilla_search_no_context.py b/test/keystrokes/firefox/say_all_bugzilla_search_no_context.py index 072c0e2..0920ea6 100644 --- a/test/keystrokes/firefox/say_all_bugzilla_search_no_context.py +++ b/test/keystrokes/firefox/say_all_bugzilla_search_no_context.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" diff --git a/test/keystrokes/firefox/say_all_empty_anchor.py b/test/keystrokes/firefox/say_all_empty_anchor.py index 9bba2be..1d455d5 100644 --- a/test/keystrokes/firefox/say_all_empty_anchor.py +++ b/test/keystrokes/firefox/say_all_empty_anchor.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" diff --git a/test/keystrokes/firefox/say_all_enter_bug.py b/test/keystrokes/firefox/say_all_enter_bug.py index 2221143..26a34dd 100644 --- a/test/keystrokes/firefox/say_all_enter_bug.py +++ b/test/keystrokes/firefox/say_all_enter_bug.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" diff --git a/test/keystrokes/firefox/say_all_entries.py b/test/keystrokes/firefox/say_all_entries.py index 0198d5a..6e88f56 100644 --- a/test/keystrokes/firefox/say_all_entries.py +++ b/test/keystrokes/firefox/say_all_entries.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" diff --git a/test/keystrokes/firefox/say_all_heading_section.py b/test/keystrokes/firefox/say_all_heading_section.py index d2580ed..ae6ea90 100644 --- a/test/keystrokes/firefox/say_all_heading_section.py +++ b/test/keystrokes/firefox/say_all_heading_section.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" diff --git a/test/keystrokes/firefox/say_all_hidden_elements.py b/test/keystrokes/firefox/say_all_hidden_elements.py index 5cc3426..9d510af 100644 --- a/test/keystrokes/firefox/say_all_hidden_elements.py +++ b/test/keystrokes/firefox/say_all_hidden_elements.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" diff --git a/test/keystrokes/firefox/say_all_imagemap.py b/test/keystrokes/firefox/say_all_imagemap.py index eb5d106..aa0ecf9 100644 --- a/test/keystrokes/firefox/say_all_imagemap.py +++ b/test/keystrokes/firefox/say_all_imagemap.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" diff --git a/test/keystrokes/firefox/say_all_multi_line_text.py b/test/keystrokes/firefox/say_all_multi_line_text.py index bed9f1f..28d52b3 100644 --- a/test/keystrokes/firefox/say_all_multi_line_text.py +++ b/test/keystrokes/firefox/say_all_multi_line_text.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" diff --git a/test/keystrokes/firefox/say_all_multi_line_text_no_context.py b/test/keystrokes/firefox/say_all_multi_line_text_no_context.py index 9b6aa68..31c1756 100644 --- a/test/keystrokes/firefox/say_all_multi_line_text_no_context.py +++ b/test/keystrokes/firefox/say_all_multi_line_text_no_context.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" diff --git a/test/keystrokes/firefox/say_all_nested_tables.py b/test/keystrokes/firefox/say_all_nested_tables.py index 238d48d..aabda1e 100644 --- a/test/keystrokes/firefox/say_all_nested_tables.py +++ b/test/keystrokes/firefox/say_all_nested_tables.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" diff --git a/test/keystrokes/firefox/say_all_nested_tables_no_context.py b/test/keystrokes/firefox/say_all_nested_tables_no_context.py index 238d48d..aabda1e 100644 --- a/test/keystrokes/firefox/say_all_nested_tables_no_context.py +++ b/test/keystrokes/firefox/say_all_nested_tables_no_context.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" diff --git a/test/keystrokes/firefox/say_all_onmouseup.py b/test/keystrokes/firefox/say_all_onmouseup.py index f49eccf..30b5697 100644 --- a/test/keystrokes/firefox/say_all_onmouseup.py +++ b/test/keystrokes/firefox/say_all_onmouseup.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" diff --git a/test/keystrokes/firefox/say_all_regions_and_fieldsets.py b/test/keystrokes/firefox/say_all_regions_and_fieldsets.py index 4633fb0..e9effa9 100644 --- a/test/keystrokes/firefox/say_all_regions_and_fieldsets.py +++ b/test/keystrokes/firefox/say_all_regions_and_fieldsets.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" diff --git a/test/keystrokes/firefox/say_all_regions_and_fieldsets_no_context.py b/test/keystrokes/firefox/say_all_regions_and_fieldsets_no_context.py index eb161cd..e93bdb0 100644 --- a/test/keystrokes/firefox/say_all_regions_and_fieldsets_no_context.py +++ b/test/keystrokes/firefox/say_all_regions_and_fieldsets_no_context.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" diff --git a/test/keystrokes/firefox/say_all_role_combo_box.py b/test/keystrokes/firefox/say_all_role_combo_box.py index a0168cc..015078b 100644 --- a/test/keystrokes/firefox/say_all_role_combo_box.py +++ b/test/keystrokes/firefox/say_all_role_combo_box.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" diff --git a/test/keystrokes/firefox/say_all_role_links.py b/test/keystrokes/firefox/say_all_role_links.py index 6c288b8..f0b0960 100644 --- a/test/keystrokes/firefox/say_all_role_links.py +++ b/test/keystrokes/firefox/say_all_role_links.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" diff --git a/test/keystrokes/firefox/say_all_role_links_no_context.py b/test/keystrokes/firefox/say_all_role_links_no_context.py index 093e445..4204370 100644 --- a/test/keystrokes/firefox/say_all_role_links_no_context.py +++ b/test/keystrokes/firefox/say_all_role_links_no_context.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" diff --git a/test/keystrokes/firefox/say_all_role_lists.py b/test/keystrokes/firefox/say_all_role_lists.py index d174135..7f376c4 100644 --- a/test/keystrokes/firefox/say_all_role_lists.py +++ b/test/keystrokes/firefox/say_all_role_lists.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" diff --git a/test/keystrokes/firefox/say_all_role_lists_no_context.py b/test/keystrokes/firefox/say_all_role_lists_no_context.py index e53a64b..f8ab589 100644 --- a/test/keystrokes/firefox/say_all_role_lists_no_context.py +++ b/test/keystrokes/firefox/say_all_role_lists_no_context.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" diff --git a/test/keystrokes/firefox/say_all_simple_form.py b/test/keystrokes/firefox/say_all_simple_form.py index 88f3535..7df9776 100644 --- a/test/keystrokes/firefox/say_all_simple_form.py +++ b/test/keystrokes/firefox/say_all_simple_form.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" diff --git a/test/keystrokes/firefox/say_all_table_caption.py b/test/keystrokes/firefox/say_all_table_caption.py index 2c7c216..dab79ac 100644 --- a/test/keystrokes/firefox/say_all_table_caption.py +++ b/test/keystrokes/firefox/say_all_table_caption.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" diff --git a/test/keystrokes/firefox/say_all_table_caption_no_context.py b/test/keystrokes/firefox/say_all_table_caption_no_context.py index f929d8f..46e91df 100644 --- a/test/keystrokes/firefox/say_all_table_caption_no_context.py +++ b/test/keystrokes/firefox/say_all_table_caption_no_context.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" diff --git a/test/keystrokes/firefox/say_all_table_cell_links.py b/test/keystrokes/firefox/say_all_table_cell_links.py index b82ef72..0f33a6e 100644 --- a/test/keystrokes/firefox/say_all_table_cell_links.py +++ b/test/keystrokes/firefox/say_all_table_cell_links.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" diff --git a/test/keystrokes/firefox/say_all_wiki.py b/test/keystrokes/firefox/say_all_wiki.py index 985c692..b484f36 100644 --- a/test/keystrokes/firefox/say_all_wiki.py +++ b/test/keystrokes/firefox/say_all_wiki.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" @@ -259,7 +259,7 @@ sequence.append(utils.AssertPresentationAction( "SPEECH OUTPUT: 'link'", "SPEECH OUTPUT: '•'", "SPEECH OUTPUT: 'Mailing list:'", - "SPEECH OUTPUT: 'cthulhu-list@gnome.org'", + "SPEECH OUTPUT: 'https://groups.io/g/stormux'", "SPEECH OUTPUT: 'link'", "SPEECH OUTPUT: '('", "SPEECH OUTPUT: 'Archives'", diff --git a/test/keystrokes/firefox/say_all_wiki_no_context.py b/test/keystrokes/firefox/say_all_wiki_no_context.py index 7274cfa..224ae13 100644 --- a/test/keystrokes/firefox/say_all_wiki_no_context.py +++ b/test/keystrokes/firefox/say_all_wiki_no_context.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of sayAll.""" @@ -252,7 +252,7 @@ sequence.append(utils.AssertPresentationAction( "SPEECH OUTPUT: 'link'", "SPEECH OUTPUT: '•'", "SPEECH OUTPUT: 'Mailing list:'", - "SPEECH OUTPUT: 'cthulhu-list@gnome.org'", + "SPEECH OUTPUT: 'https://groups.io/g/stormux'", "SPEECH OUTPUT: 'link'", "SPEECH OUTPUT: '('", "SPEECH OUTPUT: 'Archives'", diff --git a/test/keystrokes/firefox/selection_textarea.py b/test/keystrokes/firefox/selection_textarea.py index 8dfabba..eae5c39 100644 --- a/test/keystrokes/firefox/selection_textarea.py +++ b/test/keystrokes/firefox/selection_textarea.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/selection_wiki.py b/test/keystrokes/firefox/selection_wiki.py index b17ef63..26b2bf8 100644 --- a/test/keystrokes/firefox/selection_wiki.py +++ b/test/keystrokes/firefox/selection_wiki.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/spelling_errors.py b/test/keystrokes/firefox/spelling_errors.py index 7fb219a..ee74fbe 100644 --- a/test/keystrokes/firefox/spelling_errors.py +++ b/test/keystrokes/firefox/spelling_errors.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test presentation of spelling errors in text.""" diff --git a/test/keystrokes/firefox/ui_context_menu_flat_review.py b/test/keystrokes/firefox/ui_context_menu_flat_review.py index 6c36e0c..05c8234 100644 --- a/test/keystrokes/firefox/ui_context_menu_flat_review.py +++ b/test/keystrokes/firefox/ui_context_menu_flat_review.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/ui_doc_tabs.py b/test/keystrokes/firefox/ui_doc_tabs.py index 5edbea6..ebe9001 100644 --- a/test/keystrokes/firefox/ui_doc_tabs.py +++ b/test/keystrokes/firefox/ui_doc_tabs.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of document tabs""" diff --git a/test/keystrokes/firefox/ui_role_accel_label.py b/test/keystrokes/firefox/ui_role_accel_label.py index 6d58b8b..894c134 100644 --- a/test/keystrokes/firefox/ui_role_accel_label.py +++ b/test/keystrokes/firefox/ui_role_accel_label.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of menu accelerator label output of Firefox.""" diff --git a/test/keystrokes/firefox/ui_role_check_box.py b/test/keystrokes/firefox/ui_role_check_box.py index 4d8188c..c53f251 100644 --- a/test/keystrokes/firefox/ui_role_check_box.py +++ b/test/keystrokes/firefox/ui_role_check_box.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of gtk+ checkbox output from within Firefox.""" diff --git a/test/keystrokes/firefox/ui_role_check_menu_item.py b/test/keystrokes/firefox/ui_role_check_menu_item.py index 0f8aa6d..afe3d91 100644 --- a/test/keystrokes/firefox/ui_role_check_menu_item.py +++ b/test/keystrokes/firefox/ui_role_check_menu_item.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of menu checkbox output using Firefox.""" diff --git a/test/keystrokes/firefox/ui_role_entry.py b/test/keystrokes/firefox/ui_role_entry.py index 8d5b2fa..e24b004 100644 --- a/test/keystrokes/firefox/ui_role_entry.py +++ b/test/keystrokes/firefox/ui_role_entry.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of entry output using Firefox.""" diff --git a/test/keystrokes/firefox/ui_role_menu_bar.py b/test/keystrokes/firefox/ui_role_menu_bar.py index e452639..1edfc07 100644 --- a/test/keystrokes/firefox/ui_role_menu_bar.py +++ b/test/keystrokes/firefox/ui_role_menu_bar.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of menu bar output of Firefox.""" diff --git a/test/keystrokes/firefox/ui_role_menu_flat_review.py b/test/keystrokes/firefox/ui_role_menu_flat_review.py index 0323cdb..63a89e4 100644 --- a/test/keystrokes/firefox/ui_role_menu_flat_review.py +++ b/test/keystrokes/firefox/ui_role_menu_flat_review.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of menu and menu item output.""" diff --git a/test/keystrokes/firefox/ui_role_page_tab.py b/test/keystrokes/firefox/ui_role_page_tab.py index 1c4a7ca..ad2e441 100644 --- a/test/keystrokes/firefox/ui_role_page_tab.py +++ b/test/keystrokes/firefox/ui_role_page_tab.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of page tab output using Firefox.""" diff --git a/test/keystrokes/firefox/ui_role_push_button.py b/test/keystrokes/firefox/ui_role_push_button.py index abddefb..77fd31c 100644 --- a/test/keystrokes/firefox/ui_role_push_button.py +++ b/test/keystrokes/firefox/ui_role_push_button.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of push button output using Firefox.""" diff --git a/test/keystrokes/firefox/ui_role_radio_button.py b/test/keystrokes/firefox/ui_role_radio_button.py index 5cf55dc..66d944d 100644 --- a/test/keystrokes/firefox/ui_role_radio_button.py +++ b/test/keystrokes/firefox/ui_role_radio_button.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of radio button output using Firefox.""" diff --git a/test/keystrokes/firefox/ui_role_radio_menu_item.py b/test/keystrokes/firefox/ui_role_radio_menu_item.py index de3ff3f..3e77828 100644 --- a/test/keystrokes/firefox/ui_role_radio_menu_item.py +++ b/test/keystrokes/firefox/ui_role_radio_menu_item.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of menu radio button output using Firefox.""" diff --git a/test/keystrokes/firefox/ui_role_tree.py b/test/keystrokes/firefox/ui_role_tree.py index 107bc5b..e655dd1 100644 --- a/test/keystrokes/firefox/ui_role_tree.py +++ b/test/keystrokes/firefox/ui_role_tree.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of tree output using Firefox.""" diff --git a/test/keystrokes/firefox/ui_role_tree_table.py b/test/keystrokes/firefox/ui_role_tree_table.py index 410e420..047d227 100644 --- a/test/keystrokes/firefox/ui_role_tree_table.py +++ b/test/keystrokes/firefox/ui_role_tree_table.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of tree table output using Firefox.""" diff --git a/test/keystrokes/firefox/ui_title_and_status_bar.py b/test/keystrokes/firefox/ui_title_and_status_bar.py index a62f529..c2765e9 100644 --- a/test/keystrokes/firefox/ui_title_and_status_bar.py +++ b/test/keystrokes/firefox/ui_title_and_status_bar.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/firefox/word_nav_links.py b/test/keystrokes/firefox/word_nav_links.py index 682a5fd..d77c9fb 100644 --- a/test/keystrokes/firefox/word_nav_links.py +++ b/test/keystrokes/firefox/word_nav_links.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/firefox/word_nav_list_items.py b/test/keystrokes/firefox/word_nav_list_items.py index da23aed..d00f113 100644 --- a/test/keystrokes/firefox/word_nav_list_items.py +++ b/test/keystrokes/firefox/word_nav_list_items.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of line navigation output of Firefox.""" diff --git a/test/keystrokes/gnome-appearance-properties/font-preferences.py b/test/keystrokes/gnome-appearance-properties/font-preferences.py index 4f1b567..7eab85c 100644 --- a/test/keystrokes/gnome-appearance-properties/font-preferences.py +++ b/test/keystrokes/gnome-appearance-properties/font-preferences.py @@ -20,11 +20,14 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Testing of font preferences in the gnome-appearance properties dialog.""" +import gi +gi.require_version('Atspi', '2.0') +from gi.repository import Atspi from macaroon.playback import * import utils @@ -35,14 +38,14 @@ sequence = MacroSequence() # then navigate to the Fonts tab # sequence.append(WaitForWindowActivate("Appearance Preferences")) -sequence.append(WaitForFocus("Theme", acc_role=pyatspi.ROLE_PAGE_TAB)) +sequence.append(WaitForFocus("Theme", acc_role=Atspi.Role.PAGE_TAB)) sequence.append(KeyComboAction("Right")) -sequence.append(WaitForFocus("Background", acc_role=pyatspi.ROLE_PAGE_TAB)) +sequence.append(WaitForFocus("Background", acc_role=Atspi.Role.PAGE_TAB)) sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Right")) -sequence.append(WaitForFocus("Fonts", acc_role=pyatspi.ROLE_PAGE_TAB)) +sequence.append(WaitForFocus("Fonts", acc_role=Atspi.Role.PAGE_TAB)) sequence.append(utils.AssertPresentationAction( "Fonts tab", ["BRAILLE LINE: 'gnome-appearance-properties Application Appearance Preferences Dialog Fonts'", @@ -54,12 +57,12 @@ sequence.append(utils.AssertPresentationAction( # Open the 'Pick a Font' dialog # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.PUSH_BUTTON)) sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("a")) #sequence.append(WaitForWindowActivate("Pick a Font")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TABLE)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TABLE)) sequence.append(utils.AssertPresentationAction( "Pick a Font dialog", ["BRAILLE LINE: 'gnome-appearance-properties Application Pick a Font FontChooser'", @@ -80,13 +83,13 @@ sequence.append(KeyComboAction("Down")) sequence.append(WaitAction("object:state-changed:selected", None, None, - pyatspi.ROLE_TABLE_CELL, + Atspi.Role.TABLE_CELL, 5000)) sequence.append(KeyComboAction("Up")) sequence.append(WaitAction("object:state-changed:selected", None, None, - pyatspi.ROLE_TABLE_CELL, + Atspi.Role.TABLE_CELL, 5000)) sequence.append(utils.AssertPresentationAction( "Examine Family", @@ -104,7 +107,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TABLE)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TABLE)) sequence.append(utils.AssertPresentationAction( "Style table", ["BRAILLE LINE: 'gnome-appearance-properties Application Pick a Font FontChooser ScrollPane Style: Table Face ColumnHeader Regular'", @@ -118,7 +121,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TEXT)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TEXT)) sequence.append(utils.AssertPresentationAction( "Size table", ["BRAILLE LINE: 'gnome-appearance-properties Application Pick a Font FontChooser Size: 10 $l'", @@ -134,10 +137,10 @@ sequence.append(KeyComboAction("Return")) sequence.append(WaitAction("object:selection-changed", None, None, - pyatspi.ROLE_TABLE, + Atspi.Role.TABLE, 5000)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TABLE)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TABLE)) sequence.append(utils.AssertPresentationAction( "Change size", ["BRAILLE LINE: 'gnome-appearance-properties Application Pick a Font FontChooser Size: 10 $l'", @@ -162,7 +165,7 @@ sequence.append(utils.AssertPresentationAction( # Accept the change and dismiss the 'Pick a Font' dialog. # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TEXT)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TEXT)) # check the font attributes sequence.append(utils.StartRecordingAction()) @@ -175,41 +178,41 @@ sequence.append(utils.AssertPresentationAction( "SPEECH OUTPUT: 'family-name Sans'"])) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Cancel", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("Cancel", acc_role=Atspi.Role.PUSH_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("OK", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("OK", acc_role=Atspi.Role.PUSH_BUTTON)) sequence.append(KeyComboAction("o")) ######################################################################## # Bring the 'Pick a Font' dialog back up # #sequence.append(WaitForWindowActivate("Appearance Preferences")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.PUSH_BUTTON)) sequence.append(KeyComboAction("Return")) #sequence.append(WaitForWindowActivate("Pick a Font")) -sequence.append(WaitForFocus("OK", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("OK", acc_role=Atspi.Role.PUSH_BUTTON)) ######################################################################## # Go to the 'Size' areas and change it to 10 from 18 # sequence.append(KeyComboAction("z")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TEXT)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TEXT)) sequence.append(TypeAction("10")) sequence.append(KeyComboAction("Return")) sequence.append(WaitAction("object:selection-changed", None, None, - pyatspi.ROLE_TABLE, + Atspi.Role.TABLE, 5000)) ######################################################################## # Accept the change and dismiss the 'Pick a Font' dialog. # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TABLE)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TABLE)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TEXT)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TEXT)) # check the font attributes sequence.append(utils.StartRecordingAction()) @@ -222,23 +225,23 @@ sequence.append(utils.AssertPresentationAction( "SPEECH OUTPUT: 'family-name Sans'"])) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Cancel", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("Cancel", acc_role=Atspi.Role.PUSH_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("OK", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("OK", acc_role=Atspi.Role.PUSH_BUTTON)) sequence.append(KeyComboAction("o")) ######################################################################## # Revert application to original status # #sequence.append(WaitForWindowActivate("Appearance Preferences")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.PUSH_BUTTON)) sequence.append(KeyComboAction("ISO_Left_Tab")) -sequence.append(WaitForFocus("Fonts", acc_role=pyatspi.ROLE_PAGE_TAB)) +sequence.append(WaitForFocus("Fonts", acc_role=Atspi.Role.PAGE_TAB)) sequence.append(KeyComboAction("Left")) -sequence.append(WaitForFocus("Background", acc_role=pyatspi.ROLE_PAGE_TAB)) +sequence.append(WaitForFocus("Background", acc_role=Atspi.Role.PAGE_TAB)) sequence.append(KeyComboAction("Left")) -sequence.append(WaitForFocus("Theme", acc_role=pyatspi.ROLE_PAGE_TAB)) +sequence.append(WaitForFocus("Theme", acc_role=Atspi.Role.PAGE_TAB)) # Just a little extra wait to let some events get through. # diff --git a/test/keystrokes/gnome-calculator/gcalctool01.py b/test/keystrokes/gnome-calculator/gcalctool01.py index 7851289..65b5ddc 100644 --- a/test/keystrokes/gnome-calculator/gcalctool01.py +++ b/test/keystrokes/gnome-calculator/gcalctool01.py @@ -20,11 +20,14 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu '''TEST the Ability to find out the square root of a selected number''' +import gi +gi.require_version('Atspi', '2.0') +from gi.repository import Atspi from macaroon.playback import * import utils sequence = MacroSequence() @@ -34,7 +37,7 @@ sequence = MacroSequence() # sequence.append(WaitForWindowActivate("Calculator", None)) sequence.append(KeyComboAction("a")) -sequence.append(WaitForFocus("Change Mode", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("Change Mode", acc_role=Atspi.Role.PUSH_BUTTON)) sequence.append(KeyComboAction("Return")) ############################################################################### diff --git a/test/keystrokes/gnome-clocks/stop_watch_flat_review.py b/test/keystrokes/gnome-clocks/stop_watch_flat_review.py index c701e99..1bb2061 100644 --- a/test/keystrokes/gnome-clocks/stop_watch_flat_review.py +++ b/test/keystrokes/gnome-clocks/stop_watch_flat_review.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/gnome-clocks/timer_flat_review.py b/test/keystrokes/gnome-clocks/timer_flat_review.py index 355c5d9..95f3e00 100644 --- a/test/keystrokes/gnome-clocks/timer_flat_review.py +++ b/test/keystrokes/gnome-clocks/timer_flat_review.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/gnome-terminal/background_updates.py b/test/keystrokes/gnome-terminal/background_updates.py index 6fc74e1..0fbbe88 100644 --- a/test/keystrokes/gnome-terminal/background_updates.py +++ b/test/keystrokes/gnome-terminal/background_updates.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/gnome-terminal/command_not_found.py b/test/keystrokes/gnome-terminal/command_not_found.py index eb96ed7..e9bf483 100644 --- a/test/keystrokes/gnome-terminal/command_not_found.py +++ b/test/keystrokes/gnome-terminal/command_not_found.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/gnome-terminal/exit_shell.py b/test/keystrokes/gnome-terminal/exit_shell.py index d4269bc..52070b9 100644 --- a/test/keystrokes/gnome-terminal/exit_shell.py +++ b/test/keystrokes/gnome-terminal/exit_shell.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/gnome-terminal/ls.py b/test/keystrokes/gnome-terminal/ls.py index 376f99a..f8160a4 100644 --- a/test/keystrokes/gnome-terminal/ls.py +++ b/test/keystrokes/gnome-terminal/ls.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu import os diff --git a/test/keystrokes/gnome-terminal/ls_flat_review.py b/test/keystrokes/gnome-terminal/ls_flat_review.py index 903eb29..05ca217 100644 --- a/test/keystrokes/gnome-terminal/ls_flat_review.py +++ b/test/keystrokes/gnome-terminal/ls_flat_review.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu import os diff --git a/test/keystrokes/gnome-terminal/man_page.py b/test/keystrokes/gnome-terminal/man_page.py index c90712e..6a200e8 100644 --- a/test/keystrokes/gnome-terminal/man_page.py +++ b/test/keystrokes/gnome-terminal/man_page.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/gnome-terminal/man_page_flat_review.py b/test/keystrokes/gnome-terminal/man_page_flat_review.py index 2d4c5bd..2495251 100644 --- a/test/keystrokes/gnome-terminal/man_page_flat_review.py +++ b/test/keystrokes/gnome-terminal/man_page_flat_review.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/gnome-terminal/multiple_tabs.py b/test/keystrokes/gnome-terminal/multiple_tabs.py index 5903166..da16c25 100644 --- a/test/keystrokes/gnome-terminal/multiple_tabs.py +++ b/test/keystrokes/gnome-terminal/multiple_tabs.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/gnome-terminal/nano.py b/test/keystrokes/gnome-terminal/nano.py index 7a48f57..b1c9b07 100644 --- a/test/keystrokes/gnome-terminal/nano.py +++ b/test/keystrokes/gnome-terminal/nano.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/gnome-terminal/nano_flat_review.py b/test/keystrokes/gnome-terminal/nano_flat_review.py index eff5bfd..896efc3 100644 --- a/test/keystrokes/gnome-terminal/nano_flat_review.py +++ b/test/keystrokes/gnome-terminal/nano_flat_review.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/gnome-terminal/pasting.py b/test/keystrokes/gnome-terminal/pasting.py index b54eab0..0b5941a 100644 --- a/test/keystrokes/gnome-terminal/pasting.py +++ b/test/keystrokes/gnome-terminal/pasting.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/gnome-terminal/reverse_i_search.py b/test/keystrokes/gnome-terminal/reverse_i_search.py index 31f1327..115898b 100644 --- a/test/keystrokes/gnome-terminal/reverse_i_search.py +++ b/test/keystrokes/gnome-terminal/reverse_i_search.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/gnome-terminal/tab_completion.py b/test/keystrokes/gnome-terminal/tab_completion.py index 7e99612..78dfdb5 100644 --- a/test/keystrokes/gnome-terminal/tab_completion.py +++ b/test/keystrokes/gnome-terminal/tab_completion.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/gnome-terminal/vim-append.py b/test/keystrokes/gnome-terminal/vim-append.py index b28799a..29522cf 100644 --- a/test/keystrokes/gnome-terminal/vim-append.py +++ b/test/keystrokes/gnome-terminal/vim-append.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/gtk-demo/context_menu_flat_review.py b/test/keystrokes/gtk-demo/context_menu_flat_review.py index 2a41f1c..02e54fd 100644 --- a/test/keystrokes/gtk-demo/context_menu_flat_review.py +++ b/test/keystrokes/gtk-demo/context_menu_flat_review.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/gtk-demo/learn_mode.py b/test/keystrokes/gtk-demo/learn_mode.py index 87b577f..d9710df 100644 --- a/test/keystrokes/gtk-demo/learn_mode.py +++ b/test/keystrokes/gtk-demo/learn_mode.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of learn mode.""" diff --git a/test/keystrokes/gtk-demo/role_accel_label.py b/test/keystrokes/gtk-demo/role_accel_label.py index 6b555f3..2b778a4 100644 --- a/test/keystrokes/gtk-demo/role_accel_label.py +++ b/test/keystrokes/gtk-demo/role_accel_label.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of menu accelerator label output.""" diff --git a/test/keystrokes/gtk-demo/role_alert.py b/test/keystrokes/gtk-demo/role_alert.py index e1991f3..eeefbc0 100644 --- a/test/keystrokes/gtk-demo/role_alert.py +++ b/test/keystrokes/gtk-demo/role_alert.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of presentation of dialogs and alerts.""" diff --git a/test/keystrokes/gtk-demo/role_check_box.py b/test/keystrokes/gtk-demo/role_check_box.py index fcce283..78ecce4 100644 --- a/test/keystrokes/gtk-demo/role_check_box.py +++ b/test/keystrokes/gtk-demo/role_check_box.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of checkbox output.""" diff --git a/test/keystrokes/gtk-demo/role_check_menu_item.py b/test/keystrokes/gtk-demo/role_check_menu_item.py index 48f3dd3..c8861dd 100644 --- a/test/keystrokes/gtk-demo/role_check_menu_item.py +++ b/test/keystrokes/gtk-demo/role_check_menu_item.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of check menu item output.""" diff --git a/test/keystrokes/gtk-demo/role_column_header.py b/test/keystrokes/gtk-demo/role_column_header.py index 6b77696..81ea1c7 100644 --- a/test/keystrokes/gtk-demo/role_column_header.py +++ b/test/keystrokes/gtk-demo/role_column_header.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of column header output.""" diff --git a/test/keystrokes/gtk-demo/role_combo_box.py b/test/keystrokes/gtk-demo/role_combo_box.py index 846360a..2782dbf 100644 --- a/test/keystrokes/gtk-demo/role_combo_box.py +++ b/test/keystrokes/gtk-demo/role_combo_box.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of combobox output.""" diff --git a/test/keystrokes/gtk-demo/role_combo_box2.py b/test/keystrokes/gtk-demo/role_combo_box2.py index 4baeefc..2e5e426 100644 --- a/test/keystrokes/gtk-demo/role_combo_box2.py +++ b/test/keystrokes/gtk-demo/role_combo_box2.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of labelled combo box output.""" diff --git a/test/keystrokes/gtk-demo/role_dialog.py b/test/keystrokes/gtk-demo/role_dialog.py index 44cd88f..3faff17 100644 --- a/test/keystrokes/gtk-demo/role_dialog.py +++ b/test/keystrokes/gtk-demo/role_dialog.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of dialog presentation.""" diff --git a/test/keystrokes/gtk-demo/role_drawing_area.py b/test/keystrokes/gtk-demo/role_drawing_area.py index 1f37bff..fec0bdf 100644 --- a/test/keystrokes/gtk-demo/role_drawing_area.py +++ b/test/keystrokes/gtk-demo/role_drawing_area.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of drawing area output.""" diff --git a/test/keystrokes/gtk-demo/role_icon.py b/test/keystrokes/gtk-demo/role_icon.py index eceee47..e8cfbe0 100644 --- a/test/keystrokes/gtk-demo/role_icon.py +++ b/test/keystrokes/gtk-demo/role_icon.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of icon output.""" diff --git a/test/keystrokes/gtk-demo/role_icon_flat_review.py b/test/keystrokes/gtk-demo/role_icon_flat_review.py index 7ca2fe9..6cb73ad 100644 --- a/test/keystrokes/gtk-demo/role_icon_flat_review.py +++ b/test/keystrokes/gtk-demo/role_icon_flat_review.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of icon output.""" diff --git a/test/keystrokes/gtk-demo/role_label.py b/test/keystrokes/gtk-demo/role_label.py index 5772e55..0d939e8 100644 --- a/test/keystrokes/gtk-demo/role_label.py +++ b/test/keystrokes/gtk-demo/role_label.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of label presentation.""" diff --git a/test/keystrokes/gtk-demo/role_menu.py b/test/keystrokes/gtk-demo/role_menu.py index a4f7061..5ca65e8 100644 --- a/test/keystrokes/gtk-demo/role_menu.py +++ b/test/keystrokes/gtk-demo/role_menu.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of menu and menu item output.""" diff --git a/test/keystrokes/gtk-demo/role_menu_flat_review.py b/test/keystrokes/gtk-demo/role_menu_flat_review.py index 96157d2..66f262d 100644 --- a/test/keystrokes/gtk-demo/role_menu_flat_review.py +++ b/test/keystrokes/gtk-demo/role_menu_flat_review.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of menu and menu item output.""" diff --git a/test/keystrokes/gtk-demo/role_page_tab.py b/test/keystrokes/gtk-demo/role_page_tab.py index 05a6be3..36d926e 100644 --- a/test/keystrokes/gtk-demo/role_page_tab.py +++ b/test/keystrokes/gtk-demo/role_page_tab.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of page tab output.""" diff --git a/test/keystrokes/gtk-demo/role_push_button.py b/test/keystrokes/gtk-demo/role_push_button.py index c0491cf..3efc769 100644 --- a/test/keystrokes/gtk-demo/role_push_button.py +++ b/test/keystrokes/gtk-demo/role_push_button.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of push button output.""" diff --git a/test/keystrokes/gtk-demo/role_radio_button.py b/test/keystrokes/gtk-demo/role_radio_button.py index fd5fdf4..97eb28f 100644 --- a/test/keystrokes/gtk-demo/role_radio_button.py +++ b/test/keystrokes/gtk-demo/role_radio_button.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of radio button output.""" diff --git a/test/keystrokes/gtk-demo/role_spin_button.py b/test/keystrokes/gtk-demo/role_spin_button.py index f89878c..a12044f 100644 --- a/test/keystrokes/gtk-demo/role_spin_button.py +++ b/test/keystrokes/gtk-demo/role_spin_button.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of spin button output""" diff --git a/test/keystrokes/gtk-demo/role_split_pane.py b/test/keystrokes/gtk-demo/role_split_pane.py index 1a7ef7d..43c944a 100644 --- a/test/keystrokes/gtk-demo/role_split_pane.py +++ b/test/keystrokes/gtk-demo/role_split_pane.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of split pane output.""" diff --git a/test/keystrokes/gtk-demo/role_status_bar.py b/test/keystrokes/gtk-demo/role_status_bar.py index bf07e68..5e5a483 100644 --- a/test/keystrokes/gtk-demo/role_status_bar.py +++ b/test/keystrokes/gtk-demo/role_status_bar.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of status bar output.""" diff --git a/test/keystrokes/gtk-demo/role_table.py b/test/keystrokes/gtk-demo/role_table.py index 52c0b2f..8a9f018 100644 --- a/test/keystrokes/gtk-demo/role_table.py +++ b/test/keystrokes/gtk-demo/role_table.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of table output.""" diff --git a/test/keystrokes/gtk-demo/role_text_multiline.py b/test/keystrokes/gtk-demo/role_text_multiline.py index eef8452..27541be 100644 --- a/test/keystrokes/gtk-demo/role_text_multiline.py +++ b/test/keystrokes/gtk-demo/role_text_multiline.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of multiline editable text.""" diff --git a/test/keystrokes/gtk-demo/role_text_multiline_flat_review.py b/test/keystrokes/gtk-demo/role_text_multiline_flat_review.py index d357125..c750c94 100644 --- a/test/keystrokes/gtk-demo/role_text_multiline_flat_review.py +++ b/test/keystrokes/gtk-demo/role_text_multiline_flat_review.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of flat review of text and a toolbar.""" diff --git a/test/keystrokes/gtk-demo/role_text_multiline_navigation.py b/test/keystrokes/gtk-demo/role_text_multiline_navigation.py index d74d702..b8a9ffe 100644 --- a/test/keystrokes/gtk-demo/role_text_multiline_navigation.py +++ b/test/keystrokes/gtk-demo/role_text_multiline_navigation.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of text output for caret navigation.""" diff --git a/test/keystrokes/gtk-demo/role_text_multiline_navigation2.py b/test/keystrokes/gtk-demo/role_text_multiline_navigation2.py index 89bd617..41eeb78 100644 --- a/test/keystrokes/gtk-demo/role_text_multiline_navigation2.py +++ b/test/keystrokes/gtk-demo/role_text_multiline_navigation2.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of text output for caret navigation and flat review.""" diff --git a/test/keystrokes/gtk-demo/role_toggle_button.py b/test/keystrokes/gtk-demo/role_toggle_button.py index 3fe72ea..adf5e99 100644 --- a/test/keystrokes/gtk-demo/role_toggle_button.py +++ b/test/keystrokes/gtk-demo/role_toggle_button.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of toggle button output.""" diff --git a/test/keystrokes/gtk-demo/role_toolbar.py b/test/keystrokes/gtk-demo/role_toolbar.py index 5102114..cf22c42 100644 --- a/test/keystrokes/gtk-demo/role_toolbar.py +++ b/test/keystrokes/gtk-demo/role_toolbar.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of toolbar output using.""" diff --git a/test/keystrokes/gtk-demo/role_tooltip.py b/test/keystrokes/gtk-demo/role_tooltip.py index 12e19a1..2ac0685 100644 --- a/test/keystrokes/gtk-demo/role_tooltip.py +++ b/test/keystrokes/gtk-demo/role_tooltip.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of tooltips.""" diff --git a/test/keystrokes/gtk-demo/role_tree_table.py b/test/keystrokes/gtk-demo/role_tree_table.py index 56bfd94..4822da4 100644 --- a/test/keystrokes/gtk-demo/role_tree_table.py +++ b/test/keystrokes/gtk-demo/role_tree_table.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of tree table output.""" diff --git a/test/keystrokes/gtk-demo/role_window.py b/test/keystrokes/gtk-demo/role_window.py index c498a8a..a903f52 100644 --- a/test/keystrokes/gtk-demo/role_window.py +++ b/test/keystrokes/gtk-demo/role_window.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of window title output.""" diff --git a/test/keystrokes/gtk-demo/spoken_indentation.py b/test/keystrokes/gtk-demo/spoken_indentation.py index 862abcb..c435559 100644 --- a/test/keystrokes/gtk-demo/spoken_indentation.py +++ b/test/keystrokes/gtk-demo/spoken_indentation.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of presentation of whitespace with braille disabled.""" diff --git a/test/keystrokes/gtk3-demo/context_menu_flat_review.py b/test/keystrokes/gtk3-demo/context_menu_flat_review.py index 06ceaa7..91712fc 100644 --- a/test/keystrokes/gtk3-demo/context_menu_flat_review.py +++ b/test/keystrokes/gtk3-demo/context_menu_flat_review.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/gtk3-demo/learn_mode.py b/test/keystrokes/gtk3-demo/learn_mode.py index b1615ce..f93c0b4 100644 --- a/test/keystrokes/gtk3-demo/learn_mode.py +++ b/test/keystrokes/gtk3-demo/learn_mode.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of learn mode.""" diff --git a/test/keystrokes/gtk3-demo/role_accel_label.py b/test/keystrokes/gtk3-demo/role_accel_label.py index 1897d38..91a6759 100644 --- a/test/keystrokes/gtk3-demo/role_accel_label.py +++ b/test/keystrokes/gtk3-demo/role_accel_label.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of menu accelerator label output.""" diff --git a/test/keystrokes/gtk3-demo/role_alert.py b/test/keystrokes/gtk3-demo/role_alert.py index 57ee1c1..d096731 100644 --- a/test/keystrokes/gtk3-demo/role_alert.py +++ b/test/keystrokes/gtk3-demo/role_alert.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of presentation of dialogs and alerts.""" diff --git a/test/keystrokes/gtk3-demo/role_check_box.py b/test/keystrokes/gtk3-demo/role_check_box.py index 5683aee..5c63e44 100644 --- a/test/keystrokes/gtk3-demo/role_check_box.py +++ b/test/keystrokes/gtk3-demo/role_check_box.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of checkbox output.""" diff --git a/test/keystrokes/gtk3-demo/role_check_menu_item.py b/test/keystrokes/gtk3-demo/role_check_menu_item.py index 44fafd5..79e41e1 100644 --- a/test/keystrokes/gtk3-demo/role_check_menu_item.py +++ b/test/keystrokes/gtk3-demo/role_check_menu_item.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of check menu item output.""" diff --git a/test/keystrokes/gtk3-demo/role_color_chooser.py b/test/keystrokes/gtk3-demo/role_color_chooser.py index 8c521d6..6718d5a 100644 --- a/test/keystrokes/gtk3-demo/role_color_chooser.py +++ b/test/keystrokes/gtk3-demo/role_color_chooser.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of color chooser output.""" diff --git a/test/keystrokes/gtk3-demo/role_column_header.py b/test/keystrokes/gtk3-demo/role_column_header.py index f342740..7e2215b 100644 --- a/test/keystrokes/gtk3-demo/role_column_header.py +++ b/test/keystrokes/gtk3-demo/role_column_header.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of column header output.""" diff --git a/test/keystrokes/gtk3-demo/role_combo_box.py b/test/keystrokes/gtk3-demo/role_combo_box.py index 49865f7..1a2e838 100644 --- a/test/keystrokes/gtk3-demo/role_combo_box.py +++ b/test/keystrokes/gtk3-demo/role_combo_box.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of combobox output.""" diff --git a/test/keystrokes/gtk3-demo/role_combo_box2.py b/test/keystrokes/gtk3-demo/role_combo_box2.py index 0c2d3d3..ea4dafa 100644 --- a/test/keystrokes/gtk3-demo/role_combo_box2.py +++ b/test/keystrokes/gtk3-demo/role_combo_box2.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of labelled combo box output.""" diff --git a/test/keystrokes/gtk3-demo/role_dialog.py b/test/keystrokes/gtk3-demo/role_dialog.py index 6e02ec5..bd65662 100644 --- a/test/keystrokes/gtk3-demo/role_dialog.py +++ b/test/keystrokes/gtk3-demo/role_dialog.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of dialog presentation.""" diff --git a/test/keystrokes/gtk3-demo/role_dialog_flat_review.py b/test/keystrokes/gtk3-demo/role_dialog_flat_review.py index 088ce2f..38f15bb 100644 --- a/test/keystrokes/gtk3-demo/role_dialog_flat_review.py +++ b/test/keystrokes/gtk3-demo/role_dialog_flat_review.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/gtk3-demo/role_drawing_area.py b/test/keystrokes/gtk3-demo/role_drawing_area.py index 1c75840..e93e80a 100644 --- a/test/keystrokes/gtk3-demo/role_drawing_area.py +++ b/test/keystrokes/gtk3-demo/role_drawing_area.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of drawing area output.""" diff --git a/test/keystrokes/gtk3-demo/role_icon.py b/test/keystrokes/gtk3-demo/role_icon.py index 212ae51..ce1831a 100644 --- a/test/keystrokes/gtk3-demo/role_icon.py +++ b/test/keystrokes/gtk3-demo/role_icon.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of icon output.""" diff --git a/test/keystrokes/gtk3-demo/role_icon_flat_review.py b/test/keystrokes/gtk3-demo/role_icon_flat_review.py index 914fee2..b8fb842 100644 --- a/test/keystrokes/gtk3-demo/role_icon_flat_review.py +++ b/test/keystrokes/gtk3-demo/role_icon_flat_review.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of icon output.""" diff --git a/test/keystrokes/gtk3-demo/role_info_bar.py b/test/keystrokes/gtk3-demo/role_info_bar.py index 01bd5ab..f463e11 100644 --- a/test/keystrokes/gtk3-demo/role_info_bar.py +++ b/test/keystrokes/gtk3-demo/role_info_bar.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of info bar output.""" diff --git a/test/keystrokes/gtk3-demo/role_listbox.py b/test/keystrokes/gtk3-demo/role_listbox.py index c1cd568..bc30d2c 100644 --- a/test/keystrokes/gtk3-demo/role_listbox.py +++ b/test/keystrokes/gtk3-demo/role_listbox.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of listbox output.""" diff --git a/test/keystrokes/gtk3-demo/role_menu.py b/test/keystrokes/gtk3-demo/role_menu.py index c7f022c..43e2556 100644 --- a/test/keystrokes/gtk3-demo/role_menu.py +++ b/test/keystrokes/gtk3-demo/role_menu.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of menu and menu item output.""" diff --git a/test/keystrokes/gtk3-demo/role_menu_flat_review.py b/test/keystrokes/gtk3-demo/role_menu_flat_review.py index f354aee..fd0cc21 100644 --- a/test/keystrokes/gtk3-demo/role_menu_flat_review.py +++ b/test/keystrokes/gtk3-demo/role_menu_flat_review.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of menu and menu item output.""" diff --git a/test/keystrokes/gtk3-demo/role_page_tab.py b/test/keystrokes/gtk3-demo/role_page_tab.py index 412f4a6..ee36216 100644 --- a/test/keystrokes/gtk3-demo/role_page_tab.py +++ b/test/keystrokes/gtk3-demo/role_page_tab.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of page tab output.""" diff --git a/test/keystrokes/gtk3-demo/role_push_button.py b/test/keystrokes/gtk3-demo/role_push_button.py index 435fc1c..965d82f 100644 --- a/test/keystrokes/gtk3-demo/role_push_button.py +++ b/test/keystrokes/gtk3-demo/role_push_button.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of push button output.""" diff --git a/test/keystrokes/gtk3-demo/role_radio_button.py b/test/keystrokes/gtk3-demo/role_radio_button.py index e60335c..62b8701 100644 --- a/test/keystrokes/gtk3-demo/role_radio_button.py +++ b/test/keystrokes/gtk3-demo/role_radio_button.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of radio button output.""" diff --git a/test/keystrokes/gtk3-demo/role_radio_menu_item.py b/test/keystrokes/gtk3-demo/role_radio_menu_item.py index 0e87a85..f4dcadb 100644 --- a/test/keystrokes/gtk3-demo/role_radio_menu_item.py +++ b/test/keystrokes/gtk3-demo/role_radio_menu_item.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of radio menu item output.""" diff --git a/test/keystrokes/gtk3-demo/role_spin_button.py b/test/keystrokes/gtk3-demo/role_spin_button.py index e4ef648..217a2a5 100644 --- a/test/keystrokes/gtk3-demo/role_spin_button.py +++ b/test/keystrokes/gtk3-demo/role_spin_button.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of spin button output""" diff --git a/test/keystrokes/gtk3-demo/role_split_pane.py b/test/keystrokes/gtk3-demo/role_split_pane.py index 9e8924c..b110bc2 100644 --- a/test/keystrokes/gtk3-demo/role_split_pane.py +++ b/test/keystrokes/gtk3-demo/role_split_pane.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of split pane output.""" diff --git a/test/keystrokes/gtk3-demo/role_status_bar.py b/test/keystrokes/gtk3-demo/role_status_bar.py index 35ea52d..d25f819 100644 --- a/test/keystrokes/gtk3-demo/role_status_bar.py +++ b/test/keystrokes/gtk3-demo/role_status_bar.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of status bar output.""" diff --git a/test/keystrokes/gtk3-demo/role_table.py b/test/keystrokes/gtk3-demo/role_table.py index 754f8af..27d9e19 100644 --- a/test/keystrokes/gtk3-demo/role_table.py +++ b/test/keystrokes/gtk3-demo/role_table.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of table output.""" diff --git a/test/keystrokes/gtk3-demo/role_table_flat_review.py b/test/keystrokes/gtk3-demo/role_table_flat_review.py index e6e94e8..c504478 100644 --- a/test/keystrokes/gtk3-demo/role_table_flat_review.py +++ b/test/keystrokes/gtk3-demo/role_table_flat_review.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/gtk3-demo/role_text_multiline.py b/test/keystrokes/gtk3-demo/role_text_multiline.py index 692cd48..406321b 100644 --- a/test/keystrokes/gtk3-demo/role_text_multiline.py +++ b/test/keystrokes/gtk3-demo/role_text_multiline.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of multiline editable text.""" diff --git a/test/keystrokes/gtk3-demo/role_text_multiline_flat_review.py b/test/keystrokes/gtk3-demo/role_text_multiline_flat_review.py index eb8d824..f762d51 100644 --- a/test/keystrokes/gtk3-demo/role_text_multiline_flat_review.py +++ b/test/keystrokes/gtk3-demo/role_text_multiline_flat_review.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of flat review of text and a toolbar.""" diff --git a/test/keystrokes/gtk3-demo/role_text_multiline_navigation.py b/test/keystrokes/gtk3-demo/role_text_multiline_navigation.py index b5f2cf9..7ab5871 100644 --- a/test/keystrokes/gtk3-demo/role_text_multiline_navigation.py +++ b/test/keystrokes/gtk3-demo/role_text_multiline_navigation.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of text output for caret navigation.""" diff --git a/test/keystrokes/gtk3-demo/role_text_multiline_navigation2.py b/test/keystrokes/gtk3-demo/role_text_multiline_navigation2.py index 38890ed..393fe64 100644 --- a/test/keystrokes/gtk3-demo/role_text_multiline_navigation2.py +++ b/test/keystrokes/gtk3-demo/role_text_multiline_navigation2.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of text output for caret navigation and flat review.""" diff --git a/test/keystrokes/gtk3-demo/role_text_multiline_selection.py b/test/keystrokes/gtk3-demo/role_text_multiline_selection.py index fadd571..9b145f5 100644 --- a/test/keystrokes/gtk3-demo/role_text_multiline_selection.py +++ b/test/keystrokes/gtk3-demo/role_text_multiline_selection.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of multiline editable text.""" diff --git a/test/keystrokes/gtk3-demo/role_toggle_button.py b/test/keystrokes/gtk3-demo/role_toggle_button.py index 777db53..2b5abf0 100644 --- a/test/keystrokes/gtk3-demo/role_toggle_button.py +++ b/test/keystrokes/gtk3-demo/role_toggle_button.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of toggle button output.""" diff --git a/test/keystrokes/gtk3-demo/role_toggle_button_flat_review.py b/test/keystrokes/gtk3-demo/role_toggle_button_flat_review.py index 8d13308..35c2ed6 100644 --- a/test/keystrokes/gtk3-demo/role_toggle_button_flat_review.py +++ b/test/keystrokes/gtk3-demo/role_toggle_button_flat_review.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of toggle button output.""" diff --git a/test/keystrokes/gtk3-demo/role_toolbar.py b/test/keystrokes/gtk3-demo/role_toolbar.py index b1f7314..870ccf4 100644 --- a/test/keystrokes/gtk3-demo/role_toolbar.py +++ b/test/keystrokes/gtk3-demo/role_toolbar.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of toolbar output using.""" diff --git a/test/keystrokes/gtk3-demo/role_tooltip.py b/test/keystrokes/gtk3-demo/role_tooltip.py index d9c3c93..4482c9f 100644 --- a/test/keystrokes/gtk3-demo/role_tooltip.py +++ b/test/keystrokes/gtk3-demo/role_tooltip.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of tooltips.""" diff --git a/test/keystrokes/gtk3-demo/role_tree_table.py b/test/keystrokes/gtk3-demo/role_tree_table.py index 068b311..91bbbac 100644 --- a/test/keystrokes/gtk3-demo/role_tree_table.py +++ b/test/keystrokes/gtk3-demo/role_tree_table.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of tree table output.""" diff --git a/test/keystrokes/gtk3-demo/role_window.py b/test/keystrokes/gtk3-demo/role_window.py index 9036385..a9e672d 100644 --- a/test/keystrokes/gtk3-demo/role_window.py +++ b/test/keystrokes/gtk3-demo/role_window.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of window title output.""" diff --git a/test/keystrokes/gtk3-demo/spoken_indentation.py b/test/keystrokes/gtk3-demo/spoken_indentation.py index 811bf22..adb9d2d 100644 --- a/test/keystrokes/gtk3-demo/spoken_indentation.py +++ b/test/keystrokes/gtk3-demo/spoken_indentation.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of presentation of whitespace with braille disabled.""" diff --git a/test/keystrokes/helpcontent/line_nav_intro.py b/test/keystrokes/helpcontent/line_nav_intro.py index d6fc2bc..61c985d 100644 --- a/test/keystrokes/helpcontent/line_nav_intro.py +++ b/test/keystrokes/helpcontent/line_nav_intro.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of learn mode.""" diff --git a/test/keystrokes/helpcontent/line_nav_main_page.py b/test/keystrokes/helpcontent/line_nav_main_page.py index a479bde..da2727a 100644 --- a/test/keystrokes/helpcontent/line_nav_main_page.py +++ b/test/keystrokes/helpcontent/line_nav_main_page.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of learn mode.""" diff --git a/test/keystrokes/helpcontent/load_no_sayall.py b/test/keystrokes/helpcontent/load_no_sayall.py index 15c6f2f..864e370 100644 --- a/test/keystrokes/helpcontent/load_no_sayall.py +++ b/test/keystrokes/helpcontent/load_no_sayall.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of learn mode.""" diff --git a/test/keystrokes/helpcontent/load_sayall.py b/test/keystrokes/helpcontent/load_sayall.py index 28f6396..8c6c5b1 100644 --- a/test/keystrokes/helpcontent/load_sayall.py +++ b/test/keystrokes/helpcontent/load_sayall.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of learn mode.""" diff --git a/test/keystrokes/helpcontent/struct_nav_heading.py b/test/keystrokes/helpcontent/struct_nav_heading.py index b8a28bc..9fd83a9 100644 --- a/test/keystrokes/helpcontent/struct_nav_heading.py +++ b/test/keystrokes/helpcontent/struct_nav_heading.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of learn mode.""" diff --git a/test/keystrokes/helpcontent/struct_nav_list.py b/test/keystrokes/helpcontent/struct_nav_list.py index 8014d21..0349875 100644 --- a/test/keystrokes/helpcontent/struct_nav_list.py +++ b/test/keystrokes/helpcontent/struct_nav_list.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of learn mode.""" diff --git a/test/keystrokes/helpcontent/struct_nav_paragraph.py b/test/keystrokes/helpcontent/struct_nav_paragraph.py index f9f26c2..023d941 100644 --- a/test/keystrokes/helpcontent/struct_nav_paragraph.py +++ b/test/keystrokes/helpcontent/struct_nav_paragraph.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of learn mode.""" diff --git a/test/keystrokes/java/role_accel_label.py b/test/keystrokes/java/role_accel_label.py index b1155c1..fcd0291 100644 --- a/test/keystrokes/java/role_accel_label.py +++ b/test/keystrokes/java/role_accel_label.py @@ -20,11 +20,14 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of accelerator labels in Java's SwingSet2.""" +import gi +gi.require_version('Atspi', '2.0') +from gi.repository import Atspi from macaroon.playback import * import utils @@ -34,13 +37,13 @@ sequence = MacroSequence() # We wait for the demo to come up and for focus to be on the toggle button # #sequence.append(WaitForWindowActivate("SwingSet2",None)) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(PauseAction(5000)) sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("F10")) -sequence.append(WaitForFocus("File", acc_role=pyatspi.ROLE_MENU)) +sequence.append(WaitForFocus("File", acc_role=Atspi.Role.MENU)) sequence.append(utils.AssertPresentationAction( "1. F10 for File menu", ["KNOWN ISSUE - Sometimes more of the hierarchy is included in the braille; other times it is not. This applies to all tests here.", @@ -56,7 +59,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) -sequence.append(WaitForFocus("About", acc_role=pyatspi.ROLE_MENU_ITEM)) +sequence.append(WaitForFocus("About", acc_role=Atspi.Role.MENU_ITEM)) sequence.append(utils.AssertPresentationAction( "2. Arrow Down", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Swing demo menu bar MenuBar About'", @@ -82,7 +85,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) -sequence.append(WaitForFocus("Exit", acc_role=pyatspi.ROLE_MENU_ITEM)) +sequence.append(WaitForFocus("Exit", acc_role=Atspi.Role.MENU_ITEM)) sequence.append(utils.AssertPresentationAction( "4. Arrow Down", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Swing demo menu bar MenuBar Exit'", diff --git a/test/keystrokes/java/role_check_box.py b/test/keystrokes/java/role_check_box.py index 5594ad4..9a3a12c 100644 --- a/test/keystrokes/java/role_check_box.py +++ b/test/keystrokes/java/role_check_box.py @@ -20,12 +20,15 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of check boxes in Java's SwingSet2. """ +import gi +gi.require_version('Atspi', '2.0') +from gi.repository import Atspi from macaroon.playback import * import utils @@ -35,7 +38,7 @@ sequence = MacroSequence() # We wait for the demo to come up and for focus to be on the toggle button # #sequence.append(WaitForWindowActivate("SwingSet2",None)) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) # Wait for entire window to get populated. sequence.append(PauseAction(10000)) @@ -44,52 +47,52 @@ sequence.append(PauseAction(10000)) # Tab over to the button demo, and activate it. # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(TypeAction(" ")) ########################################################################## # Tab all the way down to the button page tab. # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Button Demo", acc_role=pyatspi.ROLE_PAGE_TAB)) +sequence.append(WaitForFocus("Button Demo", acc_role=Atspi.Role.PAGE_TAB)) sequence.append(KeyComboAction("Tab")) ########################################################################## # Select Check Boxes tab # -sequence.append(WaitForFocus("Buttons", acc_role=pyatspi.ROLE_PAGE_TAB)) +sequence.append(WaitForFocus("Buttons", acc_role=Atspi.Role.PAGE_TAB)) sequence.append(KeyComboAction("Right")) -sequence.append(WaitForFocus("Radio Buttons", acc_role=pyatspi.ROLE_PAGE_TAB)) +sequence.append(WaitForFocus("Radio Buttons", acc_role=Atspi.Role.PAGE_TAB)) sequence.append(KeyComboAction("Right")) -sequence.append(WaitForFocus("Check Boxes", acc_role=pyatspi.ROLE_PAGE_TAB)) +sequence.append(WaitForFocus("Check Boxes", acc_role=Atspi.Role.PAGE_TAB)) sequence.append(PauseAction(5000)) ########################################################################## @@ -97,7 +100,7 @@ sequence.append(PauseAction(5000)) # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("One ", acc_role=pyatspi.ROLE_CHECK_BOX)) +sequence.append(WaitForFocus("One ", acc_role=Atspi.Role.CHECK_BOX)) sequence.append(utils.AssertPresentationAction( "One checkbox unchecked", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Button Demo TabList Button Demo Page Check Boxes TabList Check Boxes Page Text CheckBoxes Panel < > One CheckBox'", @@ -122,7 +125,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(TypeAction(' ')) sequence.append(WaitAction("object:state-changed:checked", None, - None, pyatspi.ROLE_CHECK_BOX, 5000)) + None, Atspi.Role.CHECK_BOX, 5000)) sequence.append(utils.AssertPresentationAction( "One checkbox checked", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Button Demo TabList Button Demo Page Check Boxes TabList Check Boxes Page Text CheckBoxes Panel One CheckBox'", @@ -148,7 +151,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(TypeAction(' ')) sequence.append(WaitAction("object:state-changed:checked", None, - None, pyatspi.ROLE_CHECK_BOX, 5000)) + None, Atspi.Role.CHECK_BOX, 5000)) sequence.append(utils.AssertPresentationAction( "One checkbox unchecked", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Button Demo TabList Button Demo Page Check Boxes TabList Check Boxes Page Text CheckBoxes Panel < > One CheckBox'", @@ -156,16 +159,16 @@ sequence.append(utils.AssertPresentationAction( "SPEECH OUTPUT: 'not checked'"])) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Two", acc_role=pyatspi.ROLE_CHECK_BOX)) +sequence.append(WaitForFocus("Two", acc_role=Atspi.Role.CHECK_BOX)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Three", acc_role=pyatspi.ROLE_CHECK_BOX)) +sequence.append(WaitForFocus("Three", acc_role=Atspi.Role.CHECK_BOX)) ######################################################################## # Tab to the One lightbulb # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("One ", acc_role=pyatspi.ROLE_CHECK_BOX)) +sequence.append(WaitForFocus("One ", acc_role=Atspi.Role.CHECK_BOX)) sequence.append(utils.AssertPresentationAction( "One lightbulb checkbox unchecked", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Button Demo TabList Button Demo Page Check Boxes TabList Check Boxes Page Image CheckBoxes Panel < > One CheckBox'", @@ -191,7 +194,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(TypeAction(' ')) sequence.append(WaitAction("object:state-changed:checked", None, - None, pyatspi.ROLE_CHECK_BOX, 5000)) + None, Atspi.Role.CHECK_BOX, 5000)) sequence.append(utils.AssertPresentationAction( "One lightbulb checkbox checked", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Button Demo TabList Button Demo Page Check Boxes TabList Check Boxes Page Image CheckBoxes Panel One CheckBox'", @@ -201,7 +204,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(TypeAction(' ')) sequence.append(WaitAction("object:state-changed:checked", None, - None, pyatspi.ROLE_CHECK_BOX, 5000)) + None, Atspi.Role.CHECK_BOX, 5000)) sequence.append(utils.AssertPresentationAction( "One lightbulb unchecked checkbox", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Button Demo TabList Button Demo Page Check Boxes TabList Check Boxes Page Image CheckBoxes Panel < > One CheckBox'", @@ -209,25 +212,25 @@ sequence.append(utils.AssertPresentationAction( "SPEECH OUTPUT: 'not checked'"])) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Two", acc_role=pyatspi.ROLE_CHECK_BOX)) +sequence.append(WaitForFocus("Two", acc_role=Atspi.Role.CHECK_BOX)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Three", acc_role=pyatspi.ROLE_CHECK_BOX)) +sequence.append(WaitForFocus("Three", acc_role=Atspi.Role.CHECK_BOX)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Paint Border", acc_role=pyatspi.ROLE_CHECK_BOX)) +sequence.append(WaitForFocus("Paint Border", acc_role=Atspi.Role.CHECK_BOX)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Paint Focus", acc_role=pyatspi.ROLE_CHECK_BOX)) +sequence.append(WaitForFocus("Paint Focus", acc_role=Atspi.Role.CHECK_BOX)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Enabled", acc_role=pyatspi.ROLE_CHECK_BOX)) +sequence.append(WaitForFocus("Enabled", acc_role=Atspi.Role.CHECK_BOX)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Content Filled", acc_role=pyatspi.ROLE_CHECK_BOX)) +sequence.append(WaitForFocus("Content Filled", acc_role=Atspi.Role.CHECK_BOX)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Default", acc_role=pyatspi.ROLE_RADIO_BUTTON)) +sequence.append(WaitForFocus("Default", acc_role=Atspi.Role.RADIO_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("0", acc_role=pyatspi.ROLE_RADIO_BUTTON)) +sequence.append(WaitForFocus("0", acc_role=Atspi.Role.RADIO_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("10", acc_role=pyatspi.ROLE_RADIO_BUTTON)) +sequence.append(WaitForFocus("10", acc_role=Atspi.Role.RADIO_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TEXT)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TEXT)) sequence.append(KeyComboAction("Tab")) # Toggle the top left button, to return to normal state. diff --git a/test/keystrokes/java/role_check_menu_item.py b/test/keystrokes/java/role_check_menu_item.py index ba7269b..2cec15c 100644 --- a/test/keystrokes/java/role_check_menu_item.py +++ b/test/keystrokes/java/role_check_menu_item.py @@ -20,12 +20,15 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of check menu items in Java's SwingSet2. """ +import gi +gi.require_version('Atspi', '2.0') +from gi.repository import Atspi from macaroon.playback import * import utils @@ -35,18 +38,18 @@ sequence = MacroSequence() # We wait for the demo to come up and for focus to be on the toggle button # #sequence.append(WaitForWindowActivate("SwingSet2",None)) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(PauseAction(5000)) sequence.append(KeyComboAction("F10")) -sequence.append(WaitForFocus("File", acc_role=pyatspi.ROLE_MENU)) +sequence.append(WaitForFocus("File", acc_role=Atspi.Role.MENU)) sequence.append(KeyComboAction("Right")) -sequence.append(WaitForFocus("Look & Feel", acc_role=pyatspi.ROLE_MENU)) +sequence.append(WaitForFocus("Look & Feel", acc_role=Atspi.Role.MENU)) sequence.append(KeyComboAction("Right")) -sequence.append(WaitForFocus("Themes", acc_role=pyatspi.ROLE_MENU)) +sequence.append(WaitForFocus("Themes", acc_role=Atspi.Role.MENU)) sequence.append(KeyComboAction("Right")) -sequence.append(WaitForFocus("Options", acc_role=pyatspi.ROLE_MENU)) +sequence.append(WaitForFocus("Options", acc_role=Atspi.Role.MENU)) ######################################################################## # Go Down to the Enable Tool Tips menu item @@ -54,7 +57,7 @@ sequence.append(WaitForFocus("Options", acc_role=pyatspi.ROLE_MENU)) sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) sequence.append(WaitForFocus("Enable Tool Tips", - acc_role=pyatspi.ROLE_CHECK_BOX)) + acc_role=Atspi.Role.CHECK_BOX)) sequence.append(utils.AssertPresentationAction( "Enable Tool Tips checked check menu item", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Swing demo menu bar MenuBar Enable Tool Tips CheckBox'", @@ -67,7 +70,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) sequence.append(WaitForFocus("Enable Drag Support", - acc_role=pyatspi.ROLE_CHECK_BOX)) + acc_role=Atspi.Role.CHECK_BOX)) sequence.append(utils.AssertPresentationAction( "Enable Drag Support unchecked menu item", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Swing demo menu bar MenuBar < > Enable Drag Support CheckBox'", @@ -93,18 +96,18 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(TypeAction(" ")) sequence.append(WaitAction("object:state-changed:checked", None, - None, pyatspi.ROLE_CHECK_BOX, 5000)) + None, Atspi.Role.CHECK_BOX, 5000)) ######################################################################## # Go directly back to the checked menu item. # sequence.append(KeyComboAction("p")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) sequence.append(WaitForFocus("Enable Drag Support", - acc_role=pyatspi.ROLE_CHECK_BOX)) + acc_role=Atspi.Role.CHECK_BOX)) sequence.append(utils.AssertPresentationAction( "Enable Drag Support checked menu item", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Swing demo menu bar MenuBar Enable Drag Support CheckBox'", diff --git a/test/keystrokes/java/role_combo_box.py b/test/keystrokes/java/role_combo_box.py index 237dda2..92ab04a 100644 --- a/test/keystrokes/java/role_combo_box.py +++ b/test/keystrokes/java/role_combo_box.py @@ -20,11 +20,14 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of combo boxes in Java's SwingSet2.""" +import gi +gi.require_version('Atspi', '2.0') +from gi.repository import Atspi from macaroon.playback import * import utils @@ -34,7 +37,7 @@ sequence = MacroSequence() # We wait for the demo to come up and for focus to be on the toggle button # #sequence.append(WaitForWindowActivate("SwingSet2",None)) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) # Wait for entire window to get populated. sequence.append(PauseAction(5000)) @@ -43,42 +46,42 @@ sequence.append(PauseAction(5000)) # Tab over to the combo box demo, and activate it. # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(TypeAction(" ")) ########################################################################## # Tab all the way down to the button page tab. # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("ComboBox Demo", acc_role=pyatspi.ROLE_PAGE_TAB)) +sequence.append(WaitForFocus("ComboBox Demo", acc_role=Atspi.Role.PAGE_TAB)) sequence.append(PauseAction(5000)) @@ -88,7 +91,7 @@ sequence.append(PauseAction(5000)) sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Tab")) sequence.append(WaitForFocus("Philip, Howard, Jeff", - acc_role=pyatspi.ROLE_COMBO_BOX)) + acc_role=Atspi.Role.COMBO_BOX)) sequence.append(utils.AssertPresentationAction( "1. focusing over first combo box", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane ComboBox Demo TabList ComboBox Demo Page Presets: Philip, Howard, Jeff Combo'", @@ -246,7 +249,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Philip", acc_role=pyatspi.ROLE_COMBO_BOX)) +sequence.append(WaitForFocus("Philip", acc_role=Atspi.Role.COMBO_BOX)) sequence.append(utils.AssertPresentationAction( "15. Tab to next combo box", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane ComboBox Demo TabList ComboBox Demo Page Hair: Philip Combo'", @@ -258,7 +261,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Howard", acc_role=pyatspi.ROLE_COMBO_BOX)) +sequence.append(WaitForFocus("Howard", acc_role=Atspi.Role.COMBO_BOX)) sequence.append(utils.AssertPresentationAction( "16. Tab to next combo box", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane ComboBox Demo TabList ComboBox Demo Page Eyes & Nose: Howard Combo'", @@ -270,7 +273,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Jeff", acc_role=pyatspi.ROLE_COMBO_BOX)) +sequence.append(WaitForFocus("Jeff", acc_role=Atspi.Role.COMBO_BOX)) sequence.append(utils.AssertPresentationAction( "17. Tab to next combo box", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane ComboBox Demo TabList ComboBox Demo Page Mouth: Jeff Combo'", @@ -281,10 +284,10 @@ sequence.append(utils.AssertPresentationAction( # Tab back up to starting state # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TEXT)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TEXT)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) # Toggle the top left button, to return to normal state. sequence.append(TypeAction (" ")) diff --git a/test/keystrokes/java/role_dialog.py b/test/keystrokes/java/role_dialog.py index 9b9f8d4..8e1a576 100644 --- a/test/keystrokes/java/role_dialog.py +++ b/test/keystrokes/java/role_dialog.py @@ -20,11 +20,14 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of dialogs in Java's SwingSet2.""" +import gi +gi.require_version('Atspi', '2.0') +from gi.repository import Atspi from macaroon.playback import * import utils @@ -34,7 +37,7 @@ sequence = MacroSequence() # We wait for the demo to come up and for focus to be on the toggle button # #sequence.append(WaitForWindowActivate("SwingSet2",None)) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) # Wait for entire window to get populated. sequence.append(PauseAction(5000)) @@ -43,52 +46,52 @@ sequence.append(PauseAction(5000)) # Tab over to the JOptionPane demo, and activate it. # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(TypeAction(" ")) ########################################################################## # Tab down to the dialog activation button in the demo. # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Option Pane Demo", acc_role=pyatspi.ROLE_PAGE_TAB)) +sequence.append(WaitForFocus("Option Pane Demo", acc_role=Atspi.Role.PAGE_TAB)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Show Input Dialog", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("Show Input Dialog", acc_role=Atspi.Role.PUSH_BUTTON)) ######################################################################## # Dialog is activated # sequence.append(utils.StartRecordingAction()) sequence.append(TypeAction(" ")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TEXT)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TEXT)) sequence.append(utils.AssertPresentationAction( "1. Dialog is activated", ["BRAILLE LINE: 'SwingSet2 Application Input Dialog'", @@ -110,7 +113,7 @@ sequence.append(KeyComboAction("Return")) # Expected output when "OK" button gets focus. # sequence.append(utils.StartRecordingAction()) -sequence.append(WaitForFocus("OK", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("OK", acc_role=Atspi.Role.PUSH_BUTTON)) sequence.append(utils.AssertPresentationAction( "2. OK button gains focus", ["BUG? - We don't always present anything here. Need to investigate.", @@ -140,17 +143,17 @@ sequence.append(KeyComboAction("Return")) ######################################################################## # Wait for main application to gain focus and return to starting state. # sequence.append(WaitForWindowActivate("SwingSet2",None)) -sequence.append(WaitForFocus("Show Input Dialog", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("Show Input Dialog", acc_role=Atspi.Role.PUSH_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Show Warning Dialog", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("Show Warning Dialog", acc_role=Atspi.Role.PUSH_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Show Message Dialog", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("Show Message Dialog", acc_role=Atspi.Role.PUSH_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Show Component Dialog", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("Show Component Dialog", acc_role=Atspi.Role.PUSH_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Show Confirmation Dialog", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("Show Confirmation Dialog", acc_role=Atspi.Role.PUSH_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TEXT)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TEXT)) sequence.append(KeyComboAction("Tab")) diff --git a/test/keystrokes/java/role_menu.py b/test/keystrokes/java/role_menu.py index 6820ea7..32e6bc7 100644 --- a/test/keystrokes/java/role_menu.py +++ b/test/keystrokes/java/role_menu.py @@ -20,11 +20,14 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of menus in Java's SwingSet2.""" +import gi +gi.require_version('Atspi', '2.0') +from gi.repository import Atspi from macaroon.playback import * import utils @@ -34,7 +37,7 @@ sequence = MacroSequence() # We wait for the demo to come up and for focus to be on the toggle button # #sequence.append(WaitForWindowActivate("SwingSet2",None)) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) # Wait for entire window to get populated. sequence.append(PauseAction(5000)) @@ -42,14 +45,14 @@ sequence.append(PauseAction(5000)) # Hack to deal with a timing issue which seems to interfere with our # setting the locusOfFocus reliably. sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) ########################################################################## # Open File menu # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("f")) -sequence.append(WaitForFocus("File", acc_role=pyatspi.ROLE_MENU)) +sequence.append(WaitForFocus("File", acc_role=Atspi.Role.MENU)) sequence.append(utils.AssertPresentationAction( "1. Open File menu", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane File Menu'", @@ -80,7 +83,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Right")) -sequence.append(WaitForFocus("Look & Feel", acc_role=pyatspi.ROLE_MENU)) +sequence.append(WaitForFocus("Look & Feel", acc_role=Atspi.Role.MENU)) sequence.append(utils.AssertPresentationAction( "3. Move to Look & Feel menu", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Look & Feel Menu'", @@ -108,7 +111,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Right")) -sequence.append(WaitForFocus("Themes", acc_role=pyatspi.ROLE_MENU)) +sequence.append(WaitForFocus("Themes", acc_role=Atspi.Role.MENU)) sequence.append(utils.AssertPresentationAction( "5. Move to Themes menu", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Look & Feel Menu'", @@ -138,7 +141,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) -sequence.append(WaitForFocus("Audio", acc_role=pyatspi.ROLE_MENU)) +sequence.append(WaitForFocus("Audio", acc_role=Atspi.Role.MENU)) sequence.append(utils.AssertPresentationAction( "7. Move to Audio menu", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Swing demo menu bar MenuBar Audio Menu'", diff --git a/test/keystrokes/java/role_page_tab.py b/test/keystrokes/java/role_page_tab.py index 59e9b2d..b9a5b04 100644 --- a/test/keystrokes/java/role_page_tab.py +++ b/test/keystrokes/java/role_page_tab.py @@ -20,11 +20,14 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of page tabs in Java's SwingSet2.""" +import gi +gi.require_version('Atspi', '2.0') +from gi.repository import Atspi from macaroon.playback import * import utils @@ -34,7 +37,7 @@ sequence = MacroSequence() # We wait for the demo to come up and for focus to be on the toggle button # #sequence.append(WaitForWindowActivate("SwingSet2",None)) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) # Wait for entire window to get populated. sequence.append(PauseAction(5000)) @@ -43,57 +46,57 @@ sequence.append(PauseAction(5000)) # Tab over to the JTabbedPane demo, and activate it. # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(TypeAction(" ")) ########################################################################## # Tab all the way down to the demo. # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("TabbedPane Demo", acc_role=pyatspi.ROLE_PAGE_TAB)) +sequence.append(WaitForFocus("TabbedPane Demo", acc_role=Atspi.Role.PAGE_TAB)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Top", acc_role=pyatspi.ROLE_RADIO_BUTTON)) +sequence.append(WaitForFocus("Top", acc_role=Atspi.Role.RADIO_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Left", acc_role=pyatspi.ROLE_RADIO_BUTTON)) +sequence.append(WaitForFocus("Left", acc_role=Atspi.Role.RADIO_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Bottom", acc_role=pyatspi.ROLE_RADIO_BUTTON)) +sequence.append(WaitForFocus("Bottom", acc_role=Atspi.Role.RADIO_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Right", acc_role=pyatspi.ROLE_RADIO_BUTTON)) +sequence.append(WaitForFocus("Right", acc_role=Atspi.Role.RADIO_BUTTON)) ######################################################################## # Expected output when "Laine" tab gets focus. # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Laine", acc_role=pyatspi.ROLE_PAGE_TAB)) +sequence.append(WaitForFocus("Laine", acc_role=Atspi.Role.PAGE_TAB)) sequence.append(utils.AssertPresentationAction( "1. Move to Laine tab", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane TabbedPane Demo TabList TabbedPane Demo Page Laine Page'", @@ -105,7 +108,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Right")) -sequence.append(WaitForFocus("Ewan", acc_role=pyatspi.ROLE_PAGE_TAB)) +sequence.append(WaitForFocus("Ewan", acc_role=Atspi.Role.PAGE_TAB)) sequence.append(utils.AssertPresentationAction( "2. Move to Ewan tab", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane TabbedPane Demo TabList TabbedPane Demo Page Ewan Page'", @@ -117,7 +120,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Right")) -sequence.append(WaitForFocus("Hania", acc_role=pyatspi.ROLE_PAGE_TAB)) +sequence.append(WaitForFocus("Hania", acc_role=Atspi.Role.PAGE_TAB)) sequence.append(utils.AssertPresentationAction( "3. Move to Hania tab", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane TabbedPane Demo TabList TabbedPane Demo Page Hania Page'", @@ -129,7 +132,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Right")) -sequence.append(WaitForFocus("
Bouncing Babies!
", acc_role=pyatspi.ROLE_PAGE_TAB)) +sequence.append(WaitForFocus("
Bouncing Babies!
", acc_role=Atspi.Role.PAGE_TAB)) sequence.append(utils.AssertPresentationAction( "4. Move to Bouncing Babies! tab", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane TabbedPane Demo TabList TabbedPane Demo Page
Bouncing Babies!
Page'", @@ -150,7 +153,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Left")) -sequence.append(WaitForFocus("Hania", acc_role=pyatspi.ROLE_PAGE_TAB)) +sequence.append(WaitForFocus("Hania", acc_role=Atspi.Role.PAGE_TAB)) sequence.append(utils.AssertPresentationAction( "6. Back to Hania tab", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane TabbedPane Demo TabList TabbedPane Demo Page Hania Page'", @@ -171,7 +174,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Left")) -sequence.append(WaitForFocus("Ewan", acc_role=pyatspi.ROLE_PAGE_TAB)) +sequence.append(WaitForFocus("Ewan", acc_role=Atspi.Role.PAGE_TAB)) sequence.append(utils.AssertPresentationAction( "8. Back to Ewan tab", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane TabbedPane Demo TabList TabbedPane Demo Page Ewan Page'", @@ -192,7 +195,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Left")) -sequence.append(WaitForFocus("Laine", acc_role=pyatspi.ROLE_PAGE_TAB)) +sequence.append(WaitForFocus("Laine", acc_role=Atspi.Role.PAGE_TAB)) sequence.append(utils.AssertPresentationAction( "10. Back to Laine tab", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane TabbedPane Demo TabList TabbedPane Demo Page Laine Page'", @@ -212,7 +215,7 @@ sequence.append(utils.AssertPresentationAction( "SPEECH OUTPUT: 'tab list Laine page 1 of 4'"])) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TEXT)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TEXT)) sequence.append(KeyComboAction("Tab")) # Toggle the top left button, to return to normal state. diff --git a/test/keystrokes/java/role_push_button.py b/test/keystrokes/java/role_push_button.py index 1432119..afd4fee 100644 --- a/test/keystrokes/java/role_push_button.py +++ b/test/keystrokes/java/role_push_button.py @@ -20,11 +20,14 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of push buttons in Java's SwingSet2.""" +import gi +gi.require_version('Atspi', '2.0') +from gi.repository import Atspi from macaroon.playback import * import utils @@ -34,7 +37,7 @@ sequence = MacroSequence() # We wait for the demo to come up and for focus to be on the toggle button # #sequence.append(WaitForWindowActivate("SwingSet2",None)) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) # Wait for entire window to get populated. sequence.append(PauseAction(5000)) @@ -43,44 +46,44 @@ sequence.append(PauseAction(5000)) # Tab over to the button demo, and activate it. # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(TypeAction(" ")) ########################################################################## # Tab all the way down to the button page tab. # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Button Demo", acc_role=pyatspi.ROLE_PAGE_TAB)) +sequence.append(WaitForFocus("Button Demo", acc_role=Atspi.Role.PAGE_TAB)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Buttons", acc_role=pyatspi.ROLE_PAGE_TAB)) +sequence.append(WaitForFocus("Buttons", acc_role=Atspi.Role.PAGE_TAB)) sequence.append(PauseAction(5000)) ########################################################################## @@ -88,7 +91,7 @@ sequence.append(PauseAction(5000)) # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("One ", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("One ", acc_role=Atspi.Role.PUSH_BUTTON)) sequence.append(utils.AssertPresentationAction( "1. Move to One button", ["BRAILLE LINE: 'SwingSet2 Application (SwingSet2 Frame RootPane LayeredPane Button Demo TabList Button Demo Page |)Buttons TabList Buttons Page Text Buttons Panel One Button'", @@ -112,7 +115,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Two", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("Two", acc_role=Atspi.Role.PUSH_BUTTON)) sequence.append(utils.AssertPresentationAction( "3. Move to Two button", ["BRAILLE LINE: 'SwingSet2 Application (SwingSet2 Frame RootPane LayeredPane Button Demo TabList Button Demo Page |)Buttons TabList Buttons Page Text Buttons Panel Two Button'", @@ -124,7 +127,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Three!", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("Three!", acc_role=Atspi.Role.PUSH_BUTTON)) sequence.append(utils.AssertPresentationAction( "4. Move to Three button", ["BUG? - What's up with the extra whitespace in the speech?", @@ -140,7 +143,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.PUSH_BUTTON)) sequence.append(utils.AssertPresentationAction( "5. Move to first image button", ["BRAILLE LINE: 'SwingSet2 Application (SwingSet2 Frame RootPane LayeredPane Button Demo TabList Button Demo Page |)Buttons TabList Buttons Page Image Buttons Panel Button'", @@ -152,7 +155,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.PUSH_BUTTON)) sequence.append(utils.AssertPresentationAction( "6. Move to second image button", ["BRAILLE LINE: 'SwingSet2 Application (SwingSet2 Frame RootPane LayeredPane Button Demo TabList Button Demo Page |)Buttons TabList Buttons Page Image Buttons Panel Button'", @@ -164,7 +167,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.PUSH_BUTTON)) sequence.append(utils.AssertPresentationAction( "7. Move to third image button", ["BRAILLE LINE: 'SwingSet2 Application (SwingSet2 Frame RootPane LayeredPane Button Demo TabList Button Demo Page |)Buttons TabList Buttons Page Image Buttons Panel Button'", @@ -174,21 +177,21 @@ sequence.append(utils.AssertPresentationAction( ########################################################################## # Wrap around tabbing to top left toggle button. sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Paint Border", acc_role=pyatspi.ROLE_CHECK_BOX)) +sequence.append(WaitForFocus("Paint Border", acc_role=Atspi.Role.CHECK_BOX)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Paint Focus", acc_role=pyatspi.ROLE_CHECK_BOX)) +sequence.append(WaitForFocus("Paint Focus", acc_role=Atspi.Role.CHECK_BOX)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Enabled", acc_role=pyatspi.ROLE_CHECK_BOX)) +sequence.append(WaitForFocus("Enabled", acc_role=Atspi.Role.CHECK_BOX)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Content Filled", acc_role=pyatspi.ROLE_CHECK_BOX)) +sequence.append(WaitForFocus("Content Filled", acc_role=Atspi.Role.CHECK_BOX)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Default", acc_role=pyatspi.ROLE_RADIO_BUTTON)) +sequence.append(WaitForFocus("Default", acc_role=Atspi.Role.RADIO_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("0", acc_role=pyatspi.ROLE_RADIO_BUTTON)) +sequence.append(WaitForFocus("0", acc_role=Atspi.Role.RADIO_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("10", acc_role=pyatspi.ROLE_RADIO_BUTTON)) +sequence.append(WaitForFocus("10", acc_role=Atspi.Role.RADIO_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TEXT)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TEXT)) sequence.append(KeyComboAction("Tab")) # Toggle the top left button, to return to normal state. diff --git a/test/keystrokes/java/role_radio_button.py b/test/keystrokes/java/role_radio_button.py index a91be06..a9f39eb 100644 --- a/test/keystrokes/java/role_radio_button.py +++ b/test/keystrokes/java/role_radio_button.py @@ -20,11 +20,14 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of radio buttons in Java's SwingSet2.""" +import gi +gi.require_version('Atspi', '2.0') +from gi.repository import Atspi from macaroon.playback import * import utils @@ -34,7 +37,7 @@ sequence = MacroSequence() # We wait for the demo to come up and for focus to be on the toggle button # #sequence.append(WaitForWindowActivate("SwingSet2",None)) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) # Wait for entire window to get populated. sequence.append(PauseAction(5000)) @@ -43,50 +46,50 @@ sequence.append(PauseAction(5000)) # Tab over to the button demo, and activate it. # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(TypeAction(" ")) ########################################################################## # Tab all the way down to the button page tab. # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Button Demo", acc_role=pyatspi.ROLE_PAGE_TAB)) +sequence.append(WaitForFocus("Button Demo", acc_role=Atspi.Role.PAGE_TAB)) sequence.append(KeyComboAction("Tab")) ########################################################################## # Select Check Boxes tab # -sequence.append(WaitForFocus("Buttons", acc_role=pyatspi.ROLE_PAGE_TAB)) +sequence.append(WaitForFocus("Buttons", acc_role=Atspi.Role.PAGE_TAB)) sequence.append(KeyComboAction("Right")) -sequence.append(WaitForFocus("Radio Buttons", acc_role=pyatspi.ROLE_PAGE_TAB)) +sequence.append(WaitForFocus("Radio Buttons", acc_role=Atspi.Role.PAGE_TAB)) sequence.append(PauseAction(5000)) ########################################################################## @@ -94,7 +97,7 @@ sequence.append(PauseAction(5000)) # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Radio One ", acc_role=pyatspi.ROLE_RADIO_BUTTON)) +sequence.append(WaitForFocus("Radio One ", acc_role=Atspi.Role.RADIO_BUTTON)) sequence.append(utils.AssertPresentationAction( "1. Move to Radio One radio button", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Button Demo TabList Button Demo Page Radio Buttons TabList Radio Buttons Page Text Radio Buttons Panel Text Radio Buttons & y Radio One RadioButton'", @@ -111,7 +114,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(TypeAction(" ")) sequence.append(WaitAction("object:property-change:accessible-value", None, - None, pyatspi.ROLE_RADIO_BUTTON, 5000)) + None, Atspi.Role.RADIO_BUTTON, 5000)) sequence.append(utils.AssertPresentationAction( "2. Select the focused radio button", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Button Demo TabList Button Demo Page Radio Buttons TabList Radio Buttons Page Text Radio Buttons Panel Text Radio Buttons &=y Radio One RadioButton'", @@ -123,7 +126,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Radio Two", acc_role=pyatspi.ROLE_RADIO_BUTTON)) +sequence.append(WaitForFocus("Radio Two", acc_role=Atspi.Role.RADIO_BUTTON)) sequence.append(utils.AssertPresentationAction( "3. Move to Radio Two radio button", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Button Demo TabList Button Demo Page Radio Buttons TabList Radio Buttons Page Text Radio Buttons Panel Text Radio Buttons & y Radio Two RadioButton'", @@ -136,7 +139,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(TypeAction(" ")) sequence.append(WaitAction("object:property-change:accessible-value", None, - None, pyatspi.ROLE_RADIO_BUTTON, 5000)) + None, Atspi.Role.RADIO_BUTTON, 5000)) sequence.append(utils.AssertPresentationAction( "4. Select the focused radio button", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Button Demo TabList Button Demo Page Radio Buttons TabList Radio Buttons Page Text Radio Buttons Panel Text Radio Buttons &=y Radio Two RadioButton'", @@ -148,7 +151,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Radio Three", acc_role=pyatspi.ROLE_RADIO_BUTTON)) +sequence.append(WaitForFocus("Radio Three", acc_role=Atspi.Role.RADIO_BUTTON)) sequence.append(utils.AssertPresentationAction( "5. Move to Radio Three radio button", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Button Demo TabList Button Demo Page Radio Buttons TabList Radio Buttons Page Text Radio Buttons Panel Text Radio Buttons & y Radio Three RadioButton'", @@ -161,7 +164,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(TypeAction(" ")) sequence.append(WaitAction("object:property-change:accessible-value", None, - None, pyatspi.ROLE_RADIO_BUTTON, 5000)) + None, Atspi.Role.RADIO_BUTTON, 5000)) sequence.append(utils.AssertPresentationAction( "6. Select the focused radio button", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Button Demo TabList Button Demo Page Radio Buttons TabList Radio Buttons Page Text Radio Buttons Panel Text Radio Buttons &=y Radio Three RadioButton'", @@ -173,7 +176,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Radio One ", acc_role=pyatspi.ROLE_RADIO_BUTTON)) +sequence.append(WaitForFocus("Radio One ", acc_role=Atspi.Role.RADIO_BUTTON)) sequence.append(utils.AssertPresentationAction( "7. Move to Radio One radio button", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Button Demo TabList Button Demo Page Radio Buttons TabList Radio Buttons Page Image Radio Buttons Panel Image Radio Buttons & y Radio One RadioButton'", @@ -198,7 +201,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(TypeAction(" ")) sequence.append(WaitAction("object:property-change:accessible-value", None, - None, pyatspi.ROLE_RADIO_BUTTON, 5000)) + None, Atspi.Role.RADIO_BUTTON, 5000)) sequence.append(utils.AssertPresentationAction( "9. Select the focused radio button", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Button Demo TabList Button Demo Page Radio Buttons TabList Radio Buttons Page Image Radio Buttons Panel Image Radio Buttons &=y Radio One RadioButton'", @@ -222,7 +225,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Radio Two", acc_role=pyatspi.ROLE_RADIO_BUTTON)) +sequence.append(WaitForFocus("Radio Two", acc_role=Atspi.Role.RADIO_BUTTON)) sequence.append(utils.AssertPresentationAction( "11. Move to Radio Two radio button", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Button Demo TabList Button Demo Page Radio Buttons TabList Radio Buttons Page Image Radio Buttons Panel Image Radio Buttons & y Radio Two RadioButton'", @@ -247,7 +250,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(TypeAction(" ")) sequence.append(WaitAction("object:property-change:accessible-value", None, - None, pyatspi.ROLE_RADIO_BUTTON, 5000)) + None, Atspi.Role.RADIO_BUTTON, 5000)) sequence.append(utils.AssertPresentationAction( "13. Select the focused radio button", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Button Demo TabList Button Demo Page Radio Buttons TabList Radio Buttons Page Image Radio Buttons Panel Image Radio Buttons &=y Radio Two RadioButton'", @@ -271,7 +274,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Radio Three", acc_role=pyatspi.ROLE_RADIO_BUTTON)) +sequence.append(WaitForFocus("Radio Three", acc_role=Atspi.Role.RADIO_BUTTON)) sequence.append(utils.AssertPresentationAction( "15. Move to Radio Three radio button", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Button Demo TabList Button Demo Page Radio Buttons TabList Radio Buttons Page Image Radio Buttons Panel Image Radio Buttons & y Radio Three RadioButton'", @@ -284,7 +287,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(TypeAction(" ")) sequence.append(WaitAction("object:property-change:accessible-value", None, - None, pyatspi.ROLE_RADIO_BUTTON, 5000)) + None, Atspi.Role.RADIO_BUTTON, 5000)) sequence.append(utils.AssertPresentationAction( "16. Select the focused radio button", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Button Demo TabList Button Demo Page Radio Buttons TabList Radio Buttons Page Image Radio Buttons Panel Image Radio Buttons &=y Radio Three RadioButton'", @@ -293,21 +296,21 @@ sequence.append(utils.AssertPresentationAction( # Tab back up to beginning sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Paint Border", acc_role=pyatspi.ROLE_CHECK_BOX)) +sequence.append(WaitForFocus("Paint Border", acc_role=Atspi.Role.CHECK_BOX)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Paint Focus", acc_role=pyatspi.ROLE_CHECK_BOX)) +sequence.append(WaitForFocus("Paint Focus", acc_role=Atspi.Role.CHECK_BOX)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Enabled", acc_role=pyatspi.ROLE_CHECK_BOX)) +sequence.append(WaitForFocus("Enabled", acc_role=Atspi.Role.CHECK_BOX)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Content Filled", acc_role=pyatspi.ROLE_CHECK_BOX)) +sequence.append(WaitForFocus("Content Filled", acc_role=Atspi.Role.CHECK_BOX)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Default", acc_role=pyatspi.ROLE_RADIO_BUTTON)) +sequence.append(WaitForFocus("Default", acc_role=Atspi.Role.RADIO_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("0", acc_role=pyatspi.ROLE_RADIO_BUTTON)) +sequence.append(WaitForFocus("0", acc_role=Atspi.Role.RADIO_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("10", acc_role=pyatspi.ROLE_RADIO_BUTTON)) +sequence.append(WaitForFocus("10", acc_role=Atspi.Role.RADIO_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TEXT)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TEXT)) sequence.append(KeyComboAction("Tab")) # Toggle the top left button, to return to normal state. diff --git a/test/keystrokes/java/role_radio_menu_item.py b/test/keystrokes/java/role_radio_menu_item.py index 364b5fc..55edac1 100644 --- a/test/keystrokes/java/role_radio_menu_item.py +++ b/test/keystrokes/java/role_radio_menu_item.py @@ -20,11 +20,14 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of radio menu items in Java's SwingSet2.""" +import gi +gi.require_version('Atspi', '2.0') +from gi.repository import Atspi from macaroon.playback import * import utils @@ -34,16 +37,16 @@ sequence = MacroSequence() # We wait for the demo to come up and for focus to be on the toggle button # #sequence.append(WaitForWindowActivate("SwingSet2",None)) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(PauseAction(5000)) ########################################################################## # Invoke Themes menu sequence.append(KeyComboAction("t")) -sequence.append(WaitForFocus("Audio", acc_role=pyatspi.ROLE_MENU)) +sequence.append(WaitForFocus("Audio", acc_role=Atspi.Role.MENU)) sequence.append(KeyComboAction("Down")) -sequence.append(WaitForFocus("Fonts", acc_role=pyatspi.ROLE_MENU)) +sequence.append(WaitForFocus("Fonts", acc_role=Atspi.Role.MENU)) sequence.append(PauseAction(5000)) ########################################################################## @@ -51,7 +54,7 @@ sequence.append(PauseAction(5000)) # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) -sequence.append(WaitForFocus("Ocean", acc_role=pyatspi.ROLE_RADIO_MENU_ITEM)) +sequence.append(WaitForFocus("Ocean", acc_role=Atspi.Role.RADIO_MENU_ITEM)) sequence.append(utils.AssertPresentationAction( "1. Down Arrow to Ocean", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Swing demo menu bar MenuBar Fonts Menu'", @@ -79,7 +82,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) -sequence.append(WaitForFocus("Steel", acc_role=pyatspi.ROLE_RADIO_MENU_ITEM)) +sequence.append(WaitForFocus("Steel", acc_role=Atspi.Role.RADIO_MENU_ITEM)) sequence.append(utils.AssertPresentationAction( "3. Down Arrow to Steel", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Swing demo menu bar MenuBar & y Steel RadioItem'", @@ -105,7 +108,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) -sequence.append(WaitForFocus("Aqua", acc_role=pyatspi.ROLE_RADIO_MENU_ITEM)) +sequence.append(WaitForFocus("Aqua", acc_role=Atspi.Role.RADIO_MENU_ITEM)) sequence.append(utils.AssertPresentationAction( "5. Down Arrow to Aqua", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Swing demo menu bar MenuBar & y Aqua RadioItem'", @@ -117,7 +120,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) -sequence.append(WaitForFocus("Charcoal", acc_role=pyatspi.ROLE_RADIO_MENU_ITEM)) +sequence.append(WaitForFocus("Charcoal", acc_role=Atspi.Role.RADIO_MENU_ITEM)) sequence.append(utils.AssertPresentationAction( "6. Down Arrow to Charcoal", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Swing demo menu bar MenuBar & y Charcoal RadioItem'", @@ -129,7 +132,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) -sequence.append(WaitForFocus("High Contrast", acc_role=pyatspi.ROLE_RADIO_MENU_ITEM)) +sequence.append(WaitForFocus("High Contrast", acc_role=Atspi.Role.RADIO_MENU_ITEM)) sequence.append(utils.AssertPresentationAction( "7. Down Arrow to High Contrast", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Swing demo menu bar MenuBar & y High Contrast RadioItem'", @@ -141,7 +144,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) -sequence.append(WaitForFocus("Emerald", acc_role=pyatspi.ROLE_RADIO_MENU_ITEM)) +sequence.append(WaitForFocus("Emerald", acc_role=Atspi.Role.RADIO_MENU_ITEM)) sequence.append(utils.AssertPresentationAction( "8. Down Arrow to Emerald", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Swing demo menu bar MenuBar & y Emerald RadioItem'", @@ -153,7 +156,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) -sequence.append(WaitForFocus("Ruby", acc_role=pyatspi.ROLE_RADIO_MENU_ITEM)) +sequence.append(WaitForFocus("Ruby", acc_role=Atspi.Role.RADIO_MENU_ITEM)) sequence.append(utils.AssertPresentationAction( "9. Down Arrow to Ruby", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Swing demo menu bar MenuBar & y Ruby RadioItem'", @@ -165,7 +168,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Up")) -sequence.append(WaitForFocus("Emerald", acc_role=pyatspi.ROLE_RADIO_MENU_ITEM)) +sequence.append(WaitForFocus("Emerald", acc_role=Atspi.Role.RADIO_MENU_ITEM)) sequence.append(utils.AssertPresentationAction( "10. Up Arrow to Emerald", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Swing demo menu bar MenuBar & y Emerald RadioItem'", @@ -177,7 +180,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Up")) -sequence.append(WaitForFocus("High Contrast", acc_role=pyatspi.ROLE_RADIO_MENU_ITEM)) +sequence.append(WaitForFocus("High Contrast", acc_role=Atspi.Role.RADIO_MENU_ITEM)) sequence.append(utils.AssertPresentationAction( "11. Up Arrow to High Contrast", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Swing demo menu bar MenuBar & y High Contrast RadioItem'", @@ -189,7 +192,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Up")) -sequence.append(WaitForFocus("Charcoal", acc_role=pyatspi.ROLE_RADIO_MENU_ITEM)) +sequence.append(WaitForFocus("Charcoal", acc_role=Atspi.Role.RADIO_MENU_ITEM)) sequence.append(utils.AssertPresentationAction( "12. Up Arrow to Charcoal", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Swing demo menu bar MenuBar & y Charcoal RadioItem'", @@ -201,7 +204,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Up")) -sequence.append(WaitForFocus("Aqua", acc_role=pyatspi.ROLE_RADIO_MENU_ITEM)) +sequence.append(WaitForFocus("Aqua", acc_role=Atspi.Role.RADIO_MENU_ITEM)) sequence.append(utils.AssertPresentationAction( "13. Up Arrow to Aqua", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Swing demo menu bar MenuBar & y Aqua RadioItem'", @@ -213,7 +216,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Up")) -sequence.append(WaitForFocus("Steel", acc_role=pyatspi.ROLE_RADIO_MENU_ITEM)) +sequence.append(WaitForFocus("Steel", acc_role=Atspi.Role.RADIO_MENU_ITEM)) sequence.append(utils.AssertPresentationAction( "14. Up Arrow to Steel", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Swing demo menu bar MenuBar & y Steel RadioItem'", @@ -226,7 +229,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(TypeAction(" ")) sequence.append(WaitAction("object:property-change:accessible-value", None, - None, pyatspi.ROLE_RADIO_MENU_ITEM, 5000)) + None, Atspi.Role.RADIO_MENU_ITEM, 5000)) sequence.append(utils.AssertPresentationAction( "15. Select the radio menu item", ["BUG? - Why are we speaking JInternalFrame demo? Also, some times the state of the toggle button is wrong. Need to investigate.", @@ -238,16 +241,16 @@ sequence.append(utils.AssertPresentationAction( "SPEECH OUTPUT: 'JInternalFrame demo toggle button pressed'"])) sequence.append(KeyComboAction("t")) -sequence.append(WaitForFocus("Audio", acc_role=pyatspi.ROLE_MENU)) +sequence.append(WaitForFocus("Audio", acc_role=Atspi.Role.MENU)) sequence.append(KeyComboAction("Down")) -sequence.append(WaitForFocus("Fonts", acc_role=pyatspi.ROLE_MENU)) +sequence.append(WaitForFocus("Fonts", acc_role=Atspi.Role.MENU)) ########################################################################## # Expected output when radio menu item gets focused. # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) -sequence.append(WaitForFocus("Ocean", acc_role=pyatspi.ROLE_RADIO_MENU_ITEM)) +sequence.append(WaitForFocus("Ocean", acc_role=Atspi.Role.RADIO_MENU_ITEM)) sequence.append(utils.AssertPresentationAction( "16. Down Arrow to Ocean", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Swing demo menu bar MenuBar Fonts Menu'", @@ -261,7 +264,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) -sequence.append(WaitForFocus("Steel", acc_role=pyatspi.ROLE_RADIO_MENU_ITEM)) +sequence.append(WaitForFocus("Steel", acc_role=Atspi.Role.RADIO_MENU_ITEM)) sequence.append(utils.AssertPresentationAction( "17. Down Arrow to Steel", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Swing demo menu bar MenuBar &=y Steel RadioItem'", @@ -273,7 +276,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) -sequence.append(WaitForFocus("Aqua", acc_role=pyatspi.ROLE_RADIO_MENU_ITEM)) +sequence.append(WaitForFocus("Aqua", acc_role=Atspi.Role.RADIO_MENU_ITEM)) sequence.append(utils.AssertPresentationAction( "18. Down Arrow to Aqua", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Swing demo menu bar MenuBar & y Aqua RadioItem'", @@ -285,7 +288,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Up")) -sequence.append(WaitForFocus("Steel", acc_role=pyatspi.ROLE_RADIO_MENU_ITEM)) +sequence.append(WaitForFocus("Steel", acc_role=Atspi.Role.RADIO_MENU_ITEM)) sequence.append(utils.AssertPresentationAction( "19. Up Arrow to Steel", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Swing demo menu bar MenuBar &=y Steel RadioItem'", @@ -297,7 +300,7 @@ sequence.append(utils.AssertPresentationAction( # sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Up")) -sequence.append(WaitForFocus("Ocean", acc_role=pyatspi.ROLE_RADIO_MENU_ITEM)) +sequence.append(WaitForFocus("Ocean", acc_role=Atspi.Role.RADIO_MENU_ITEM)) sequence.append(utils.AssertPresentationAction( "20. Up Arrow to Ocean", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Swing demo menu bar MenuBar & y Ocean RadioItem'", @@ -306,7 +309,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(TypeAction(" ")) sequence.append(WaitAction("object:property-change:accessible-value", None, - None, pyatspi.ROLE_RADIO_MENU_ITEM, 5000)) + None, Atspi.Role.RADIO_MENU_ITEM, 5000)) # Just a little extra wait to let some events get through. # diff --git a/test/keystrokes/java/role_table.py b/test/keystrokes/java/role_table.py index 861aeb9..715103d 100644 --- a/test/keystrokes/java/role_table.py +++ b/test/keystrokes/java/role_table.py @@ -20,11 +20,14 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of push buttons in Java's SwingSet2.""" +import gi +gi.require_version('Atspi', '2.0') +from gi.repository import Atspi from macaroon.playback import * import utils @@ -34,7 +37,7 @@ sequence = MacroSequence() # We wait for the demo to come up and for focus to be on the toggle button # #sequence.append(WaitForWindowActivate("SwingSet2",None)) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) # Wait for entire window to get populated. sequence.append(PauseAction(5000)) @@ -43,70 +46,70 @@ sequence.append(PauseAction(5000)) # Tab over to the button demo, and activate it. # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(TypeAction(" ")) ########################################################################## # Tab all the way down to the table. # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Table Demo", acc_role=pyatspi.ROLE_PAGE_TAB)) +sequence.append(WaitForFocus("Table Demo", acc_role=Atspi.Role.PAGE_TAB)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Reordering allowed", acc_role=pyatspi.ROLE_CHECK_BOX)) +sequence.append(WaitForFocus("Reordering allowed", acc_role=Atspi.Role.CHECK_BOX)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Row selection", acc_role=pyatspi.ROLE_CHECK_BOX)) +sequence.append(WaitForFocus("Row selection", acc_role=Atspi.Role.CHECK_BOX)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Horiz. Lines", acc_role=pyatspi.ROLE_CHECK_BOX)) +sequence.append(WaitForFocus("Horiz. Lines", acc_role=Atspi.Role.CHECK_BOX)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Column selection", acc_role=pyatspi.ROLE_CHECK_BOX)) +sequence.append(WaitForFocus("Column selection", acc_role=Atspi.Role.CHECK_BOX)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Vert. Lines", acc_role=pyatspi.ROLE_CHECK_BOX)) +sequence.append(WaitForFocus("Vert. Lines", acc_role=Atspi.Role.CHECK_BOX)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Inter-cell spacing", acc_role=pyatspi.ROLE_SLIDER)) +sequence.append(WaitForFocus("Inter-cell spacing", acc_role=Atspi.Role.SLIDER)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Row height", acc_role=pyatspi.ROLE_SLIDER)) +sequence.append(WaitForFocus("Row height", acc_role=Atspi.Role.SLIDER)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Multiple ranges", acc_role=pyatspi.ROLE_COMBO_BOX)) +sequence.append(WaitForFocus("Multiple ranges", acc_role=Atspi.Role.COMBO_BOX)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Subsequent columns", acc_role=pyatspi.ROLE_COMBO_BOX)) +sequence.append(WaitForFocus("Subsequent columns", acc_role=Atspi.Role.COMBO_BOX)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TEXT)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TEXT)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TEXT)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TEXT)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Fit Width", acc_role=pyatspi.ROLE_CHECK_BOX)) +sequence.append(WaitForFocus("Fit Width", acc_role=Atspi.Role.CHECK_BOX)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Print", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("Print", acc_role=Atspi.Role.PUSH_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TABLE)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TABLE)) ########################################################################## # Expected output when focus is on "Mike" cell: @@ -114,7 +117,7 @@ sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TABLE)) sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Right")) sequence.append(WaitAction("object:selection-changed", None, None, - pyatspi.ROLE_TABLE, 5000)) + Atspi.Role.TABLE, 5000)) sequence.append(utils.AssertPresentationAction( "1. Control Right Arrow into the cell", ["BUG? - No output when navigating JTable with cursor. See bug 483214."])) @@ -125,7 +128,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Right")) sequence.append(WaitAction("object:selection-changed", None, None, - pyatspi.ROLE_TABLE, 5000)) + Atspi.Role.TABLE, 5000)) sequence.append(utils.AssertPresentationAction( "2. Control Right Arrow into the cell", ["BUG? - No output when navigating JTable with cursor. See bug 483214."])) @@ -154,7 +157,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Right")) sequence.append(WaitAction("object:selection-changed", None, None, - pyatspi.ROLE_TABLE, 5000)) + Atspi.Role.TABLE, 5000)) sequence.append(utils.AssertPresentationAction( "4 Control Right Arrow into the cell", ["BUG? - No output when navigating JTable with cursor. See bug 483214."])) @@ -165,7 +168,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Right")) sequence.append(WaitAction("object:selection-changed", None, None, - pyatspi.ROLE_TABLE, 5000)) + Atspi.Role.TABLE, 5000)) sequence.append(utils.AssertPresentationAction( "5. Control Right Arrow into the cell", ["BUG? - No output when navigating JTable with cursor. See bug 483214."])) @@ -176,7 +179,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Right")) sequence.append(WaitAction("object:selection-changed", None, None, - pyatspi.ROLE_TABLE, 5000)) + Atspi.Role.TABLE, 5000)) sequence.append(utils.AssertPresentationAction( "6. Control Right Arrow into the cell", ["BUG? - No output when navigating JTable with cursor. See bug 483214."])) @@ -187,7 +190,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Right")) sequence.append(WaitAction("object:selection-changed", None, None, - pyatspi.ROLE_TABLE, 5000)) + Atspi.Role.TABLE, 5000)) sequence.append(utils.AssertPresentationAction( "7. Control Right Arrow into the cell", ["BUG? - No output when navigating JTable with cursor. See bug 483214."])) @@ -198,7 +201,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) sequence.append(WaitAction("object:selection-changed", None, None, - pyatspi.ROLE_TABLE, 5000)) + Atspi.Role.TABLE, 5000)) sequence.append(utils.AssertPresentationAction( "8. Control Down Arrow into the cell", ["BUG? - No output when navigating JTable with cursor. See bug 483214."])) @@ -209,7 +212,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Left")) sequence.append(WaitAction("object:selection-changed", None, None, - pyatspi.ROLE_TABLE, 5000)) + Atspi.Role.TABLE, 5000)) sequence.append(utils.AssertPresentationAction( "9. Control Left into the cell", ["BUG? - No output when navigating JTable with cursor. See bug 483214."])) @@ -220,7 +223,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Left")) sequence.append(WaitAction("object:selection-changed", None, None, - pyatspi.ROLE_TABLE, 5000)) + Atspi.Role.TABLE, 5000)) sequence.append(utils.AssertPresentationAction( "10. Control Left into the cell", ["BUG? - No output when navigating JTable with cursor. See bug 483214."])) @@ -231,7 +234,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Left")) sequence.append(WaitAction("object:selection-changed", None, None, - pyatspi.ROLE_TABLE, 5000)) + Atspi.Role.TABLE, 5000)) sequence.append(utils.AssertPresentationAction( "11. Control Left into the cell", ["BUG? - No output when navigating JTable with cursor. See bug 483214."])) @@ -242,7 +245,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Left")) sequence.append(WaitAction("object:selection-changed", None, None, - pyatspi.ROLE_TABLE, 5000)) + Atspi.Role.TABLE, 5000)) sequence.append(utils.AssertPresentationAction( "12. Control Left into the cell", ["BUG? - No output when navigating JTable with cursor. See bug 483214."])) @@ -253,7 +256,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(TypeAction(" ")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TABLE, 5000)) + Atspi.Role.TABLE, 5000)) sequence.append(utils.AssertPresentationAction( "13. Space Bar on the cell", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Table Demo TabList Table Demo Page ScrollPane Viewport Table Last Name ColumnHeader Andrews'", @@ -286,7 +289,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Return")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TABLE, 5000)) + Atspi.Role.TABLE, 5000)) sequence.append(utils.AssertPresentationAction( "16. Press Return", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Table Demo TabList Table Demo Page ScrollPane Viewport Table Last Name ColumnHeader Beck'", @@ -299,7 +302,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Left")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TABLE, 5000)) + Atspi.Role.TABLE, 5000)) sequence.append(utils.AssertPresentationAction( "17. Press Left Arrow", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Table Demo TabList Table Demo Page ScrollPane Viewport Table First Name ColumnHeader Brian'", @@ -312,7 +315,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Up")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TABLE, 5000)) + Atspi.Role.TABLE, 5000)) sequence.append(utils.AssertPresentationAction( "18. Press Up Arrow", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Table Demo TabList Table Demo Page ScrollPane Viewport Table First Name ColumnHeader Mark'", @@ -325,7 +328,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Right")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TABLE, 5000)) + Atspi.Role.TABLE, 5000)) sequence.append(utils.AssertPresentationAction( "19. Press Right Arrow", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Table Demo TabList Table Demo Page ScrollPane Viewport Table Last Name ColumnHeader Andy'", @@ -357,7 +360,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Return")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TABLE, 5000)) + Atspi.Role.TABLE, 5000)) sequence.append(utils.AssertPresentationAction( "22. Press Return", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Table Demo TabList Table Demo Page ScrollPane Viewport Table Last Name ColumnHeader Beck'", @@ -370,7 +373,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Left")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TABLE, 5000)) + Atspi.Role.TABLE, 5000)) sequence.append(utils.AssertPresentationAction( "23. Press Left Arrow", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Table Demo TabList Table Demo Page ScrollPane Viewport Table First Name ColumnHeader Brian'", @@ -383,7 +386,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Up")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TABLE, 5000)) + Atspi.Role.TABLE, 5000)) sequence.append(utils.AssertPresentationAction( "24. Shift Up Arrow to select the row", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Table Demo TabList Table Demo Page ScrollPane Viewport Table First Name ColumnHeader Mark'", @@ -396,7 +399,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Up")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TABLE, 5000)) + Atspi.Role.TABLE, 5000)) sequence.append(utils.AssertPresentationAction( "25. Shift Up Arrow to select the row", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Table Demo TabList Table Demo Page ScrollPane Viewport Table First Name ColumnHeader Mike'", @@ -421,16 +424,16 @@ sequence.append(utils.AssertPresentationAction( sequence.append(KeyComboAction("Right")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TABLE, 5000)) + Atspi.Role.TABLE, 5000)) sequence.append(KeyComboAction("Left")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TABLE, 5000)) + Atspi.Role.TABLE, 5000)) ########################################################################## # Leave table. sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TEXT)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TEXT)) sequence.append(KeyComboAction("Tab")) # Toggle the top left button, to return to normal state. diff --git a/test/keystrokes/java/role_tree.py b/test/keystrokes/java/role_tree.py index c1540a6..bb8bdd3 100644 --- a/test/keystrokes/java/role_tree.py +++ b/test/keystrokes/java/role_tree.py @@ -20,11 +20,14 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of push buttons in Java's SwingSet2.""" +import gi +gi.require_version('Atspi', '2.0') +from gi.repository import Atspi from macaroon.playback import * import utils @@ -34,7 +37,7 @@ sequence = MacroSequence() # We wait for the demo to come up and for focus to be on the toggle button # #sequence.append(WaitForWindowActivate("SwingSet2",None)) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) # Wait for entire window to get populated. sequence.append(PauseAction(5000)) @@ -43,43 +46,43 @@ sequence.append(PauseAction(5000)) # Tab over to the button demo, and activate it. # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TOGGLE_BUTTON)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TOGGLE_BUTTON)) sequence.append(TypeAction(" ")) ########################################################################## # Tab all the way down to the tree. # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Tree Demo", acc_role=pyatspi.ROLE_PAGE_TAB)) +sequence.append(WaitForFocus("Tree Demo", acc_role=Atspi.Role.PAGE_TAB)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TREE)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TREE)) ########################################################################## # Expected output when node is selected: @@ -87,7 +90,7 @@ sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TREE)) sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TREE, 5000)) + Atspi.Role.TREE, 5000)) sequence.append(utils.AssertPresentationAction( "1. Down Arrow in the tree", ["BRAILLE LINE: 'SwingSet2 Application SwingSet2 Frame RootPane LayeredPane Tree Demo TabList Tree Demo Page ScrollPane Viewport Tree Music expanded TREE LEVEL 1'", @@ -100,7 +103,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TREE, 5000)) + Atspi.Role.TREE, 5000)) sequence.append(utils.AssertPresentationAction( "2. Down Arrow in the tree", ["BRAILLE LINE: 'SwingSet2 Application Classical collapsed TREE LEVEL 2'", @@ -113,7 +116,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TREE, 5000)) + Atspi.Role.TREE, 5000)) sequence.append(utils.AssertPresentationAction( "3. Down Arrow in the tree", ["BRAILLE LINE: 'SwingSet2 Application Jazz collapsed TREE LEVEL 2'", @@ -126,7 +129,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Right")) sequence.append(WaitAction("object:state-changed:expanded", None, None, - pyatspi.ROLE_LABEL, 5000)) + Atspi.Role.LABEL, 5000)) sequence.append(utils.AssertPresentationAction( "4. Right Arrow in the tree", ["BRAILLE LINE: 'SwingSet2 Application Jazz expanded TREE LEVEL 2'", @@ -139,7 +142,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TREE, 5000)) + Atspi.Role.TREE, 5000)) sequence.append(utils.AssertPresentationAction( "5. Down Arrow in the tree", ["BRAILLE LINE: 'SwingSet2 Application Albert Ayler collapsed TREE LEVEL 3'", @@ -152,7 +155,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TREE, 5000)) + Atspi.Role.TREE, 5000)) sequence.append(utils.AssertPresentationAction( "6. Down Arrow in the tree", ["BRAILLE LINE: 'SwingSet2 Application Chet Baker collapsed TREE LEVEL 3'", @@ -165,7 +168,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Right")) sequence.append(WaitAction("object:state-changed:expanded", None, None, - pyatspi.ROLE_LABEL, 5000)) + Atspi.Role.LABEL, 5000)) sequence.append(utils.AssertPresentationAction( "7. Right Arrow in the tree", ["BRAILLE LINE: 'SwingSet2 Application Chet Baker expanded TREE LEVEL 3'", @@ -178,7 +181,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TREE, 5000)) + Atspi.Role.TREE, 5000)) sequence.append(utils.AssertPresentationAction( "8. Down Arrow in the tree", ["BRAILLE LINE: 'SwingSet2 Application Sings and Plays collapsed TREE LEVEL 4'", @@ -191,7 +194,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TREE, 5000)) + Atspi.Role.TREE, 5000)) sequence.append(utils.AssertPresentationAction( "9. Down Arrow in the tree", ["BRAILLE LINE: 'SwingSet2 Application My Funny Valentine collapsed TREE LEVEL 4'", @@ -204,7 +207,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TREE, 5000)) + Atspi.Role.TREE, 5000)) sequence.append(utils.AssertPresentationAction( "10. Down Arrow in the tree", ["BRAILLE LINE: 'SwingSet2 Application Grey December collapsed TREE LEVEL 4'", @@ -229,7 +232,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Right")) sequence.append(WaitAction("object:state-changed:expanded", None, None, - pyatspi.ROLE_LABEL, 5000)) + Atspi.Role.LABEL, 5000)) sequence.append(utils.AssertPresentationAction( "12. Right Arrow in the tree", ["BRAILLE LINE: 'SwingSet2 Application Grey December expanded TREE LEVEL 4'", @@ -254,7 +257,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TREE, 5000)) + Atspi.Role.TREE, 5000)) sequence.append(utils.AssertPresentationAction( "14. Down Arrow in the tree", ["BRAILLE LINE: 'SwingSet2 Application Grey December TREE LEVEL 5'", @@ -267,7 +270,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TREE, 5000)) + Atspi.Role.TREE, 5000)) sequence.append(utils.AssertPresentationAction( "15. Down Arrow in the tree", ["BRAILLE LINE: 'SwingSet2 Application I Wish I Knew TREE LEVEL 5'", @@ -280,7 +283,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Down")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TREE, 5000)) + Atspi.Role.TREE, 5000)) sequence.append(utils.AssertPresentationAction( "16. Down Arrow in the tree", ["BRAILLE LINE: 'SwingSet2 Application Someone To Watch Over Me TREE LEVEL 5'", @@ -305,7 +308,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Up")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TREE, 5000)) + Atspi.Role.TREE, 5000)) sequence.append(utils.AssertPresentationAction( "18. Up Arrow in the tree", ["BRAILLE LINE: 'SwingSet2 Application I Wish I Knew TREE LEVEL 5'", @@ -318,7 +321,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Up")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TREE, 5000)) + Atspi.Role.TREE, 5000)) sequence.append(utils.AssertPresentationAction( "19. Up Arrow in the tree", ["BRAILLE LINE: 'SwingSet2 Application Grey December TREE LEVEL 5'", @@ -331,7 +334,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Up")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TREE, 5000)) + Atspi.Role.TREE, 5000)) sequence.append(utils.AssertPresentationAction( "20. Up Arrow in the tree", ["BRAILLE LINE: 'SwingSet2 Application Grey December expanded TREE LEVEL 4'", @@ -344,7 +347,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Left")) sequence.append(WaitAction("object:state-changed:expanded", None, None, - pyatspi.ROLE_LABEL, 5000)) + Atspi.Role.LABEL, 5000)) sequence.append(utils.AssertPresentationAction( "21. Left Arrow in the tree", ["BRAILLE LINE: 'SwingSet2 Application Grey December collapsed TREE LEVEL 4'", @@ -357,7 +360,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Up")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TREE, 5000)) + Atspi.Role.TREE, 5000)) sequence.append(utils.AssertPresentationAction( "22. Up Arrow in the tree", ["BRAILLE LINE: 'SwingSet2 Application My Funny Valentine collapsed TREE LEVEL 4'", @@ -370,7 +373,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Up")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TREE, 5000)) + Atspi.Role.TREE, 5000)) sequence.append(utils.AssertPresentationAction( "23. Up Arrow in the tree", ["BRAILLE LINE: 'SwingSet2 Application Sings and Plays collapsed TREE LEVEL 4'", @@ -383,7 +386,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Up")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TREE, 5000)) + Atspi.Role.TREE, 5000)) sequence.append(utils.AssertPresentationAction( "24. Up Arrow in the tree", ["BRAILLE LINE: 'SwingSet2 Application Chet Baker expanded TREE LEVEL 3'", @@ -396,7 +399,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Left")) sequence.append(WaitAction("object:state-changed:expanded", None, None, - pyatspi.ROLE_LABEL, 5000)) + Atspi.Role.LABEL, 5000)) sequence.append(utils.AssertPresentationAction( "25. Left Arrow in the tree", ["BRAILLE LINE: 'SwingSet2 Application Chet Baker collapsed TREE LEVEL 3'", @@ -409,7 +412,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Up")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TREE, 5000)) + Atspi.Role.TREE, 5000)) sequence.append(utils.AssertPresentationAction( "26. Up Arrow in the tree", ["BRAILLE LINE: 'SwingSet2 Application Albert Ayler collapsed TREE LEVEL 3'", @@ -422,7 +425,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Up")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TREE, 5000)) + Atspi.Role.TREE, 5000)) sequence.append(utils.AssertPresentationAction( "27. Up Arrow in the tree", ["BRAILLE LINE: 'SwingSet2 Application Jazz expanded TREE LEVEL 2'", @@ -435,7 +438,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Left")) sequence.append(WaitAction("object:state-changed:expanded", None, None, - pyatspi.ROLE_LABEL, 5000)) + Atspi.Role.LABEL, 5000)) sequence.append(utils.AssertPresentationAction( "28. Left Arrow in the tree", ["BRAILLE LINE: 'SwingSet2 Application Jazz collapsed TREE LEVEL 2'", @@ -448,7 +451,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Up")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TREE, 5000)) + Atspi.Role.TREE, 5000)) sequence.append(utils.AssertPresentationAction( "29. Up Arrow in the tree", ["BRAILLE LINE: 'SwingSet2 Application Classical collapsed TREE LEVEL 2'", @@ -461,7 +464,7 @@ sequence.append(utils.AssertPresentationAction( sequence.append(utils.StartRecordingAction()) sequence.append(KeyComboAction("Up")) sequence.append(WaitAction("object:active-descendant-changed", None, None, - pyatspi.ROLE_TREE, 5000)) + Atspi.Role.TREE, 5000)) sequence.append(utils.AssertPresentationAction( "30. Up Arrow in the tree", ["BUG? - Seems a bit chatty", @@ -473,7 +476,7 @@ sequence.append(utils.AssertPresentationAction( # Leave tree # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TEXT)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TEXT)) sequence.append(KeyComboAction("Tab")) diff --git a/test/keystrokes/oobase/bug_463172.py b/test/keystrokes/oobase/bug_463172.py index 9cc9812..35586df 100644 --- a/test/keystrokes/oobase/bug_463172.py +++ b/test/keystrokes/oobase/bug_463172.py @@ -20,13 +20,16 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test to verify bug #463172 is still fixed. OOo sbase application crashes when entering a database record. """ +import gi +gi.require_version('Atspi', '2.0') +from gi.repository import Atspi from macaroon.playback import * sequence = MacroSequence() @@ -36,7 +39,7 @@ sequence = MacroSequence() # appear. # sequence.append(WaitForWindowActivate("Database Wizard", None)) -sequence.append(WaitForFocus("Select database", acc_role=pyatspi.ROLE_LABEL)) +sequence.append(WaitForFocus("Select database", acc_role=Atspi.Role.LABEL)) ###################################################################### # 2. Press Return to get to the second screen of the startup wizard. @@ -46,7 +49,7 @@ sequence.append(WaitForFocus("Select database", acc_role=pyatspi.ROLE_LABEL)) # SPEECH OUTPUT: 'Save and proceed label' # sequence.append(KeyComboAction("Return")) -sequence.append(WaitForFocus("Save and proceed", acc_role=pyatspi.ROLE_LABEL)) +sequence.append(WaitForFocus("Save and proceed", acc_role=Atspi.Role.LABEL)) ###################################################################### # 3. Press Tab to get to the database registration radio buttons. @@ -56,7 +59,7 @@ sequence.append(WaitForFocus("Save and proceed", acc_role=pyatspi.ROLE_LABEL)) # SPEECH OUTPUT: 'Yes, register the database for me selected radio button' # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Yes, register the database for me", acc_role=pyatspi.ROLE_RADIO_BUTTON)) +sequence.append(WaitForFocus("Yes, register the database for me", acc_role=Atspi.Role.RADIO_BUTTON)) ###################################################################### # 4. Press down arrow to not register this database. @@ -66,7 +69,7 @@ sequence.append(WaitForFocus("Yes, register the database for me", acc_role=pyats # SPEECH OUTPUT: 'No, do not register the database selected radio button' # sequence.append(KeyComboAction("Down")) -sequence.append(WaitForFocus("No, do not register the database", acc_role=pyatspi.ROLE_RADIO_BUTTON)) +sequence.append(WaitForFocus("No, do not register the database", acc_role=Atspi.Role.RADIO_BUTTON)) sequence.append(PauseAction(3000)) ###################################################################### @@ -78,7 +81,7 @@ sequence.append(PauseAction(3000)) # SPEECH OUTPUT: 'Files table' # sequence.append(KeyComboAction("Return")) -sequence.append(WaitForFocus("Files", acc_role=pyatspi.ROLE_TABLE)) +sequence.append(WaitForFocus("Files", acc_role=Atspi.Role.TABLE)) sequence.append(PauseAction(3000)) ###################################################################### @@ -97,19 +100,19 @@ sequence.append(PauseAction(3000)) # sequence.append(KeyComboAction("Return")) sequence.append(WaitForWindowActivate("New Database - OpenOffice.org Base", None)) -sequence.append(WaitForFocus("IconChoiceControl", acc_role=pyatspi.ROLE_TREE)) +sequence.append(WaitForFocus("IconChoiceControl", acc_role=Atspi.Role.TREE)) ###################################################################### # 7. Enter Alt-f, Alt-c to close the database window. # sequence.append(KeyComboAction("f")) -sequence.append(WaitForFocus("New", acc_role=pyatspi.ROLE_MENU)) +sequence.append(WaitForFocus("New", acc_role=Atspi.Role.MENU)) sequence.append(KeyComboAction("c")) sequence.append(WaitAction("object:property-change:accessible-name", None, None, - pyatspi.ROLE_ROOT_PANE, + Atspi.Role.ROOT_PANE, 30000)) ###################################################################### diff --git a/test/keystrokes/oobase/bug_465109.py b/test/keystrokes/oobase/bug_465109.py index 8cd470f..7b43df4 100644 --- a/test/keystrokes/oobase/bug_465109.py +++ b/test/keystrokes/oobase/bug_465109.py @@ -20,13 +20,16 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test to verify bug #465109 is still fixed. OOo sbase application crashes when entering a database record. """ +import gi +gi.require_version('Atspi', '2.0') +from gi.repository import Atspi from macaroon.playback import * sequence = MacroSequence() @@ -49,10 +52,10 @@ sequence.append(WaitForWindowActivate("bug_465109 - OpenOffice.org Base", None)) # SPEECH OUTPUT: 'Tables label' # sequence.append(KeyComboAction("v")) -sequence.append(WaitForFocus("Database Objects", acc_role=pyatspi.ROLE_MENU)) +sequence.append(WaitForFocus("Database Objects", acc_role=Atspi.Role.MENU)) sequence.append(KeyComboAction("Right")) -sequence.append(WaitForFocus("Tables", acc_role=pyatspi.ROLE_MENU_ITEM)) +sequence.append(WaitForFocus("Tables", acc_role=Atspi.Role.MENU_ITEM)) sequence.append(KeyComboAction("Return")) @@ -61,13 +64,13 @@ sequence.append(KeyComboAction("Return")) # Tables pane. # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TREE)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TREE)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("None", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("None", acc_role=Atspi.Role.PUSH_BUTTON)) sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TREE)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TREE)) ###################################################################### # 4. Enter down arrow and Return to bring up a separate window showing @@ -82,7 +85,7 @@ sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TREE)) sequence.append(KeyComboAction("Down")) sequence.append(KeyComboAction("Return")) sequence.append(WaitForWindowActivate("bug_465109: NameAddrPhone", None)) -sequence.append(WaitForFocus("Data source table view", acc_role=pyatspi.ROLE_PANEL)) +sequence.append(WaitForFocus("Data source table view", acc_role=Atspi.Role.PANEL)) ###################################################################### # 5. Press Tab to get focus into the LastName field and enter "smith". @@ -93,7 +96,7 @@ sequence.append(WaitForFocus("Data source table view", acc_role=pyatspi.ROLE_PAN # SPEECH OUTPUT: 'text ' # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TEXT)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TEXT)) sequence.append(TypeAction("smith")) @@ -101,7 +104,7 @@ sequence.append(TypeAction("smith")) # 6. Press Tab to get focus into the City field and enter "san francisco". # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TEXT)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TEXT)) sequence.append(TypeAction("san francisco")) @@ -110,7 +113,7 @@ sequence.append(TypeAction("san francisco")) # "california". # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TEXT)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TEXT)) sequence.append(TypeAction("california")) @@ -119,7 +122,7 @@ sequence.append(TypeAction("california")) # "415-555-1212". # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TEXT)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TEXT)) sequence.append(TypeAction("415-555-1212")) @@ -127,33 +130,33 @@ sequence.append(TypeAction("415-555-1212")) # 9. Enter Alt-f, up arrow and Return to select Exit from the File menu. # sequence.append(KeyComboAction("f")) -sequence.append(WaitForFocus("New", acc_role=pyatspi.ROLE_MENU)) +sequence.append(WaitForFocus("New", acc_role=Atspi.Role.MENU)) sequence.append(KeyComboAction("Up")) -sequence.append(WaitForFocus("Exit", acc_role=pyatspi.ROLE_MENU_ITEM)) +sequence.append(WaitForFocus("Exit", acc_role=Atspi.Role.MENU_ITEM)) sequence.append(KeyComboAction("Return")) -sequence.append(WaitForFocus("Yes", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("Yes", acc_role=Atspi.Role.PUSH_BUTTON)) ###################################################################### # 10. Press Tab and Return to not save the current changes. # This dismisses the NameAddrPhone table window. # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("No", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("No", acc_role=Atspi.Role.PUSH_BUTTON)) sequence.append(KeyComboAction("Return")) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_TREE)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.TREE)) ###################################################################### # 9. Enter Alt-f, up arrow and Return to select Exit from the File menu. # of the main oobase window. # sequence.append(KeyComboAction("f")) -sequence.append(WaitForFocus("New", acc_role=pyatspi.ROLE_MENU)) +sequence.append(WaitForFocus("New", acc_role=Atspi.Role.MENU)) sequence.append(KeyComboAction("Up")) -sequence.append(WaitForFocus("Exit", acc_role=pyatspi.ROLE_MENU_ITEM)) +sequence.append(WaitForFocus("Exit", acc_role=Atspi.Role.MENU_ITEM)) sequence.append(KeyComboAction("Return")) diff --git a/test/keystrokes/oocalc/coordinate_announcement_off.py b/test/keystrokes/oocalc/coordinate_announcement_off.py index c4e5acb..54335f2 100644 --- a/test/keystrokes/oocalc/coordinate_announcement_off.py +++ b/test/keystrokes/oocalc/coordinate_announcement_off.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test presentation when coordinate announcement is off""" diff --git a/test/keystrokes/oocalc/coordinate_announcement_on.py b/test/keystrokes/oocalc/coordinate_announcement_on.py index 651f813..2a082c1 100644 --- a/test/keystrokes/oocalc/coordinate_announcement_on.py +++ b/test/keystrokes/oocalc/coordinate_announcement_on.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test presentation when coordinate announcement is on""" diff --git a/test/keystrokes/oocalc/document_enter_text.py b/test/keystrokes/oocalc/document_enter_text.py index 5f0050f..71e9c4e 100644 --- a/test/keystrokes/oocalc/document_enter_text.py +++ b/test/keystrokes/oocalc/document_enter_text.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of presentation when typing in a cell.""" diff --git a/test/keystrokes/oocalc/document_enter_text_no_context.py b/test/keystrokes/oocalc/document_enter_text_no_context.py index b82ac39..33c8947 100644 --- a/test/keystrokes/oocalc/document_enter_text_no_context.py +++ b/test/keystrokes/oocalc/document_enter_text_no_context.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of presentation when typing in a cell.""" diff --git a/test/keystrokes/oocalc/document_nav_dynamic_headers.py b/test/keystrokes/oocalc/document_nav_dynamic_headers.py index 7c2ec33..83c849b 100644 --- a/test/keystrokes/oocalc/document_nav_dynamic_headers.py +++ b/test/keystrokes/oocalc/document_nav_dynamic_headers.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test dynamic header support.""" diff --git a/test/keystrokes/oocalc/document_text_attributes.py b/test/keystrokes/oocalc/document_text_attributes.py index 15ab597..236841d 100644 --- a/test/keystrokes/oocalc/document_text_attributes.py +++ b/test/keystrokes/oocalc/document_text_attributes.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test presentation of character attributes.""" diff --git a/test/keystrokes/oocalc/manage_names_combobox.py b/test/keystrokes/oocalc/manage_names_combobox.py index 49c4fda..a73a8ea 100644 --- a/test/keystrokes/oocalc/manage_names_combobox.py +++ b/test/keystrokes/oocalc/manage_names_combobox.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test presentation of the Manage Names combobox""" diff --git a/test/keystrokes/oocalc/manage_names_combobox_no_context.py b/test/keystrokes/oocalc/manage_names_combobox_no_context.py index 0ff8ffc..41a1315 100644 --- a/test/keystrokes/oocalc/manage_names_combobox_no_context.py +++ b/test/keystrokes/oocalc/manage_names_combobox_no_context.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test presentation of the Manage Names combobox""" diff --git a/test/keystrokes/oocalc/messages_dynamic_headers.py b/test/keystrokes/oocalc/messages_dynamic_headers.py index cacd3da..1fe8dfa 100644 --- a/test/keystrokes/oocalc/messages_dynamic_headers.py +++ b/test/keystrokes/oocalc/messages_dynamic_headers.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of messages associated with dynamic headers.""" diff --git a/test/keystrokes/oocalc/ui_role_check_menu_item.py b/test/keystrokes/oocalc/ui_role_check_menu_item.py index 72fd208..1584f64 100644 --- a/test/keystrokes/oocalc/ui_role_check_menu_item.py +++ b/test/keystrokes/oocalc/ui_role_check_menu_item.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test presentation of checked menu item state.""" diff --git a/test/keystrokes/ooimpress/bug_462239.py b/test/keystrokes/ooimpress/bug_462239.py index 0c8e596..8d3b8d4 100644 --- a/test/keystrokes/ooimpress/bug_462239.py +++ b/test/keystrokes/ooimpress/bug_462239.py @@ -20,14 +20,17 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test to verify bug #462239 is still fixed. OpenOffice OOo-dev 2.3.0 Presentation application crashes when trying to open an existing presentation. """ +import gi +gi.require_version('Atspi', '2.0') +from gi.repository import Atspi from macaroon.playback import * sequence = MacroSequence() @@ -48,13 +51,13 @@ sequence.append(WaitForWindowActivate("subtlewaves - OpenOffice.org Impress", No # 2. Enter Alt-f, Alt-c to close the presentation window. # sequence.append(KeyComboAction("f")) -sequence.append(WaitForFocus("New", acc_role=pyatspi.ROLE_MENU)) +sequence.append(WaitForFocus("New", acc_role=Atspi.Role.MENU)) sequence.append(KeyComboAction("c")) sequence.append(WaitAction("object:property-change:accessible-name", None, None, - pyatspi.ROLE_ROOT_PANE, + Atspi.Role.ROOT_PANE, 30000)) ###################################################################### diff --git a/test/keystrokes/ooimpress/bug_462256.py b/test/keystrokes/ooimpress/bug_462256.py index 1999658..b0fae26 100644 --- a/test/keystrokes/ooimpress/bug_462256.py +++ b/test/keystrokes/ooimpress/bug_462256.py @@ -20,14 +20,17 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test to verify bug #462256 is still fixed. Cthulhu doesn't speak/braille anything when going to the 2nd screen in the OOo Presentation startup wizard. """ +import gi +gi.require_version('Atspi', '2.0') +from gi.repository import Atspi from macaroon.playback import * sequence = MacroSequence() @@ -42,7 +45,7 @@ sequence = MacroSequence() # SPEECH OUTPUT: 'Next >> button' # sequence.append(WaitForWindowActivate("Presentation Wizard", None)) -sequence.append(WaitForFocus("Next", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("Next", acc_role=Atspi.Role.PUSH_BUTTON)) ###################################################################### # 2. Press Return to get to the second screen of the startup wizard. @@ -62,7 +65,7 @@ sequence.append(KeyComboAction("Return")) # SPEECH OUTPUT: 'Create button' # sequence.append(KeyComboAction("Return")) -sequence.append(WaitForFocus("Create", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("Create", acc_role=Atspi.Role.PUSH_BUTTON)) ###################################################################### # 4. Press Return to start up ooimpress with an empty presentation. @@ -75,22 +78,22 @@ sequence.append(WaitForFocus("Create", acc_role=pyatspi.ROLE_PUSH_BUTTON)) # sequence.append(KeyComboAction("Return")) sequence.append(WaitForWindowActivate("Untitled1 - OpenOffice.org Impress", None)) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_SCROLL_PANE)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.SCROLL_PANE)) ###################################################################### # 5. Enter Alt-f, Alt-c to close the presentation window. # sequence.append(KeyComboAction("f")) -sequence.append(WaitForFocus("New", acc_role=pyatspi.ROLE_MENU)) +sequence.append(WaitForFocus("New", acc_role=Atspi.Role.MENU)) sequence.append(KeyComboAction("c")) -sequence.append(WaitForFocus("Save", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("Save", acc_role=Atspi.Role.PUSH_BUTTON)) ###################################################################### # 6. Enter Tab and Return to discard the current changes. # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Discard", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("Discard", acc_role=Atspi.Role.PUSH_BUTTON)) sequence.append(KeyComboAction("Return")) diff --git a/test/keystrokes/ooimpress/bug_462547.py b/test/keystrokes/ooimpress/bug_462547.py index 47dcf97..88b8ca7 100644 --- a/test/keystrokes/ooimpress/bug_462547.py +++ b/test/keystrokes/ooimpress/bug_462547.py @@ -20,13 +20,16 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test to verify bug #462547 is still fixed. OOo-dev 2.3.0 simpress application startup wizard hangs the desktop. """ +import gi +gi.require_version('Atspi', '2.0') +from gi.repository import Atspi from macaroon.playback import * sequence = MacroSequence() @@ -41,7 +44,7 @@ sequence = MacroSequence() # SPEECH OUTPUT: 'Next >> button' # sequence.append(WaitForWindowActivate("Presentation Wizard", None)) -sequence.append(WaitForFocus("Next", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("Next", acc_role=Atspi.Role.PUSH_BUTTON)) ###################################################################### # 2. Press Space to get to the second screen of the startup wizard. @@ -61,7 +64,7 @@ sequence.append(KeyComboAction("space")) # SPEECH OUTPUT: 'Create button' # sequence.append(KeyComboAction("space")) -sequence.append(WaitForFocus("Create", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("Create", acc_role=Atspi.Role.PUSH_BUTTON)) ###################################################################### # 4. Press Space to start up ooimpress with an empty presentation. @@ -74,22 +77,22 @@ sequence.append(WaitForFocus("Create", acc_role=pyatspi.ROLE_PUSH_BUTTON)) # sequence.append(KeyComboAction("space")) sequence.append(WaitForWindowActivate("Untitled1 - OpenOffice.org Impress", None)) -sequence.append(WaitForFocus("", acc_role=pyatspi.ROLE_SCROLL_PANE)) +sequence.append(WaitForFocus("", acc_role=Atspi.Role.SCROLL_PANE)) ###################################################################### # 5. Enter Alt-f, Alt-c to close the presentation window. # sequence.append(KeyComboAction("f")) -sequence.append(WaitForFocus("New", acc_role=pyatspi.ROLE_MENU)) +sequence.append(WaitForFocus("New", acc_role=Atspi.Role.MENU)) sequence.append(KeyComboAction("c")) -sequence.append(WaitForFocus("Save", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("Save", acc_role=Atspi.Role.PUSH_BUTTON)) ###################################################################### # 6. Enter Tab and Return to discard the current changes. # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Discard", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("Discard", acc_role=Atspi.Role.PUSH_BUTTON)) sequence.append(KeyComboAction("Return")) diff --git a/test/keystrokes/ooimpress/bug_465449.py b/test/keystrokes/ooimpress/bug_465449.py index 4ee6db5..f72f209 100644 --- a/test/keystrokes/ooimpress/bug_465449.py +++ b/test/keystrokes/ooimpress/bug_465449.py @@ -20,13 +20,16 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test to verify bug #465449 is still fixed. OOo simpress crashes when trying to change view modes. """ +import gi +gi.require_version('Atspi', '2.0') +from gi.repository import Atspi from macaroon.playback import * sequence = MacroSequence() @@ -52,28 +55,28 @@ sequence.append(WaitForWindowActivate("subtlewaves - OpenOffice.org Impress", No # SPEECH OUTPUT: 'scroll pane' # sequence.append(KeyComboAction("v")) -sequence.append(WaitForFocus("Normal", acc_role=pyatspi.ROLE_MENU_ITEM)) +sequence.append(WaitForFocus("Normal", acc_role=Atspi.Role.MENU_ITEM)) sequence.append(KeyComboAction("Down")) -sequence.append(WaitForFocus("Outline", acc_role=pyatspi.ROLE_MENU_ITEM)) +sequence.append(WaitForFocus("Outline", acc_role=Atspi.Role.MENU_ITEM)) sequence.append(KeyComboAction("Return")) -sequence.append(WaitForFocus("Paragraph 0", acc_role=pyatspi.ROLE_PARAGRAPH)) +sequence.append(WaitForFocus("Paragraph 0", acc_role=Atspi.Role.PARAGRAPH)) ###################################################################### # 3. Enter Alt-f, Alt-c to close the presentation window. # sequence.append(KeyComboAction("f")) -sequence.append(WaitForFocus("New", acc_role=pyatspi.ROLE_MENU)) +sequence.append(WaitForFocus("New", acc_role=Atspi.Role.MENU)) sequence.append(KeyComboAction("c")) -sequence.append(WaitForFocus("Save", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("Save", acc_role=Atspi.Role.PUSH_BUTTON)) ###################################################################### # 4. Enter Tab and Return to discard any changes. # sequence.append(KeyComboAction("Tab")) -sequence.append(WaitForFocus("Discard", acc_role=pyatspi.ROLE_PUSH_BUTTON)) +sequence.append(WaitForFocus("Discard", acc_role=Atspi.Role.PUSH_BUTTON)) sequence.append(KeyComboAction("Return")) ###################################################################### diff --git a/test/keystrokes/oowriter/document_nav_cell.py b/test/keystrokes/oowriter/document_nav_cell.py index 56398cd..c65a67e 100644 --- a/test/keystrokes/oowriter/document_nav_cell.py +++ b/test/keystrokes/oowriter/document_nav_cell.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of speech output when tabbing amongst table cells.""" diff --git a/test/keystrokes/oowriter/document_nav_line.py b/test/keystrokes/oowriter/document_nav_line.py index 719b702..b63c041 100644 --- a/test/keystrokes/oowriter/document_nav_line.py +++ b/test/keystrokes/oowriter/document_nav_line.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test presentation of caret navigation by line.""" diff --git a/test/keystrokes/oowriter/document_nav_line_bullets.py b/test/keystrokes/oowriter/document_nav_line_bullets.py index ef1592f..1c14a8b 100644 --- a/test/keystrokes/oowriter/document_nav_line_bullets.py +++ b/test/keystrokes/oowriter/document_nav_line_bullets.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test presentation of caret navigation by line in list with bullets.""" diff --git a/test/keystrokes/oowriter/document_nav_paragraph.py b/test/keystrokes/oowriter/document_nav_paragraph.py index f228d1b..c262118 100644 --- a/test/keystrokes/oowriter/document_nav_paragraph.py +++ b/test/keystrokes/oowriter/document_nav_paragraph.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of presentation of caret navigation by paragraph.""" diff --git a/test/keystrokes/oowriter/document_nav_word.py b/test/keystrokes/oowriter/document_nav_word.py index bdfab0a..6c98452 100644 --- a/test/keystrokes/oowriter/document_nav_word.py +++ b/test/keystrokes/oowriter/document_nav_word.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test presentation of caret navigation by word.""" diff --git a/test/keystrokes/oowriter/document_new.py b/test/keystrokes/oowriter/document_new.py index 89258cf..f1fce1e 100644 --- a/test/keystrokes/oowriter/document_new.py +++ b/test/keystrokes/oowriter/document_new.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test to verify what is announced when you create a new document.""" diff --git a/test/keystrokes/oowriter/flat_review_context_menu.py b/test/keystrokes/oowriter/flat_review_context_menu.py index 3f94a4c..bd0d18d 100644 --- a/test/keystrokes/oowriter/flat_review_context_menu.py +++ b/test/keystrokes/oowriter/flat_review_context_menu.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/oowriter/flat_review_line.py b/test/keystrokes/oowriter/flat_review_line.py index 3a20ae0..a8bda6e 100644 --- a/test/keystrokes/oowriter/flat_review_line.py +++ b/test/keystrokes/oowriter/flat_review_line.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test flat review by line.""" diff --git a/test/keystrokes/oowriter/flat_review_line_columns.py b/test/keystrokes/oowriter/flat_review_line_columns.py index a4c4fa0..d95aa00 100644 --- a/test/keystrokes/oowriter/flat_review_line_columns.py +++ b/test/keystrokes/oowriter/flat_review_line_columns.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test flat review in multi-columned text.""" diff --git a/test/keystrokes/oowriter/flat_review_platform_menubar.py b/test/keystrokes/oowriter/flat_review_platform_menubar.py index 7025720..19150cc 100644 --- a/test/keystrokes/oowriter/flat_review_platform_menubar.py +++ b/test/keystrokes/oowriter/flat_review_platform_menubar.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test flat review on menubar.""" diff --git a/test/keystrokes/oowriter/flat_review_word.py b/test/keystrokes/oowriter/flat_review_word.py index 8384bab..863deaf 100644 --- a/test/keystrokes/oowriter/flat_review_word.py +++ b/test/keystrokes/oowriter/flat_review_word.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test flat review by word.""" diff --git a/test/keystrokes/oowriter/messages_table.py b/test/keystrokes/oowriter/messages_table.py index ad923bb..7eedddf 100644 --- a/test/keystrokes/oowriter/messages_table.py +++ b/test/keystrokes/oowriter/messages_table.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test to verify table message presentation.""" diff --git a/test/keystrokes/oowriter/messages_table_no_context.py b/test/keystrokes/oowriter/messages_table_no_context.py index c6b35eb..b99d5fe 100644 --- a/test/keystrokes/oowriter/messages_table_no_context.py +++ b/test/keystrokes/oowriter/messages_table_no_context.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test to verify table message presentation.""" diff --git a/test/keystrokes/oowriter/say_all.py b/test/keystrokes/oowriter/say_all.py index 7dd39ec..c94eebd 100644 --- a/test/keystrokes/oowriter/say_all.py +++ b/test/keystrokes/oowriter/say_all.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test to verify SayAll works in Writer.""" diff --git a/test/keystrokes/oowriter/say_all_no_sentences.py b/test/keystrokes/oowriter/say_all_no_sentences.py index 2e02ce6..c934712 100644 --- a/test/keystrokes/oowriter/say_all_no_sentences.py +++ b/test/keystrokes/oowriter/say_all_no_sentences.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test SayAll presentation in document without sentence punctuation.""" diff --git a/test/keystrokes/oowriter/say_all_table.py b/test/keystrokes/oowriter/say_all_table.py index 2b50b15..88db0c2 100644 --- a/test/keystrokes/oowriter/say_all_table.py +++ b/test/keystrokes/oowriter/say_all_table.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test to verify SayAll works in Writer.""" diff --git a/test/keystrokes/oowriter/say_all_table_no_context.py b/test/keystrokes/oowriter/say_all_table_no_context.py index 5075d58..392fbb7 100644 --- a/test/keystrokes/oowriter/say_all_table_no_context.py +++ b/test/keystrokes/oowriter/say_all_table_no_context.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test to verify SayAll works in Writer.""" diff --git a/test/keystrokes/oowriter/selection_word.py b/test/keystrokes/oowriter/selection_word.py index 9d811ab..82c86bd 100644 --- a/test/keystrokes/oowriter/selection_word.py +++ b/test/keystrokes/oowriter/selection_word.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of Cthulhu's presentation of Writer word navigation.""" diff --git a/test/keystrokes/oowriter/spellcheck.py b/test/keystrokes/oowriter/spellcheck.py index cc75f72..fd7d22b 100644 --- a/test/keystrokes/oowriter/spellcheck.py +++ b/test/keystrokes/oowriter/spellcheck.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test to verify spell checking support.""" diff --git a/test/keystrokes/oowriter/spoken_indentation.py b/test/keystrokes/oowriter/spoken_indentation.py index 29c89fd..1928485 100644 --- a/test/keystrokes/oowriter/spoken_indentation.py +++ b/test/keystrokes/oowriter/spoken_indentation.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of presentation of indentation.""" diff --git a/test/keystrokes/oowriter/structural_nav_table.py b/test/keystrokes/oowriter/structural_nav_table.py index e35aa9f..95eaff3 100644 --- a/test/keystrokes/oowriter/structural_nav_table.py +++ b/test/keystrokes/oowriter/structural_nav_table.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test for structural navigation amongst table cells in Writer.""" diff --git a/test/keystrokes/oowriter/table_cell_row.py b/test/keystrokes/oowriter/table_cell_row.py index 6503080..77022ef 100644 --- a/test/keystrokes/oowriter/table_cell_row.py +++ b/test/keystrokes/oowriter/table_cell_row.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of cell and row reading in Writer tables.""" diff --git a/test/keystrokes/oowriter/text_attributes.py b/test/keystrokes/oowriter/text_attributes.py index 158e17a..06ba378 100644 --- a/test/keystrokes/oowriter/text_attributes.py +++ b/test/keystrokes/oowriter/text_attributes.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test presentation of character attributes.""" diff --git a/test/keystrokes/oowriter/ui_find.py b/test/keystrokes/oowriter/ui_find.py index c111be6..d212091 100644 --- a/test/keystrokes/oowriter/ui_find.py +++ b/test/keystrokes/oowriter/ui_find.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu from macaroon.playback import * import utils diff --git a/test/keystrokes/oowriter/ui_navigator.py b/test/keystrokes/oowriter/ui_navigator.py index 31399b0..8496c87 100644 --- a/test/keystrokes/oowriter/ui_navigator.py +++ b/test/keystrokes/oowriter/ui_navigator.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test to verify presentation of the navigator.""" diff --git a/test/keystrokes/oowriter/ui_role_combo_box.py b/test/keystrokes/oowriter/ui_role_combo_box.py index 30f9d2e..aad7083 100644 --- a/test/keystrokes/oowriter/ui_role_combo_box.py +++ b/test/keystrokes/oowriter/ui_role_combo_box.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of Cthulhu's presentation of a combo box.""" diff --git a/test/keystrokes/oowriter/ui_role_label.py b/test/keystrokes/oowriter/ui_role_label.py index 1469e4a..e3e9634 100644 --- a/test/keystrokes/oowriter/ui_role_label.py +++ b/test/keystrokes/oowriter/ui_role_label.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test to verify presentation of focusable labels.""" diff --git a/test/keystrokes/oowriter/ui_role_list_item.py b/test/keystrokes/oowriter/ui_role_list_item.py index d308f73..c165657 100644 --- a/test/keystrokes/oowriter/ui_role_list_item.py +++ b/test/keystrokes/oowriter/ui_role_list_item.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test to verify presentation of selectable list items.""" diff --git a/test/keystrokes/oowriter/ui_role_menu.py b/test/keystrokes/oowriter/ui_role_menu.py index bf5102d..043f2d6 100644 --- a/test/keystrokes/oowriter/ui_role_menu.py +++ b/test/keystrokes/oowriter/ui_role_menu.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test to verify result of entering and escaping out of a submenu.""" diff --git a/test/keystrokes/oowriter/ui_role_menu_flat_review.py b/test/keystrokes/oowriter/ui_role_menu_flat_review.py index 6cd70d4..fa5e3e3 100644 --- a/test/keystrokes/oowriter/ui_role_menu_flat_review.py +++ b/test/keystrokes/oowriter/ui_role_menu_flat_review.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of menu and menu item output.""" diff --git a/test/keystrokes/oowriter/ui_role_toolbar.py b/test/keystrokes/oowriter/ui_role_toolbar.py index 1b707fd..dda2776 100644 --- a/test/keystrokes/oowriter/ui_role_toolbar.py +++ b/test/keystrokes/oowriter/ui_role_toolbar.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of Cthulhu's presentation of Writer toolbar buttons.""" diff --git a/test/keystrokes/oowriter/where_am_i_document.py b/test/keystrokes/oowriter/where_am_i_document.py index 3a88b41..168417c 100644 --- a/test/keystrokes/oowriter/where_am_i_document.py +++ b/test/keystrokes/oowriter/where_am_i_document.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test WhereAmI presentation.""" diff --git a/test/keystrokes/progressbar/progress_updates.py b/test/keystrokes/progressbar/progress_updates.py index bda4af4..c4b28dc 100644 --- a/test/keystrokes/progressbar/progress_updates.py +++ b/test/keystrokes/progressbar/progress_updates.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of progressbar output using custom program.""" diff --git a/test/keystrokes/slider/slider.py b/test/keystrokes/slider/slider.py index fac9958..fbfb841 100644 --- a/test/keystrokes/slider/slider.py +++ b/test/keystrokes/slider/slider.py @@ -20,8 +20,8 @@ # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # -# Fork of Orca Screen Reader (GNOME) -# Original source: https://gitlab.gnome.org/GNOME/orca +# Forked from Orca screen reader. +# Cthulhu project: https://git.stormux.org/storm/cthulhu """Test of slider output using custom program."""