pax_global_header00006660000000000000000000000064152016315670014520gustar00rootroot0000000000000052 comment=cdfa9c233b596f4c97f8fa7c925015759baa9345 dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/000077500000000000000000000000001520163156700210205ustar00rootroot00000000000000dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/.fmf/000077500000000000000000000000001520163156700216465ustar00rootroot00000000000000dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/.fmf/version000066400000000000000000000000011520163156700232450ustar00rootroot000000000000001dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/.gitignore000066400000000000000000000001531520163156700230070ustar00rootroot00000000000000api_docs/* build/* dist/* MANIFEST *.pyc dogtail.egg-info/* dogtail/dogtail.egg-info/* .testing _version.pydogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/.gitlab-ci.yml000066400000000000000000000051161520163156700234570ustar00rootroot00000000000000image: python:3 stages: - lint - check_version - build # - upload_testpypi - automatic_release - deploy_to_git variables: PACKAGE_DIR: "dist" lint_code: stage: lint script: - pip install ruff - ruff check . - python -m compileall dogtail/ only: - tags - branches check_version: stage: check_version rules: - if: '$CI_COMMIT_TAG' # Only run on tag pushes. script: - pip install build setuptools-scm # or whatever dynamic version plugin you use # Get the version that pyproject-build will produce - VERSION=$(python -m setuptools_scm) - | echo "Detected version from setuptools-scm: $VERSION" # Strip 'v' prefix from tag if present - TAG_VERSION=${CI_COMMIT_TAG#v} - | echo "Git tag version: $TAG_VERSION" # Compare - | if [ "$VERSION" != "$TAG_VERSION" ]; then echo "❌ Tag ($TAG_VERSION) does not match project version ($VERSION)" exit 1 else echo "✅ Tag matches project version." fi build_package: stage: build before_script: - pip install build script: - pyproject-build artifacts: paths: - $PACKAGE_DIR only: - tags # Only relevant if pushing to test.pypi is wanted. #- branches dependencies: - check_version #upload_to_testpypi: # stage: upload_testpypi # script: # - twine upload --repository-url https://test.pypi.org/legacy/ -u "$TWINE_USERNAME_TESTPYPI" -p "$TWINE_PASSWORD_TESTPYPI" $PACKAGE_DIR/* --verbose # only: # - tags # # Only relevant if pushing to test.pypi is wanted. # #- branches # dependencies: # - build_package # when: manual upload_to_pypi: stage: automatic_release script: - python --version - pip install --upgrade pip setuptools wheel twine - twine upload -u "$TWINE_USERNAME" -p "$TWINE_PASSWORD" $PACKAGE_DIR/* --verbose only: - tags dependencies: - build_package # Considered the version to be released manually after all checks are done. # But once the tag is pushed, we want the version to be released always. Lets not wait for user. # when: manual publish-index: stage: deploy_to_git # only: # refs: # - master before_script: - pip install build twine script: - pyproject-build - python -m twine upload --repository-url ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi dist/* -u $PACKAGE_REGISTRY_USERNAME -p $PACKAGE_REGISTRY_PASSWORD --verbose artifacts: paths: - dist when: always # Maybe lets only do this manually, not sure if this is needed for every commit. when: manual dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/.packit.yaml000066400000000000000000000002551520163156700232370ustar00rootroot00000000000000jobs: - job: tests use_internal_tf: true trigger: pull_request targets: #- fedora-rawhide - centos-stream-10-x86_64 skip_build: true manual_trigger: true dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/.vscode/000077500000000000000000000000001520163156700223615ustar00rootroot00000000000000dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/.vscode/settings.json000066400000000000000000000040671520163156700251230ustar00rootroot00000000000000{ "[python]": { "editor.formatOnSave": false, "editor.codeActionsOnSave": { /* The 'true' was changed to 'explicit' by VS Code from 1.85 version.*/ "source.fixAll": "explicit" }, "editor.defaultFormatter": "ms-python.black-formatter", }, "editor.minimap.enabled": true, "editor.minimap.showSlider": "always", "editor.minimap.size": "proportional", "editor.stickyScroll.enabled": true, "editor.rulers": [ 88, 120 ], "workbench.colorCustomizations": { "editorRuler.foreground": "#000000" }, "autoDocstring.docstringFormat": "sphinx", "autoDocstring.startOnNewLine": true, /* Disable specific pylance errors that are covered by linters. */ "python.analysis.diagnosticSeverityOverrides": { "reportUnusedImport": "warning", "reportMissingImports": "none", "reportGeneralTypeIssues": "information", "reportWildcardImportFromLibrary": "none", "python.analysis.typeCheckingMode": "strict", }, "cSpell.words": [ "abrt", "accessibles", "Adwaita", "Atspi", "autologin", "autoretry", "busctl", "ccpp", "colour", "coredump", "coredumpctl", "coredumps", "dasbus", "dbus", "dconf", "debuginfo", "debuginfos", "dogtail", "ENOENT", "ENOEXEC", "flathub", "flatpak", "flatpaks", "Flickr", "freedesktop", "gdbus", "gedit", "geteuid", "getframe", "getpgid", "getpid", "gsetting", "gsettings", "imread", "imwrite", "journalctl", "keyrings", "labelee", "levelname", "libexec", "lockdown", "logind", "Nextcloud", "nmci", "opencv", "pgrep", "pids", "ppid", "psutil", "qecore", "rawinput", "registryd", "rhbz", "rpms", "rtype", "scandir", "Screencast", "screencasting", "screencasts", "signum", "termcolor", "tname", "uinput", "vmcore", "webkitgtk", "XAUTHORITY", "xdmcp", "Xephyr", "xorg", "xrandr", "xsessions", "Xwaylandauth", "zenity" ], }dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/CHANGES000066400000000000000000003243041520163156700220210ustar00rootroot000000000000002026-05-15 Michal Odehnal dogtail 2.0.3 ============= Reimplementing ActionNotSupported, kind of, it has its uses. Fixing AccessibleAction to not suppress valid issues. Raising ActionNotSupported. 2026-03-30 Michal Odehnal dogtail 2.0.2 ============= Handling older config values and letting users know they have no effect. Fixing Issue #50 Handling forward compatibility. If user uses "button" let it match "push button" as well. Remove workaround to change user wanted query for push button to button. Use tree.button method to match both, instead of having workaround in predicate. 2026-02-25 Michal Odehnal dogtail 2.0.1 ============= Reducing Log Redundancy with a Class Decorator. Based on Issue #29 Switching order of names in __init__ to better reflect main points of contact. Keeping original developers listed. 2026-01-17 Michal Odehnal dogtail 2.0.0 ============= Tracked in Issue #29 Remove obsolete and legacy code that I have not observed in usage for many years while working with dogtail and automation in general. Improve Wayland support, iron out some wrinkles in very specific corner cases. Improving code structure and moving specific parts to its own files to improve readability in high level Node. Remove all pyatspi2 usage, switching to only Atspi usage. Adding accessibility classes, which were heavily inspired by Orca. Logging was rewritten to be more useful. The ponytail handling was rewritten and added to its own file, so it is not implemented in several files. Printing Accessibility Structure Representation with dump and tree was improved in readability and efficiency. Configuration handling was improved and ability to have config file per project was added. Changing casing to snake_case, but still have backwards compatibility to camelCase. Custom keys and values - users can define their own config values in .ini file. Type checking: users will get ValueError when trying to set e.g. bool to value defined as float in config.ini. Unittests were rewritten and their execution have been validated with testing-farm. These will now execute before every MR. Examples were updated. Removing Sniff, I will not release dogtail with Sniff in a broken state. It is not working well under Wayland and effort to rewrite it would be great. If someone is willing to rewrite it and improve it I will accept MR with it. Until then there are better tools that fullfil the same role. Preparation for Newton was added to Accessible classes, currently switched off. Preparation for documentation is also present as docstring in class methods, but not executed upon yet. For now one of my colleagues said he will take a look, if not, I will do it myself later. README file was updated with up to date information. Released version 2.0.dev2 to pypi which is ready to be used by wider audience. Metadata updated where possible. The dogtail.spec and Makefile remain mostly untouched. I have never used those, adapting those should come from someone more knowledgeable. 2024-12-31 Michal Odehnal dogtail 2.x =========== See the NEWS file for all changes. 2024-12-31 Vita Humpa dogtail 1.0 =========== Releasing version with Wayland support to PYPI 2018-11-09 Vita Humpa dogtail 1.0 ============== After more than five years of continuous experimentation and development, we are excited to finally release Dogtail 1.0—a Wayland-enabled version of Dogtail! How did we achieve this? It was made possible by the `gnome-ponytail-daemon`, originally crafted by Olivier Fourdan: https://gitlab.gnome.org/ofourdan/gnome-ponytail-daemon. This tool allows us to perform actions in a Wayland GNOME session in a similar way to how we have been doing so with X functions. See NEWS or README.md for more details! 2018-11-09 Vita Humpa dogtail 0.9.11 ============== A number of fixes related mainly to python3 and bilingual nature of dogtail. Scripts (sniff and scripts/) get /usr/bin/python3 shebangs, so distros can consider splitting those to subpackage. 2017-05-01 Vita Humpa dogtail 0.9.10 ============== There has been a good deal of fixes and improvements since last year's release. We're not really ready for 1.0 yet, which is why we release this important cumulative release as 0.9.10. We have also moved our homesite to gitlab (fedoraproject has been discontinued). For the list of changes, simply go to: https://gitlab.com/dogtail/dogtail/commits/master 2016-01-18 Vita Humpa dogtail 0.9.9 ============= A 1.0 pre-release. Contains a big number of fixes and changes done since 0.9.0. Most important of these is framework's compatibility with Python3. We've modified the code everywhere to run with both Python 2.7 and 3.3+. This means that from now on we maintain the same codebase and are releasing the same tarball, which can be used to package both py2 and py3 installations downstream. A complete list of fixes and changes will be present here, when we release 1.0 after 0.9.9 has been thouroughly tested. Though considered a pre-release, don'worry! 0.9.9 is considered stable and have gone through some rigorious testing already. Thanks to newly extended unit-test-set, it is actually the most self-tested release so far. 2014-04-16 Vita Humpa dogtail 0.9.0 ============= Over the last year we've made a good number of fixes, added some new features and got overall stability to the point where we can make a new 'major minor' release. With 0.9 we're finally getting only a step away from what we'd like to get done with 1.0. * Added a fix for situations when a previously crashed application still registered with at-spi blocks search ('Broken' session problem) Fixed for root.application(), root.applications() and for whenever GeneralPredicate is used (.child()). Also done for Sniff. RHBZ 972257 * Added a support for gnome-continuous build-api (#36) * Added a direct lambda support in the findChildren method, that allows using non-predicate lambdas to create search properties. These can be all pure dogtail Node properties (i.e. showing), not limiting to just pyatspi ones as is when using Predicate. * Added a GnomeShell 'helper' class into utils module allowing to work with the new top-panel application menu. * With GTK3.10+ Gdk changes, needed to tune the keyNameToKeyCode function so that it provides valid keySym in as many cases possible - with both pre-GTK3.10 as well as GTK3.11+ having the same behavior in typeText and pressKey * Sniff ui update: the info pane now stays down on resizing * Got rid of some deprecated GObject calls * Several updates to the dogtail-run-headless-next - Any kind of session can now be used in any combination with both gdm/kdm --session can be used to specify any xsession desktop file, 'kde' defaults to 'kde-plasma' too keep pre-updated compatibility; default is now the previous user session --dm - new param to pick either gdm (default) or kdm --session-binary - new param to specify any in-session binary to grab script env from - to be used with non gnome/kde session (lxde, ubuntu) - do not confuse with session execs like gnome-session and starkde, that may actually exit after session loadup; it should be everpresent (kwin, shell...) - Added a --restart switch to force restart the previously running (often left-over) session before proceeding - More logging of script execution (start/stop/PID), and some cleanups * Trap GErrors in findAllDescendants and retry if it occurs: This fixes crashes in findChildren if elements are being added/removed quickly * Unicode handling updates: - Make sure safeDecode is not skipping any strings - Don't crash when logging message contains unicode * Error Handling for Attribute Error * Fixed utils.run issue when having whitespace containing binary by using shlex shell-like parsing split. * Removed an always true if condition in i18n * Log child description when clicking. The log will now print the element description when it will be clicked * Trap TypeError in findAllDescendants: Sometimes pytaspi returns None as a Node if children are being removed too fast. We should retry search in this case * Extended the default TypingDelay from 0.075 to 0.1 seconds (helps working with certain high unicode charactes) * Fixed an bug in sniff discovered by jmolet that made sniff crash when run from /bin/sniff not /usr/bin/sniff * Generate a better script method call for labels * Fix for failing 'focus.application.node' seting * Derive all classes from object explicitely * Changed icons used in sniff that got moved to the gnome-icon-theme-legacy recently for the ones from non-legacy package. * Unified the 'isSth' vs. 'sth' properties for only 'sth' - added 'selected', but also introduced 'isChecked' property for 0.7.x compatibility * Added instructions on how to enable accessibility into utils.bailBecauseA11yIsDisabled() * Added a 'retry' option to the tree.root.application() * Fixed the concurrent creation of sniff_refresh lock from both tree and procedural * Switched to use environ value to get user in config. os.getlogin breaks when run scheduled from a testing system with no terminal. * Patched the mouse related rawinput methods to prevent using negative coordinates * Updated and added more unittests 2013-07-09 Vita Humpa dogtail 0.8.2 ============= Second update to the 0.8 series containing several fixes and improvements * Added the dogtail-run-headless-next to replace the dogtail-run-headless in future. -next, uses a diplay manager (kdm or gdm) for session management, should be used instead of older headless on systemd systems already * Unittests vastly improved and updated * Fixed a missing reset of FocusWindow to None on application refocus * Fixed the dogtail-logout to use gnome-shell instead of old gnome-panel 2012-09-07 Vita Humpa dogtail 0.8.1 ============= A first update to the new GNOME3 / KDE4 compatible release containing several fixes: * Sniff's autorefresh made togglable, to help avoid collisions with potential running dogtail scripts. Sniff checks at startup whether some script is running and sets up the autorefresh off if it is. * Added a locking mechanism into utils that can be used to solve situations when multiple dogtail processes/thread are running (applied in tree/procedural and sniff already) * Removed the deprecated dependency on CORBA * Icons no longer use absolute pathing in sniff (thx roignac) * Deprecated .has_key() operator replaced with 'in'. * Removed .svg inside sniff's .desktop file 2012-05-30 Vita Humpa dogtail 0.8.0 ============= Finally a big release updating dogtail after more than 2 years ! Dogtail is developed and maintained now again. Simply put, ported to be compatible with the new GNOME 3 and to work well in major GNOME 3 (GTK3) distributions. We'd like to make the 0.8.x version onward the GNOME 3 compatible branch usable for Fedora, RHEL7 and others, while keeping 0.7.x releases for fixes in older GNOME2 systems. --- What was done --- Notably, Sniff's UI needed to ported completely to GTK3, yet there are also several places in dogtail 'itself' that needed to be rewritten to go ahead with the new technologies. Those were all the modules where the old pygtk was used for various reasons (rawinput, utils, tree...). Incompatibilites were also present due to the new version of pyatspi (notably with the doAction method). Release highlights: * A great number of fixes everywhere related to GTK/GNOME/At-Spi updates * The dogtail-recorder was dropped for now, but might return in RC or 0.8.1 * Thanks to the qt-at-spi project, dogtail now works out-of-the-box for QT! * Headless supports KDE sessions * Headless working with full 3D-Accelerated GNOME session (no fallback anymore) * Highlight used in sniff completely re-written and made toggleable (of by default) * Makefile updated, 'make run' builds packages in homedir/rpmbuld now * Several examples fixed for updated gedit * Added tree.isChild() convenience method * Renamed doAction to doActionNamed due to conflict with doAction inside pyatspi namespace * Delay made configurable in absoluteMotion and relativeMotion functions * Changed at-spi activation to use DConf instead of GConf * A bit of code clean-up and modernization (using @property now etc.) 2009-12-07 Zack Cerza * sniff/sniff: Add a little more error checking to avoid some cases where sniff, if left running for long periods of time, would crash. 2009-10-27 Zack Cerza * tests/Node.py: Add tests for getting labeler/labelee. 2009-10-27 Zack Cerza * dogtail/tree.py: Refactor all Node properties, to clean up namespace pollution. 2009-10-27 Zack Cerza * dogtail/tree.py: Rewrite the Node.value property in a way that's compatible with python 2.4 while still avoiding namespace pollution. Add docstrings for the AccessibleValue-related properties. 2009-10-23 Zack Cerza * dogtail/tree.py: Rewrite Node.findChildren() using pyatspi.utils.findAllDescendants(). It's around 10% faster now and allows you to use any kind of predicate you wish. * tests/Node.py: Make one of the Node.findChildren() tests use non-recursive mode. 2009-10-23 Zack Cerza * tests/Node.py: Add another unit test for Node.findChildren(). Add docs for the rest. 2009-10-23 Zack Cerza * tests/Node.py: Add several unit tests for AccessibleValue-related properties, since they were all broken a long time ago without anyone noticing. * dogtail/tree.py: Fix all the AccessibleValue-related properties. 2009-10-22 Zack Cerza * tests/Node.py: Add another unit test for Node.findChildren(). 2009-10-22 Zack Cerza * tests/Node.py: Add unit test for Node.findChildren(), since I'm going to rewrite it using the more-optimized pyatspi.utils.findAllDescendants(). 2009-10-20 Zack Cerza * tests/procedural.py: Add more tests, make miscellaneous tweaks. * dogtail/procedural.py: Fix a bug found by unit tests. 2009-10-19 Zack Cerza * tests/procedural.py: Add some unit tests for the procedural interface. 2009-10-19 Zack Cerza * tests/Node.py: Add tests for Node.caretOffset and Node.combovalue; minor tweaks to other tests. 2009-10-08 Zack Cerza * setup.py, dogtail/__init__.py: Post-release version bump. 2009-10-08 Zack Cerza * setup.py, dogtail/__init__.py: Bump version to 0.7.0. Remove references to .svn directory. Reflect new site. 2009-10-08 Zack Cerza * NEWS, README: Add notes about API docs. 2009-10-08 Zack Cerza * NEWS: Update. 2009-10-08 Zack Cerza * tests/Node.py: Remove tests for Node.version. This actually comes straight from pyatspi, and is just plain not useful. It is also inconsistent. For example, on RHEL5 it gives you the gail version, while on Fedora 12 it gives you the gtk2 version. 2009-10-06 Zack Cerza * dogtail/__init__.py: Don't import pyatspi so early. 2009-10-06 Zack Cerza * dogtail/sessions.py: Make sure that binaries (at least those with absolute or relative paths specified) exist and are executable long before we try to execute them. 2009-10-06 Zack Cerza * recorder/dogtail-recorder: Catch LookupErrors (e.g. quitting an app) 2009-09-30 Zack Cerza * recorder/dogtail-recorder: Disable debug logging... again. 2009-09-29 Zack Cerza * sniff/sniff: Remove an unused set object. Trying to stay compatible with older pythons... 2009-09-29 Zack Cerza * dogtail/tree.py: Add Node.contains(), which can be used to determine if a given point onscreen lies inside a Node's extents. Also add Node.getAccessibleAtPoint(), which tries to find the Node at a given point on screen. * recorder/dogtail-recorder: Use the new Node methods to reimplement most of the click detection; the old method was too expensive and fragile. Also turn on pyatspi's caching. 2009-09-24 Zack Cerza * dogtail/procedural.py: Add FocusWindow class, and corresponding focus.window() API. I think eventually I want focus.window() calls to raise the window to the foreground. 2009-09-23 Zack Cerza * dogtail/tree.py: Fix a bug in Node.typetext(). Actually, change its behavior significantly: if we can't grab keyboard focus, fall back to inserting text via the editabletext interface at the current caret location. old behavior was to replace contents entirely, which doesn't scale at all. 2009-09-11 Zack Cerza * dogtail/procedural.py: Use specific Predicate subclasses where available. 2009-09-11 Zack Cerza * recorder/dogtail-recorder: Keyboard recording had been disabled by accident. Reenable it. 2009-09-04 Zack Cerza * dogtail/tree.py: Simplify Node.click(); add Node.doubleClick(). * tests/Node.py: Make GtkDemoTest.runDemo() use Node.doubleClick() instead of the activate action, since gtk-demo doesn't appear to use that action anymore. This will be backward-compatible anyway. 2009-09-04 Zack Cerza * tests/Node.py: Look for at-spi instead of gail in testGetVersion since Fedora no longer has a package called gail. 2009-09-04 Zack Cerza * INSTALL: Update. 2009-09-04 Zack Cerza * README: Update. 2009-09-04 Zack Cerza * dogtail/distro.py, dogtail/tree.py: Fix some formatting that epydoc doesn't like. 2009-09-04 Zack Cerza * NEWS: Update. 2009-08-26 Zack Cerza * scripts/dogtail-run-headless: Tweak usage output. 2009-08-26 Zack Cerza * scripts/dogtail-run-headless: Require a script argument to be run! 2009-08-03 Zack Cerza * recorder/dogtail-recorder: Refactor eventlistener (de)registration code. Also move some logEvent calls. 2009-07-24 Zack Cerza * examples/test-events.py: Update to pyatspi. Disable some events as they just flood the terminal. 2009-05-08 Zack Cerza * scripts/dogtail-logout: Work with RHEL5. 2009-05-05 Zack Cerza * dogtail/sessions.py: Use os.kill() instead of Popen.terminate(), since the latter doesn't exist before python 2.6... 2009-05-05 Zack Cerza * dogtail/sessions.py: Oops, don't close() the xinitrc object! 2009-05-05 Zack Cerza * dogtail/sessions.py: Let scratch files be deleted upon closing. Keep Session.xinitrcFileOBj around until its start() method is next called, so it can be read in the meantime. 2009-05-05 Zack Cerza * scripts/dogtail-logout: Don't use raw clicks. Also add a little more documentation. 2009-05-05 Zack Cerza * dogtail/sessions.py: Fix path to dogtail-logout 2009-05-05 Zack Cerza * dogtail/sessions.py: Remove/comment some debug output. 2009-05-05 Zack Cerza * dogtail/sessions.py: Make scriptCmdList an optional argument to Session.init() 2009-05-01 Zack Cerza * scripts/dogtail-run-headless: Fix option application. 2009-05-01 Zack Cerza * dogtail/sessions.py: Tweak the way we find the correct environment for the script. 2009-05-01 Zack Cerza * dogtail/sessions.py, scripts/dogtail-run-headless: Refactor most of dogtail.sessions. 2009-04-30 Zack Cerza * dogtail/sessions.py: Merge Session and GNOMESession; refactor Session; add Script. 2009-04-29 Zack Cerza * scripts/dogtail-logout: Add a note about the script still "working" if gnome-session doesn't end up being accessible. 2009-04-29 Zack Cerza * scripts/dogtail-run-headless: Give a choice of X servers. 2009-04-29 Zack Cerza * scripts/dogtail-logout: Add a sleep(1), something's wrong... 2009-04-28 Zack Cerza * scripts/dogtail-run-headless: Actually instantiate the session object. 2009-04-28 Zack Cerza * dogtail/sessions.py: Properly report exit code. 2009-04-28 Zack Cerza * scripts/dogtail-run-headless: Draft of a rewrite using dogtail.sessions 2009-04-28 Zack Cerza * scripts/dogtail-logout: Add a new script, dogtail-logout. This is for use with headless sessions. Currently doesn't work yet. 2009-04-28 Zack Cerza * dogtail/sessions.py: Add a sessions module. This will become the basis for a rewritten dogtail-run-headless. 2009-03-21 Zack Cerza * epydoc.conf: Add the Epydoc config file. 2009-03-21 Zack Cerza * Makefile: Add proper targets for generating/uploading API docs. We're using Epydoc now. 2009-03-21 Zack Cerza * dogtail/utils.py: If we're running under pydoc or epydoc, don't bail if accessibility is disabled. 2008-12-31 Zack Cerza * examples/appstartup.py: Add a new example that launches a given application, then checks to see that it started correctly by looking for a Node with the given roleName. 2008-08-18 Zack Cerza * examples/gedit-test-utf8-procedural-api.py: Actually, handle the file chooser's expander widget more gracefully by looking to see if it has the 'checked' state before expanding it - apparently 'checked' means it is expanded. Go figure. 2008-08-18 Zack Cerza * dogtail/procedural.py: Make Click.__call__() work without arguments again. 2008-08-11 Zack Cerza * tests/Node.py: Don't import pyatspi unnecessarily. 2008-08-11 Zack Cerza * dogtail/rawinput.py: Use config.actionDelay for click(). 2008-08-11 Zack Cerza * examples/gedit-test-utf8-procedural-api.py: Collapse the file chooser's expander widget when we're done using it. 2008-08-11 Zack Cerza * dogtail/tree.py: Don't log giant messages to the debug log if we're setting a Node's text to a very large value. Also, use config.actionDelay for Node.click(). 2008-08-11 Zack Cerza * dogtail/tc.py: Put a timestamp on TCImage's diff.png. 2008-08-08 Zack Cerza * dogtail/tc.py: Eliminate double slashes in TCImage.diff. Also remove do-nothing TCImage.__init__(). 2008-08-08 Zack Cerza * dogtail/tc.py: Rewrite TCImage to use PIL instead of ImageMagick. Update unit tests to reflect this rewrite. 2008-08-08 Zack Cerza * dogtail/tc.py: Make TC a new-style class. No particular reason. 2008-08-08 Zack Cerza * dogtail/logging.py: Make ResultsLogger log to a file no matter what. 2008-07-24 Zack Cerza * recorder/dogtail-recorder: Update to reflect API change in rawinput. 2008-02-19 Zack Cerza * dogtail/config.py: Add new parameter: checkForA11y, which defaults to True. If set to False, we assume accessibility is enabled. * dogtail/utils.py: Don't import gconf until necessary. * dogtail/tree.py, sniff/sniff: Don't check for accessibility unless config.config.checkForA11y is True. 2008-02-19 Zack Cerza * examples/gedit-test-utf8-procedural-api.py: Use click() instead of activate(). 2008-02-19 Zack Cerza * dogtail/tc.py: Add TCBool and TCNode. 2008-02-19 Zack Cerza * dogtail/tree.py, dogtail/dump.py: Allow tree.Node.dump() to output to a file instead of stdout. 2008-02-19 Zack Cerza * dogtail/utils.py: Use subprocess.Popen() instead of os.spawnvpe(). * dogtail/procedural.py: Make run() return the PID. 2008-02-19 Zack Cerza * dogtail/i18n.py: Add loadAllTranslationsForLanguage(). 2008-02-19 Zack Cerza * dogtail/distro.py: Make distribution detection code a little less ugly. Remove docstrings from the Distro subclasses. Clean up some other documentation. * dogtail/i18n.py: Don't import the distro module until we actually need it. * dogtail/logging.py: Add an optional 'newline' arg to Logger.log(), which defaults to True. Passing False will cause the logger to not print or write a newline at the end of the message. Also stop catching some IOErrors, as that made the code awkward for no good reason. 2008-02-06 Zack Cerza * dogtail/dump.py: Properly print Node.actions; ditch "xml" output as it was not even really a good start. * dogtail/tree.py: Strip some useless information of out Action.__str__(). 2008-02-06 Zack Cerza * dogtail/__init__.py: Fix inconsistent version number. 2008-02-06 Zack Cerza * dogtail/distro.py: Fix minor inconsistency. 2008-01-31 Zack Cerza * setup.py, dogtail/__init__.py: Bump version to 0.6.90. 2008-01-31 Zack Cerza * Makefile, MANIFEST.in: Remove mentions of debian/ and dogtail.spec. 2008-01-31 Zack Cerza * dogtail.spec: Remove; see Fedora CVS for the canonical spec file. 2008-01-31 Zack Cerza * debian/: Remove as it's so out-of-date it's probably dangerous, and Debian doesn't use it anyway. 2008-01-31 Zack Cerza * dogtail/rawinput.py: Refactor keyname <-> keysym conversion code. Remove unnecessary code, including the ctypes/xlib-based keyStringToKeyCode in favor of the gtk.gdk version. This completely removes the ctypes dependency. 2008-01-21 Zack Cerza * scripts/dogtail-run-headless: Use a real GNOME session instead of a "fake" one. 2008-01-21 Zack Cerza * dogtail/wrapped.py: Add dogtail.wrapped module, with superclasses for Node and Application wrappers to use in custom application wrappers. We need this because dogtail.tree.{Node,Application} are no longer wrapper classes, and thus can't be subclasses directly. 2008-01-15 Zack Cerza * dogtail/tree.py: Change Node.valueMin and valueMax to Node.minValue and maxValue; add minValueIncrement. 2008-01-15 Zack Cerza * sniff/sniff: Remove SniffModel.checkNodes() and SniffModel.addDeadNode() as we're not using them now that we have events working. While there are applications that don't have proper event support, the performance hit on constantly crawling over nodes is too large. 2008-01-15 Zack Cerza * sniff/sniff: Make rows in the main TreeView expand/collapse on pressing Enter. 2008-01-15 Zack Cerza * sniff/sniff, sniff.glade: Add a new tab to the bottom pane that lists the present states (just the ones supported by dogtail). 2008-01-14 Zack Cerza * sniff/sniff, sniff/sniff.glade: Make the relation buttons only visible when a target exists. Also prepare SniffController.showRelationTarget() more prepared for when we add more relations to dogtail and sniff. 2008-01-14 Zack Cerza * dogtail/tree.py: Fix a stupid error in the Node.dead check. 2008-01-10 Zack Cerza * sniff/glade, sniff/sniff: Split bottom pane into tabs: first is name, roleName, description, actions; second is (editable)text; third is relations. Currently there are only two relations: labeler and labelee. Relations in sniff are currently represented as buttons, which, when clicked, call the relation targets' blink() method. Also fix a minor bug in SniffController.setUpTable() where sometimes we would try to disconnect a signal handler multiple times. 2008-01-10 Zack Cerza * NEWS: Add a note about a broken unit test due to #498563. 2008-01-10 Zack Cerza * dogtail/tree.py: Add Node.dead property; useful if you're unsure if the accessible you're about to poke at is still valid, and don't feel like trying to catch 3 or more different exceptions. 2008-01-10 Zack Cerza * sniff/sniff: Call SniffController.setUpTable() inside selectionChanged() instead of buttonPress(). 2008-01-10 Zack Cerza * sniff/sniff: Instead of processing events synchronously, put them in a queue to be processed during an idle function. Rewrite SniffModel.getPath(). 2007-12-18 Zack Cerza * dogtail/logging.py: Remove IconLogger. Make Logger not create logfiles until its log() method is called. This means sniff won't be creating empty logs all the time. * dogtail/config.py: Clean up some cruft and remove IconLogger stuff. * dogtail/trayicon.py: Removed. 2007-12-18 Zack Cerza * sniff/sniff: Remove the 'finally' from a try-except-finally, since that would be the first time we broke compatibility with python 2.4, and it's far from worth it :) 2007-12-17 Zack Cerza * sniff/sniff: Overhaul sniff to use pyatspi events to be notified when a node is renamed or gains/loses children. As a result, SniffModel does not use its addDeadNode() and checkNodes() methods. Those methods have not yet been removed, however. This change will result in properly-written applications working far better than before in sniff. Improperly-written apps (e.g. Firefox, but they know this) will behave better than sniff in 0.6.1; checkNodes() was causing serious slowdowns in some cases since it's fairly heavy code that needs to be looped continuously. Since sniff now has to do a lot less work than it did ever before, it should be somewhat faster in all cases. 2007-12-11 Zack Cerza * sniff/sniff, sniff/glade: Add 'Set Root' and 'Unset Root' menu items to Sniff's View menu. Useful if you want to dig into a single app and want to reduce the amount of scrolling around necessary slightly. 2007-12-11 Zack Cerza * sniff/sniff: Improve the removal of dead nodes from SniffModel, mainly by removing the highest-level dead node in the case that an entire subtree is found to be dead. Thanks to Michal Babej for the initial patch. Closes: #503056. 2007-12-10 Zack Cerza * dogtail/tree.py: Make Action.__node public (i.e. rename to Action.node). 2007-12-10 Zack Cerza * dogtail/tree.py: Force Node._getChildren() to return an empty list if the node's parent is a hyper link. 2007-12-10 Zack Cerza * dogtail/tree.py: Er, fix typo in last commit. 2007-12-10 Zack Cerza * dogtail/tree.py: Add AccessibleValue support. Thanks to Pelya for submitting a patch before the port was merged. 2007-12-10 Zack Cerza * dogtail/tree.py: Fix typo. 2007-12-10 Zack Cerza * sniff/sniff: Show lines interconnecting TreeView rows. 2007-12-10 Zack Cerza * sniff/sniff: Expand the root node of the TreeView on startup. 2007-12-10 Zack Cerza * sniff/sniff: Move SniffModel.getNodeAttr() to SniffController, modify it to be specific to the 'name' property of a node. That function is now called nameDataFunc(). The behavior of nameDataFunc() has also been changed: instead of changing the text of the TreeView cell representing a defunct Node to '(broken node)', it now flags the row for removal by adding a reference to it to SniffModel.deadNodes. Every 1000ms SniffModel will prune such rows from itself. In my tests, this caused no extra measurable increase in CPU utilization. Also, remove all columns in SniffModel other than the node column, as the rest were not being used anyway. 2007-12-10 Zack Cerza * sniff/sniff: Refactor sniff. Organize into 3 classes: SniffApp with methods for setting up the application; SniffModel as a subclass of gtk.TreeStore; SniffController for poking at the SniffModel and gtk.TreeView instances. 2007-12-07 Zack Cerza * dogtail/rawinput.py: Fall back to using a gtk.gdk.Keymap implementation of keyStringToKeyCode() if ctypes is not available. This will probably become the default implementation. 2007-12-07 Zack Cerza * dogtail/tree.py: Add a patch from Michal Babej to fix a python 2.4 compatibility issue. Closes: #502324. 2007-12-06 Zack Cerza * recorder/dogtail-recorder: Only write the script preamble once. 2007-12-06 Zack Cerza * sniff/sniff: Make the Close button in sniff's About dialog actually close the dialog. 2007-11-30 Zack Cerza * Makefile: Minor update. 2007-11-30 Zack Cerza * NEWS: Add a note about a broken unit test due to #498557. 2007-11-30 Zack Cerza * dogtail/tree.py: Port Accessible.Hypertext and Hyperlink support. tree.Link is now called LinkAnchor and is no longer a subclass of Node. The new behavior is now that when a Hypertext interface is found on a Node, that node will have one extra child Node per Hyperlink/anchor pair. The child Nodes will have a role name of 'hyper link' and the 'URL' property. Also, remove much dead code. 2007-10-31 Zack Cerza * dogtail/procedural.py: Add a patch from Cole Robinson to both make all focus.*() queries return a Boolean based on their success and to make the failure warning more verbose. * dogtail/config.py, dogtail/logging.py: Add a patch from Cole Robinson to allow the logging of debug information to stdout to be disabled via config.logDebugToStdOut. Still enabled by default. 2007-10-22 Zack Cerza * examples/gedit-test-utf8-procedural-api.py, examples/gedit-test-utf8-tree-api.py: Tweak slightly to work with the new dogtail behavior of click() using raw mouse events. 2007-10-22 Zack Cerza * dogtail/procedural.py: Remove all calls to time.sleep(). 2007-10-22 Zack Cerza * dogtail/tree.py: Add some missing doDelay()'s. Also remove a chunk of dead code. 2007-10-22 Zack Cerza * dogtail/errors.py, dogtail/tc.py: Fix encoding headers. 2007-10-22 Zack Cerza * MAINTAINERS: Updated. 2007-08-29 Zack Cerza * dogtail/tree.py: Update and clean up the toplevel docstring. 2007-08-29 Zack Cerza * dogtail/tree.py: Rewrite Node.__doc__. 2007-08-29 Zack Cerza * dogtail/tree.py: Remove some redundant documentation from Node.__doc__. 2007-08-23 Zack Cerza * dogtail/tree.py: Port the Action class over. Mainly needed to get at keybindings. 2007-08-23 Zack Cerza * dogtail/__init__.py: Make __init__.py import pyatspi. 2007-08-23 Zack Cerza * tests/Node.py: Update unit tests with API changes: Node States (from the old stateSet) are accessed directly, e.g. node.sensitive; read-only attributes now raise AttributeError instead of ReadOnlyError; atspi.SpiException has been replaced by CORBA.COMM_FAILURE. * dogtail/tree.py: Replace ReadOnlyError with AttributeError. Also make Node.doAction() raise NotSensitiveError if necessary. 2007-08-23 Zack Cerza * dogtail/tree.py: Fix Application.window() to not try to initialize the Window class. 2007-08-10 Zack Cerza * dogtail/tree.py: Port setting of Node.combovalue and additionally allow retrieval of the value. 2007-08-10 Zack Cerza * dogtail/tree.py: Port Node.caretOffset. 2007-08-10 Zack Cerza * dogtail/tree.py: Add most of the rest of the checks from the old Node.children into the new one, i.e. children limits and null-child filtering. Also, when filtering null children, warn if config.debugSearching is True. 2007-07-27 Zack Cerza * sniff/sniff: Handle broken nodes more gracefully. 2007-07-27 Zack Cerza * recorder/dogtail-recorder: Minor change in how we keep track of keyboard press/release events. 2007-07-26 Zack Cerza * recorder/dogtail-recorder: Port dogtail-recorder to pyatspi. Remove some dead code. Add lots of comments. The keyboard modifier processing had to be rewritten. As a bonus, things like should actually get noticed now. Also, the madness that was FakeNode is no more. 2007-07-25 Zack Cerza * dogtail/i18n.py: Fix duplicate checking in loadTranslationsFromPackageMoFiles(). Closes: #452232. 2007-06-22 Zack Cerza * dogtail/dump.py: Remove SIGINT handler. 2007-06-22 Zack Cerza * dogtail/tree.py: Port Selection stuff to pyatspi and convert to use normal instance methods and properties. 2007-06-20 Zack Cerza * examples/no-help-at-all.py: Add patch from Jan Hutař to use cgi.escape() where necessary. 2007-06-20 Zack Cerza * examples/filechooser-stress-test.py: Add patch from Jan Hutař to add i18n support. 2007-05-31 Zack Cerza * dogtail/tree.py: Fix a minor bug in Node._fastFindChild() that caused a non-recursive search to halt completely if a null child was encountered. 2007-05-30 Zack Cerza * dogtail/tree.py: Move some documentation into the new properties. Remove some unhelpful documentation. Further clean up bits of documentation here and there. Note: not done with docs yet :) 2007-05-29 Zack Cerza * dogtail/tree.py: Remove actual search code from Node.findChild() and use pyatspi.utils.findDescendent, via a new Node._fastFindChild() function. 2007-05-29 Zack Cerza * dogtail/predicate.py: Turn all predicates' satisfiedByNode() functions into pseudo-staticmethods, so we can pass those methods directly to the search methods in pyatspi.utils. 2007-05-25 Zack Cerza * sniff/sniff: Port Sniff to pyatspi. It should all work now, but there could be a bug or two. 2007-05-25 Zack Cerza * dogtail/tree.py: Begin port from pyspi to pyatspi. Disable __init__, __getattr__, __setattr__, __str__ while their functionality is ported. Attributes that were accessed via __[gs]attr__ are now declared as properties, to make the code easier to read and document. 2007-05-25 Zack Cerza * dogtail/logging.py: Actually obey config.logDebugToFile. * dogtail/config.py: Implement runtime changing of logDebugToFile. 2007-05-25 Zack Cerza * dogtail/config.py: Fix logic in first unit test. 2007-05-16 Zack Cerza * dogtail/rawinput.py: Port from pyspi to pyatspi. Use ctypes and Xlib to do the keystring -> keycode keycode conversion. 2007-05-11 Zack Cerza * dogtail/tree.py, recorder/dogtail-recorder, scripts/dogtail-run-headless, sniff/sniff: Replace 'except:' with 'except: Exception', since we don't want KeyboardInterrupt and SystemExit to be ignored in any of those cases. See: http://www.python.org/dev/peps/pep-0352/. Also catch only ImportError in once case inside of dogtail-recorder. 2007-05-03 Zack Cerza * sniff/sniff: Add patch from Michal Babej to correctly reset the actions label when a Node with no actions is selected. Closes: #432999. 2007-03-16 Zack Cerza * setup.py: Add patch from Fernando Herrera to fix building from a subversion checkout. Closes: #418923. 2007-02-20 Zack Cerza * dogtail/predicate.py: Add unit tests for all predicates. 2007-02-13 Zack Cerza * dogtail/tree.py: Add patch from Michal Babej to fix Node.childLabelled(). 2007-02-09 Zack Cerza * examples/no-help-at-all.py: Add patch from Michal Babej to use cgi.escape() to escape content for SGML. 2007-02-09 Zack Cerza * dogtail/tc.py: Add patch from Michal Babej that removes some confusion in TCImage.compare(), and also fixes a unit test. 2007-02-08 Daniel Drake * dogtail/tree.py: Fix wrong logic in Node.selectedChildren implementation 2007-02-08 Zack Cerza * dogtail/utils.py: Make isA11yEnabled() look at $GTK_MODULES, too. 2006-11-28 Zack Cerza * dogtail/config.py: When creating the default directories for logs, etc. (/tmp/dogtail/*), create them world-writable so that they may be shared by multiple users. 2006-11-22 Zack Cerza * dogtail/config.py: Add parameter 'childrenLimit', which is an integer representing how many children of a Node instance to return at maximum. The default is 100. * dogtail/tree.py: Only return config.childrenLimit children. Log a warning to the debug logger the first time the list is clipped, but remain silent on subsequent occurences. 2006-11-17 Zack Cerza * examples/test-events.py: Make sure dogtail.tree is imported. Closes: #374898. Also add warning that the script is outdated, may do evil, and will probably be removed soon. 2006-11-15 David Malcolm * dogtail/tree.py: ensure that a Node and a Link's children attribute always is a list and hence iteratable (previously returned None for Link instances and for Node instances that had __hideChildren set); similar fix for the Node.findChildren method 2006-10-23 Zack Cerza * dogtail/procedural.py: Make run() insert a space while concatenating the binary name and arguments to pass. Closes: #364553. 2006-10-23 Zack Cerza * setup.py, dogtail/__init__.py, NEWS: Post-release version bump. 2006-10-13 Zack Cerza * Released 0.6.1. 2006-10-13 Zack Cerza * NEWS: Update. 2006-10-13 Zack Cerza * examples/data/GNOME-Street1.png, examples/data/GNOME-Street.png, examples/data/g-star1.png, examples/data/g-star.png: Remove and replace with far smaller test images. * examples/data/10b.png, examples/data/10w.png, examples/data/20b.png, examples/data/20w.png: Add, to replace older, larger test images. * dogtail/tc.py: Update to reflect the change in test images. 2006-10-12 Zack Cerza * recorder/dogtail-recorder: Keep track of which 'frame' (window) the widget we're poking is under. If necessary, write out 'focus.frame()' lines for that frame. This should keep us from getting confused about which non-dialog window of a given application we care about. 2006-10-12 Zack Cerza * recorder/dogtail-recorder: Be consistent about what name we use for the recorder internally. 2006-10-11 Zack Cerza * NEWS: Update with changes since 0.6.0. 2006-10-11 Zack Cerza * dogtail/rawinput.py: Make typeText('foo\nbar') work again. 2006-10-10 Zack Cerza * dogtail/distro.py: Fix up Solaris detection so that an /etc/release that does not contain 'Solaris' doesn't prevent the exception from being raised. 2006-10-10 Zack Cerza * recorder/dogtail-recorder: Re-add Dave Malcolm's patch to add syntax hilighting to the script view, which was accidentally dropped. Closes: #345374. 2006-10-10 Zack Cerza * dogtail/distro.py: Add patch from Brian Cameron to detect Solaris. Closes: #360479. Also, check for Fedora before checking for Red Hat. 2006-10-10 Zack Cerza * dogtail/i18n.py: Make TranslatableString.matchedBy() check for equality before trying to match via regular expression. This will ensure that traditional, exact-string searches will work regardless of the presence of any regexy-looking characters. Additionaly escape '(' and ')', since grouping will never need to be used in this context. Closes: #348891. 2006-10-02 Zack Cerza * dogtail/utils.py: Fix 'Technolofy' typo :) 2006-09-27 Zack Cerza * recorder/dogtail-recorder: Add support for recording right clicks. It mostly works, but I'm seeing some inconsistency regarding what gets focus when a right-click event occurs. For example, when a nautilus icon seems to retain focus only about half the times it is right-clicked; the other half, the context menu receives the focus. Obviously, when that happens, the recorder records the wrong event. 2006-09-26 Zack Cerza * recorder/dogtail-recorder, sniff/sniff: Stop whining if the glade file isn't in the current directory. 2006-09-26 Zack Cerza * recorder/dogtail-recorder, recorder/recorder.glade: Implement script playback using python's threading module. 2006-09-26 Zack Cerza * dogtail/procedural.py: Make type() and keyCombo() fall back to their corresponding function in rawinput if focus.widget.node == None. 2006-09-26 Zack Cerza * recorder/dogtail-recorder, recorder/recorder.glade: Remove Quit button and add Play button. The Play button isn't fully implemented yet. 2006-09-26 Zack Cerza * recorder/dogtail-recorder, recorder/recorder.glade: Remove combo box since OOScriptWriter needs fixing. Also remove the expander since a GTK bug was keeping it from expanding properly. 2006-09-22 Zack Cerza * dogtail/rawinput.py: Make pressKey(), which is used by typeText(), use UniCharToKeySym(). Now typeText('foo, bar; baz!?!') works again. 2006-09-21 Zack Cerza * examples/filechooser-stress-test.py: Apply a patch from Michal Babej to correctly look for the u'Open files\u2026' dialog. * examples/no-help-at-all.py: Apply a patch from Michal Babej fail gracefully if no argument was supplied, and also to start the requested app if it is not running. 2006-09-21 Zack Cerza * dogtail/logging.py: Refactor logging mechanism. DebugLogger is now just Logger, and LogWriter is now a Logger subclass called ResultsLogger. Logger can (and will by default) write its output to a file in config.logDir. To disable the logging of debug information to a file, set config.logDebugToFile to False. ResultsLogger now also prints its results to standard out. * dogtail/distro.py, dogtail/tree.py, dogtail/utils.py: Replace all print statements with calls to the debug logger. * dogtail/tc.py: Update to reflect changes in dogtail.logging. * dogtail/procedural.py: Don't raise a FocusError if config.fatalErrors is False. Instead, spit a warning out via dogtail.errors.warn(), which relays to the debug logger. * dogtail/errors.py: Add warn(), which is the mechanism by which an exception is replaced by a warning if config.fatalErrors is False. Uses dogtail.logging.debugLogger, which is a dogtail.logging.Logger instance. * dogtail/config.py: Add two new parameters: 'fatalErrors', which specifies whether errors encountered while using dogtail.procedural should be considered fatal; and 'logDebugToFile', which specifies whether to write debug output to a file. They default to False and True, respectively. * dogtail/predicate.py: Give debugNames to all the Predicates that were missing them. * dogtail/i18n.py: Don't spam warnings and probably-useless tracebacks when a broken app spews fake UTF-8. * dogtail/__init__.py: Post-release version bump. Add the errors module to __all__. * NEWS, setup.py: Post-release version bump. 2006-09-13 Zack Cerza Released 0.6.0. 2006-09-13 Zack Cerza * README: Update. * setup.py, dogtail/__init__.py: Bump version numbers. 2006-09-13 Zack Cerza * MANIFEST.in: Remove debian directory from tarball. * setup.py: Remove references to dogtail.apps.wrappers. 2006-09-13 David Malcolm * examples/rhythmbox-test.py: removed * examples/nautilus-test-icon-view-collage.py: removed (moved to dogtail-tests/nautilus) * examples/google-search.py: removed (moved to dogtail-tests/epiphany) * examples/gnome-panel-test-starting-every-app.py: removed (moved to dogtail-tests/gnome-panel) * examples/gcalctool-test-fibonacci.py: removed (moved to dogtail-tests/gcalctool) * examples/firefox-test-browsing-local-html-file.py: removed * examples/evolution-test-switching-components.py: * examples/data/CAN-2005-0806.mbox: * examples/evolution-test-survives-email-CAN-2005-0806.py: * examples/evolution-test-sending-email.py: * examples/evolution-test-first-time-wizard.py: * examples/evolution-test-configuring-imap-smtp.py: * examples/evolution-test-configuring-exchange.py: * examples/evolution-test-composing-html.py: removed (moved to dogtail-tests/evolution) * examples/crack.py: removed (never worked) * examples/abiword-test.py: removed (moved to dogtail-tests/abiword) 2006-09-13 David Malcolm * dogtail/apps: remove entire subdirectory structure: what remained is now in GNOME CVS inside dogtail-tests/appwrappers 2006-09-13 David Malcolm * dogtail/apps/wrappers/kicker.py: remove * dogtail/apps/wrappers/konqueror.py: remove * dogtail/apps/wrappers/mozilla.py: remove * dogtail/apps/wrappers/epiphany.py, * dogtail/apps/wrappers/evolution.py, * dogtail/apps/wrappers/gnomepanel.py, * dogtail/apps/wrappers/gnomepanel.py, * dogtail/apps/wrappers/yelp.py, * dogtail/apps/wrappers/nautilus.py: remove reference to categories * dogtail/apps/categories.py: remove * dogtail/apps/__init__.py: remove categories 2006-09-13 Zack Cerza * NEWS: Update. 2006-09-11 Zack Cerza * recorder/dogtail-recorder: Replace newlines in node.name values with an escaped '\n' in ProceduralScriptWriter.recordClick(). Remove a debug statement in EventRecorder.onKeyPress(). 2006-09-11 David Malcolm * dogtail/apps/wrappers/yelp.py, dogtail/apps/wrappers/mozilla.py, * dogtail/apps/wrappers/gnomepanel.py: rename self.version in each app wrapper to avoid a naming collision with the synthesized Tree.version attribute. 2006-09-11 Zack Cerza * sniff/sniff, recorder/dogtail-recorder: Add patch from Tim Lee so sniff and dogtail-recorder can find glade files even if they're not installed into the /usr prefix. Closes: #353719, #353731. 2006-09-11 Zack Cerza * dogtail/utils.py, sniff/sniff, recorder/dogtail-recorder, dogtail/tree.py: Add utility functions to check whether accessibility is enabled via GConf, and either present a dialog asking to enable it (for sniff & dogtail-recorder) or just bail (for scripts). 2006-09-07 Zack Cerza * recorder/dogtail-recorder: Make sure the recorder can't record itself. Also remove some old comments. 2006-09-07 Zack Cerza * dogtail/i18n.py: Also use safeDecode() instead of calling any string's encode() method directly. Now i18n.py should work exclusively with Unicode internally, and the safe conversion means that dogtail should never crash because of an invalid Unicode string ever again. 2006-09-07 Zack Cerza * dogtail/i18n.py: Add safeDecode(), a function based on the fix committed on 2006-08-22. Use it instead of calling any string's decode() method directly. Closes: #354515. 2006-09-07 Zack Cerza * scripts/dogtail-run-headless: Fix syntax error, and look for gnome-settings-daemon in /usr/libexec before $PATH. Closes: #354518. 2006-09-07 Zack Cerza * dogtail/utils.py: Rewrite screenshot() to use GDK instead of ImageMagick. 2006-09-06 Zack Cerza * dogtail/distro.py: Add patch from Tim Lee to fix up SuSe support. Closes: #353601. 2006-09-05 Zack Cerza * dogtail/logging.py: Don't use an IconLogger by default. * dogtail/config.py: Add useIconLogger setting, which defaults to False. Setting it to True enables the IconLogger immediately. Add documentation and a unit test to that effect. 2006-08-28 Zack Cerza * dogtail/tree.py: Ignore any SpiException, only when trying to access root.children. This is so people using broken applications while trying to test non-broken applications don't have to suffer. 2006-08-25 Zack Cerza * dogtail/tree.py: Add patch from Chris Lee to add the 'checked' Node attribute, which indicates whether the Node is a check box that is currently checked. 2006-08-23 Zack Cerza * dogtail/config.py: Add parameter 'typingDelay', defaulting to 0.075. * dogtail/rawinput.py: Make typeText() use pressKey(). Add keySymAliases for '\t' and '\n'. 2006-08-23 Zack Cerza * setup.py, MANIFEST.in: Remove mentions of textinput.glade. 2006-08-22 Zack Cerza * recorder/dogtail-recorder: Remove the text input UI. Implement actual keystroke recording. Add support for the ProceduralScriptWriter to write out script calls. The OOScriptWriter is unsupported at this time. Also, make exceptions occuring inside AT-SPI-registered callbacks more informative by printing their tracebacks before they're lost in the marshaling process. * recorder/recorder.glade: Remove 'Input Text' button. * recorder/textinput.glade: Remove glade file for text input UI. 2006-08-22 Zack Cerza * dogtail/procedural.py: Add type(), which for now just calls focus.widget.node.typeText(). Add keyCombo(), which for now just calls focus.widget.node.keyCombo(). 2006-08-22 Zack Cerza * dogtail/tree.py: Add new Node attributes: 'focusable' and 'focused'. Both are read-only Boolean values. 'focusable' indicates whether the Node is able of taking keyboard focus. 'focused' indicates whether the Node currently has keyboard focus. Remove Node.rawType() and move its functionality to Node.typeText(). In Node.typeText(), if the node is focusable and not focused, try to grab its focus with Node.grabFocus before using rawtype.typeText() to do the typing. If the node is not focusable, just append the string to the node's 'text' property. Add Node.keyCombo(), which does a similar thing but cannot have a fallback mechanism. 2006-08-22 Zack Cerza * dogtail/rawinput.py: Add keyCombo(), which takes a string in the same format of gtk.accelerator_parse(), e.g. "Delete" and generates the appropriate keyboard events to simulate the user pressing that combination. Add __buildKeyStringsDict(), which essentially generates an inverted version of the keySyms dict. Add keySymToUniChar(), which takes a keysym and returns the corresponding Unicode character, if one exists. Add uniCharToKeySym(), which does the reverse. Also add a few more keysym aliases to the keySymAliases dict. 2006-08-22 Zack Cerza * dogtail/i18n.py: If we encounter a UnicodeDecodeError because of an invalid string, print the exception plus a warning explanation, and contintue by decoding the string and replacing invalid characters with question marks. 2006-08-21 Zack Cerza * recorder/dogtail-recorder.desktop: Move to the 'Programming' category in the menu. * sniff/sniff.desktop: Move to the 'Programming' category in the menu. 2006-08-11 Zack Cerza * scripts/dogtail-run-headless: Use the python GConf bindings. 2006-08-11 Zack Cerza * NEWS, setup.py, dogtail/__init__.py, dogtail/apps/__init__.py, dogtail/apps/wrappers/__init__.py: Post-release version bump. 2006-08-01 Zack Cerza Released 0.5.2. 2006-08-01 Zack Cerza * NEWS: Update. 2006-08-01 Zack Cerza * tests/Node.py: Put a time.sleep() in GtkDemoTest.tearDown() to let gtk-demo actually die before launching another. AT-SPI seems to really disapprove of the speed at which it was being hammered. 2006-08-01 Zack Cerza * NEWS: Update. 2006-07-31 Zack Cerza * dogtail/config.py, dogtail/tree.py: Remove shebang lines. 2006-07-28 David Malcolm * Makefile: add a working default target ('all') and an install target, so that it builds within jhbuild (Fixes: #318535) 2006-07-27 David Malcolm * tests/Node.py: added TestExceptions: ensure that when an app dies, attempts to contact it result in an exception being raised (even more final part of bg #318135) 2006-07-27 David Malcolm * sniff/sniff: handle broken communication with nodes (e.g. for a crashed app) by displaying "broken node" and an error icon (hopefully the final part of bug #318135) 2006-07-24 Zack Cerza * dogtail/rawinput.py: Use gtk.kesyms to look up keysyms. Retain compatibility with old names via a keySymAliases dict. Also, use only one atspi.EventGenerator. 2006-07-21 Zack Cerza * dogtail/utils.py: Set GTK_MODULES='gail:atk-bridge' in run() before executing the app, to make sure it is accessible. It seems most non-GNOME apps need this set. 2006-07-21 Zack Cerza * recorder/dogtail-recorder: Add a UI for setting the text property of a Node instance. While recording, you click 'Input Text', and a dialog pops up. Click the node whose text you want to set, then type the text into the dialog and press OK. The recorder sets the node's text, then writes out lines in the script to do the same. * recorder/recorder.glade: Add 'Input Text' button. * recorder/textinput.glade: Add new glade file: the text input dialog. * MANIFEST.in: Add textinput.glade to tarball. * setup.py: Add textinput.glade to packages. 2006-07-19 Zack Cerza * recorder/dogtail-recorder: Initialize EventRecorder.listeners to an empty list, to fix a traceback when the user tries to quit the recorder without actually recording anything. 2006-07-14 Zack Cerza * recorder/dogtail-recorder: Make the script TextView editable at all times, so the user can add comments and the like while recording. 2006-07-14 Zack Cerza * examples/gedit-test-utf8-tree-api.py: Add patch from Len DiMaggio to fix this script. Also make the script search for u'Save As\u2026' in addition to 'Save As...' for the dialog title. Closes: #340734. 2006-07-14 Zack Cerza * dogtail.spec: Forward-port a fix from the Fedora Extras specfile, fixing a broken call to desktop-file-install. Closes: #347322. 2006-07-13 Zack Cerza * scripts/dogtail-run-headless: Rewrite the script in Python. It now accepts command line options to specify what session is to be used. Currently those are: '--gnome' or '-g' for the old minimal GNOME session, and '--none' or '-n' for just metacity. The script also has a usage statement and helpful error messages. In general it's more featureful, robust and easy to use. Closes: #344902. * scripts/dogtail-detect-session: Also exit normally if any apps are found at all. 2006-06-30 David Malcolm * tests/Node.py: add unit test for getting bogus attributes of a Node; document many of the existing unit tests; add unit tests for writing to a read-only attribute of Node; fix some typos in the names of the unit tests; check that Node.extents has non-zero size, and that Node.size is non-zero * dogtail/tree.py: make attempts to write to a read-only attribute of Node raise a ReadOnlyError, rather than fall through to an AttributeError; remove the old stubbed unit test from the tail of the file 2006-06-29 Zack Cerza * dogtail/procedural.py: Add warning not to read without understanding the API. 2006-06-27 David Malcolm * recorder/dogtail-recorder: use GtkSourceView if available to get syntax-highlighting. Closes: #345374. * tests/Node.py: new file: unit tests for the dogtail.Node class. * tests: new subdirectory, to hold unit tests for Dogtail itself 2006-06-27 Zack Cerza * dogtail/tree.py: Make SearchError a proper Exception subclass. * dogtail/procedural.py: Prepare module for support for accepting regular expressions as 'name' arguments. Use the IsAnApplicationNamed predicate in FocusApplication. Remove unused 'description' arguments. * dogtail/i18n.py: Add regular expression support to TranslatableString.matchedBy(). This means that any 'name' argument supports regular expression matching, by virtue of the fact that all Predicate subclasses uses TranslatableString.matchedBy() for 'name' arguments. 2006-06-16 Zack Cerza * dogtail/tree.py: Add patch from Dave Malcolm to work around the problem where pyspi would essentially just stop working, until the user logged out and back in. Closes: #321273. 2006-06-15 Zack Cerza * dogtail/config.py: Fix help(dogtail.config.config). Fix outdated documentation for dogtail.config.config by reorganizing and documenting accidentally-undocumented parameters. 2006-06-02 Zack Cerza * dogtail/config.py: Fix unit test that was broken by a commit from 2005-11-01, which made config.ensureSensitivity default to True. The unit test wasn't updated in that commit. The unit test now doesn't assume what the default is, and uses the value obtained from config.defaults. Thanks to Len DiMaggio for reporting. Closes: #342028. 2006-05-01 Zack Cerza * recorder/recorder.glade: Remove the 'Stop' button, and add a 'Clear' button. * recorder/dogtail-recorder: Make the 'Record' button turn into a 'Stop' button after it is pressed, and vice versa. Add toggleRecording() to RecorderGUI and remove startRecording(). Make RecorderGUI.quit() stop the recording before it calls gtk.main_quit(). 2006-04-17 Zack Cerza * recorder/dogtail-recorder: Make ProceduralScriptWriter.recordClick() write out correct calls for Node instances that don't have action['click'] and instead need, for example, 'activate'. If other actions aren't available, use the node's select(). If that doesn't work, do a click(raw=True), to spoof raw mouse input. Also, remove some old AttributeError handlers from FakeNode.__init__(). Also, make EventRecorder stop listening to window:create events, since those are just thrown away. Also, refactor and rewrite EventRecorder.onMouseButton() to be much more accurate in detecting clicks. Reporting the wrong nodes happens far less often, now. There is still the problem of clicks getting dropped when the mouse button isn't help down for, say, 200ms. Clicks on toolbar buttons tend to get dropped too. But there are no more false clicks, e.g. clicking on a non-accessible application still recorded a (bogus) action. Also, make clicking on table cells work properly, working around the fact that mouse:button events are always reported by the table and not the cell, by finding the smallest child of the table whose extents the event occured inside. * dogtail/procedural.py: Add Select, an Action subclass that doesn't deal with actions. It lets us have select() and deselect() calls. The former is how page tabs are activated. 2006-04-17 Zack Cerza * sniff/sniff: Restore sane default window size. 2006-04-17 Zack Cerza * dogtail/tree.py: Fix Node.size by using the values from Node.__accessible.getExtents() instead of .getSize(). 2006-04-13 Zack Cerza * dogtail/tree.py: Change the way Node reports that attributes provided by specialized Accessible interface extensions do not apply. For example, for some instance 'foo', trying to access 'foo.text' when the AccessibleText interface is not available for that node will now return None instead of raising an AttributeError (if it were available, and the text was zero-length, it would have returned ''). Trying to access 'foo.children' will return [] instead of raising an AttributeError. The reason for this change is that these conditions are not "exceptional". Trying to access 'foo.thisIsANonexistentAttribute' would be exceptional. Also, add a ReadOnlyError exception which is raised when, for example, someone tries to set 'foo.text' when 'foo' does not have an AccessibleEditableText implementation. * sniff/sniff: Update Sniff to reflect the removal of most of Node's AttributeErrors. * sniff/sniff.glade: Use a GtkWindow instead of a GnomeApp. The latter was unnecessary and forced us to use widgets we didn't want. 2006-04-11 Zack Cerza * recorder/dogtail-recorder: Scroll to the end of the TextView after inserting a new line, so you can watch the script as it's written. 2006-04-11 Zack Cerza * recorder/dogtail-recorder, recorder/recorder.glade: Add ComboBox to allow selection of ScriptWriter. Make ProceduralScriptWriter default. Remove those assertions, because they were evil. 2006-04-11 Zack Cerza * recorder/dogtail-recorder: Implement ProceduralScriptWriter class. It implements recordClick() just like OOScriptWriter, but it also has the special method findAncestor(), for deciding whether the script will need to do a focus.application() or focus.dialog(). For this, I needed to implement a FakeNode.__cmp__(). It only compares the name, roleName and description of the FakeNode instances. Bother me if you need it to compare more attributes for it to work. Make ProceduralScriptWriter the default ScriptWriter for now. Also, add a couple assertions in various methods to make sure the node parameter is never None. 2006-04-04 Zack Cerza * setup.py, dogtail/__init__.py, dogtail/config.py, dogtail/distro.py, dogtail/dump.py, dogtail/errors.py, dogtail/i18n.py, dogtail/logging.py, dogtail/path.py, dogtail/predicate.py, dogtail/procedural.py, dogtail/rawinput.py, dogtail/tc.py, dogtail/trayicon.py, dogtail/tree.py, dogtail/utils.py, dogtail/version.py, dogtail/apps/categories.py, dogtail/apps/wrappers/__init__.py, dogtail/apps/wrappers/epiphany.py, dogtail/apps/wrappers/evolution.py, dogtail/apps/wrappers/gcalctool.py, dogtail/apps/wrappers/gedit.py, dogtail/apps/wrappers/gnomepanel.py, dogtail/apps/wrappers/kicker.py, dogtail/apps/wrappers/konqueror.py, dogtail/apps/wrappers/mozilla.py, dogtail/apps/wrappers/nautilus.py, dogtail/apps/wrappers/yelp.py, examples/abiword-test.py, examples/crack.py, examples/evolution-test-switching-components.py, examples/filechooser-stress-test.py, examples/gcalctool-test-fibonacci.py, examples/gedit-test-utf8-procedural-api.py, examples/gedit-test-utf8-tree-api.py, examples/gnome-panel-test-starting-every-app.py, examples/google-search.py, examples/i18n-test.py, examples/nautilus-test-icon-view-collage.py, examples/no-help-at-all.py, examples/rhythmbox-test.py, examples/test-events.py, recorder/dogtail-recorder, sniff/sniff: Convert everything to use 4 spaces for indentation, instead of tabs. * HACKING: Remove all the notes about forcing editors to use tabs for indentation. Closes: #318833. 2006-04-04 Zack Cerza * sniff/sniff.desktop: Move to the 'Accessories' category in the menu. 2006-04-04 Zack Cerza * recorder/dogtail-recorder.desktop: Add desktop file for dogtail-recorder. * MANIFEST.in, setup.py: Ship and install dogtail-recorder.desktop. 2006-04-04 Zack Cerza * recorder/recorder.glade: Add gtk.Expander and gtk.TextView to allow viewing and editing a script. Note that when the Expander is collapsed, the main window doesn't shrink back to its original size. This is a bug. * recorder/dogtail-recorder: Make ScriptWriters either store the script in a normal Python string, or a gtk.TextView if it's available. Continue to print the script to standard out, for debugging. Implement saving scripts via the GUI, automatically appending a '.py' to the filename if necessary. Also, add some basic docstrings to some OOScriptWriter methods (mostly to note that the keyboard stuff doesn't work yet). 2006-03-31 Zack Cerza * dogtail/tree.py: Added support for AccessibleSelection to Node, via Node.{de,}select(), Node.{de,}selectAll(), Node.isSelected, Node.selectedChildren and Node.indexInParent.. Thanks to Peter Johanson and Jose Dapena Paz. Closes: #336562. 2006-03-28 Zack Cerza * MANIFEST.in: Add the new recorder files to the tarball. * setup.py: Add the new recorder files to packages. 2006-03-28 Zack Cerza * recorder/dogtail-recorder: Add GUI with Record, Stop, Save and Quit buttons. Add new methods to EventRecorder: startRecording() and stopRecording(), and move all the event listener (de)registration and and AT-SPI event loop handling into those methods. Remove hack that killed the recorder when an Edit menu was clicked, since there is a working Stop button. This requires today's pyspi HEAD. * recorder/recorder.glade: Add Glade file for dogtail-recorder. 2006-03-28 Zack Cerza * examples/recorder.py: Move to recorder/dogtail-recorder. * recorder/dogtail-recorder: Move from examples/recorder.py. 2006-03-28 Zack Cerza * examples/recorder.py: Remove the Writer class and move its only method to the ScriptWriter class. Create a a subclass of dogtail.tree.node, called FakeNode, which can wrap a Node instance and store certain attributes of it, so AT-SPI calls are not used when reading them. This was necessary because events are useless if they are received after their source has been destroyed. For example, by storing recorder.lastFocusedNode as a FakeNode instead of a Node, if a Close button in a dialog is clicked we will still have all the information we need to record the event. So, make EventRecorder.on{Focus,Select}() store self.last{Focus,Select}edNode as FakeNodes. Also, make EventRecorder.onMouseButton() properly dance around dogtail.tree's handling of attributes. This will have to be fixed in the nearish future, though. 2006-03-21 Zack Cerza * examples/recorder.py: s/Focussed/Focused/g. Add lastSelectedNode callbacks and event listener registration. s/MouseClick/MouseButton/g. Keep track of lastPressedNode and lastReleasedNode. Add code to try to detect the "real" source nodes by checking the X and Y coordinates of lastFocusedNode and lastSelectedNode. Add a way to exit the recorder - currently, clicking on an Edit menu will do it. When the OOScriptWriter is initialized, make it spit out "from dogtail.tree import *". 2006-03-21 Zack Cerza * dogtail/predicate.py: Remove extra quotes from all the script generation code. 2006-03-21 Zack Cerza * dogtail/__init__.py, dogtail/apps/__init__.py, dogtail/apps/wrappers/__init__.py, setup.py, NEWS: Post-release version bump. 2006-03-14 Zack Cerza Released 0.5.1. 2006-03-14 Zack Cerza * examples/recorder.py: Store each EventListener in a list instead of initializing it and throwing it away. Add the beginnings of a OOScriptWriter.recordKey() method - for when pyspi's DeviceListener works properly. * NEWS: Updated. 2006-03-14 Zack Cerza * dogtail/i18n.py, dogtail/rawinput.py, dogtail/i18n.py, dogtail/rawinput.py, dogtail/logging.py, dogtail/utils.py, dogtail/path.py: Remove '#!/usr/bin/env python' headers to fix rpmlint warnings. The rest apparently can keep them, since they have unit tests. Add UTF-8 encoding headers. 2006-02-28 Zack Cerza * dogtail/predicate.py: Fix makeCamel() to deal with TranslatableString instances properly by calling str() on the parameter as a safeguard. 2006-02-24 Zack Cerza * setup.py, dogtail/__init__.py, dogtail/apps/__init__.py, dogtail/apps/wrappers/__init__.py: Belated post-release version bump. 2006-02-24 Zack Cerza * dogtail/distro.py, dogtail/i18n.py, dogtail/logging.py, dogtail/tc.py, dogtail/trayicon.py, dogtail/tree.py, dogtail/apps/wrappers/evolution.py: s/ / /g. This will make it easier to convert to four-space indentation, since that's all the rage these days. * dogtail/apps/wrappers/nautilus.py: Fix up indentation; was mixing tabs and spaces. 2006-02-24 Zack Cerza * dogtail/procedural.py: Tweak the FocusWidget and FocusDialog algorithms so that they store the predicate instance. That way, we can give more meaningful FocusErrors by passing the predicate's debugName. 2006-02-14 Zack Cerza * Makefile: Split 'rpm' target into 'rpm_prep' and 'rpm'. Add a new target, 'srpm', to build just a source RPM. Make it ignore BuildRequires. Make 'rpm' target only build binary RPMs. Don't make the 'tarball' target depend on the 'clean' target. 2006-02-07 Zack Cerza Released 0.5.0. 2006-02-07 Zack Cerza * README: Add pointed to gedit-procedural example, and note that it's the most frequently updated. 2006-02-03 Zack Cerza * sniff/sniff: Add some error checking to getSelectedNode(), to get rid of a (harmless, but scary) traceback on startup. 2006-02-03 Zack Cerza * dogtail/logging.py: Update to use new icon path. 2006-02-03 Zack Cerza * setup.py, sniff/sniff: Move sniff's application icons from /usr/share/pixmaps/ into /usr/share/hicolor/*/apps/. * setup.cfg: Remove. 2006-02-03 Zack Cerza * README: Add README file. It's a start. 2006-01-31 Zack Cerza * dogtail/i18n.py, dogtail/predicate.py dogtail/trayicon.py: Be much more intelligent about encodings. This should really, truly fix the last remaining blocker on the automatic runtime translation feature. Closes: 328652. 2006-01-31 Zack Cerza * dogtail/i18n.py: Generalize language detection. 2006-01-31 Zack Cerza * dogtail/apps/wrappers/epiphany.py: Update to new Action API. 2006-01-27 Zack Cerza * dogtail/i18n.py: Comment out some super-verbose debug statements. * examples/gedit-test-utf8-procedural-api.py: Use a proper Unicode ellipsis. 2006-01-27 Zack Cerza * dogtail/i18n.py: Make sure we filter the list of mo-files so that we only get the ones in the target language. 2006-01-26 Zack Cerza * dogtail/i18n.py: Fix automatic runtime translation. Stop using gettext.dgettext(), and instead use the gettext.GNUTranslations class and its member function, ugettext(). This ensures that we always get Unicode back. As a consequence, stop caring about "domains" and just deal with mo-files. Remove getTranslationDomainsForPackage(). Closes: #328652. Also, fix isMoFile()'s regexp matching. 2006-01-24 Zack Cerza * dogtail/tree.py: Comment out the warning about missing wnck bindings, since the functionality is nonworking anyway. * NEWS: Add note about the last change to dogtail.tree. 2006-01-17 Zack Cerza * examples/gedit-test-utf8-procedural-api.py: Use codecs.open() instead of open() for reading the UTF-8 files. open() apparently ignores encoding issues, and codecs.open() allows us to specify the encoding of the file. Add comments to that effect. * dogtail/tc.py: Make a note in TC.compare()'s docstring that codecs.open() must be used to open any non-ASCII files that are intended to be compared using that function. 2006-01-17 Zack Cerza * dogtail/utils.py: Make screenshot() raise a DependencyNotFoundError if ImageMagick is not installed. * NEWS: Add note about the last change to screenshot(). 2006-01-17 Zack Cerza * NEWS: Add notes about dogtail.errors and the change to TCImage. 2006-01-17 Zack Cerza * dogtail/errors.py: Add errors.py, a module for generalized exceptions, i.e. its first exception: DependencyNotFoundError. Other exceptions such as distro.PackageNotFoundError and procedural.FocusError probably do not belong here. * dogtail/tc.py: Give a better error message if TCImage is instantiated while ImageMagick is not installed, by raising a DependencyNotFoundError. * examples/gedit-test-utf8-procedural-api.py: Remove unncessary TCImage instantiation. Also, be aware that the naming of the "Save as" dialog changed slightly. Try both strings. 2006-01-13 Zack Cerza * setup.py: Change author_email to dogtail-list@gnome.org. 2006-01-13 Zack Cerza * setup.py, dogtail/__init__.py, dogtail/apps/__init__.py, dogtail/apps/wrappers/__init__.py: Bump version numbers. 2006-01-13 Zack Cerza * MANIFEST.in: Add NEWS file to tarball. 2006-01-05 Zack Cerza * NEWS: Update with changes since 0.4.3. 2006-01-04 Zack Cerza * examples/gedit-test-utf8-procedural-api.py: Add comment about how to turn on automatic translation. 2005-12-01 Zack Cerza * sniff/sniff: Update to the new Action API. 2005-12-01 Zack Cerza * dogtail/tree.py: Add patch from David Malcolm to add a single shortcut method for Node.doAction('click') - simply Node.click() as a compromise. Closes: #321196. 2005-11-27 Zack Cerza * dogtail/utils.py: Remove spurious print statement from screenshot(). 2005-11-27 Zack Cerza * NEWS: Reformat to make more readable. 2005-11-27 Zack Cerza * dogtail/tree.py: Re-add helper methods that were removed on 11-08. Change Action API from "Node.actionName()" to "Node.doAction(actionName)" for an action whose name is 'actionName'. * dogtail/procedural.py: Update to new Action API. 2005-11-22 Zack Cerza * debian/control: Fix typo. Thanks to Alex Roitman for reminding me to commit this. Closes: #322169. 2005-11-22 Zack Cerza * dogtail/tc.py: Make TCImage.compare() work around a change in ImageMagick that broke the method, by comparing its version string against the one at which it changed. Closes: #321431. 2005-11-18 Zack Cerza * dogtail/config.py: Add config parameter 'blinkOnActions'. The parameter defaults to False. Set it to True to watch your scripts blinken die lichten! * dogtail/tree.py: Make Action.do() call blink() on the associated node. 2005-11-18 Zack Cerza * dogtail/utils.py: Add Blinker class from sniff/sniff, which, upon initialization, blinks a rectangular shape onscreen. * dogtail/tree.py: Add Node.blink(), which blinks a rectangle around the node's extents (if it has extents). This is intended to help debugging situations where, during script writing, the node found in a search is not what was expeced. * sniff/sniff: Remove Blinker class and blinkExtents(). Make selectionChanged() just call node.blink(). 2005-11-18 Zack Cerza * sniff/sniff: Move blink interval constant into Blinker class. Make Blinker.__init__()'s count argument optional, defaulting to 2. 2005-11-17 Zack Cerza * dogtail/tree.py: Add Hyperlink and Hypertext support via the Link class (subclass of Node). Link objects have a roleName of 'hyper link' and a 'jump' action in Epiphany - I haven't tested Firefox yet. 2005-11-17 Zack Cerza * dogtail/tree.py: Add debug logging to Node.rawClick() and Node.rawType(). 2005-11-17 David Malcolm * sniff/sniff: reduce it to a single blink (2 calls of the callback) rather than two blinks (4 calls) to avoid visual confusion when rapidly changing the selection. 2005-11-17 David Malcolm * sniff/sniff: implement a selectionChanged callback for the tree view; use it to make the extents of the selected node blink, porting code from at-poke's src/poke.c (though without the grabs that uses). Closes: #321655 2005-11-17 David Malcolm * sniff/sniff: remove the unused "populate" function 2005-11-16 Zack Cerza * sniff/sniff: Properly catch an AttributeError where an AssertionError was previously expected. Closes: #321564:. 2005-11-16 Zack Cerza * dogtail/procedural.py: Make FocusDialog.__call__() pass recursive=False to FocusApplication.node.findChild(). Closes: #321624. 2005-11-15 Zack Cerza * dogtail/logging.py: Make LogWriter print the name of the logfile just before it creates it. 2005-11-15 Zack Cerza * dogtail/logging.py: Fix TimeStamp.zeroPad() by defaulting to width=2. Fix TimeStamp.fileStamp() by renaming 'long' argument to 'addTime'. 2005-11-14 Zack Cerza * dogtail/procedural.py: Use {get,set}attr() inside FocusBase.__{get,set}attr__() instead of FocusBase.node.__{get,set}attr__(), in order to composite attributes that aren't dynamically generated. 2005-11-14 Zack Cerza * dogtail/procedural.py: Fix compositing of Node properties into FocusBase subclasses. 2005-11-14 Zack Cerza * dogtail/procedural.py: Fix focusing. There are supposed to be three different focus 'slots': application, dialog, and widget. Previously Actions were not operating on the same nodes as FocusWidget as intended. Also remove a couple print statements left over from previous debugging. 2005-11-14 Zack Cerza * dogtail/rawinput.py: rewrite pressKey() to take as an argument the English key name as seen on the keyboard, look its keysym up via a dict, and pass that value to AT-SPI to synthesize the event. This means each key will have to be added to the keySyms dict explicitly, but I think it's worth it for the API to make sense and be portable. Add docstrings to pressKey() and typeText(). 2005-11-10 Zack Cerza * dogtail/logging.py: Fix TimeStamper.zeroPad()'s padding, and note in its docstring that it will not truncate. 2005-11-10 Zack Cerza * dogtail/predicate.py, dogtail/rawinput.py: Clean up imports. 2005-11-10 Zack Cerza * dogtail/rawinput.py: Add typeText() and pressKey(). For now pressKey() takes an integer keycode argument, but I'd like to enable something like "pressKey('Return')" in the future. * dogtail/tree.py: Make Node.grabFocus() get added to the object in Node.__init__(), but only of the node wraps a Component. Also add Node.rawClick() and Node.rawType(). * dogtail/procedural.py: Implement click(raw=True). 2005-11-10 Zack Cerza * dogtail/procedural.py: Make FocusWidget.__call__() actually raise a FocusError if it can't find what it's looking for. 2005-11-10 Zack Cerza * dogtail/procedural.py: Add menu(), Focus.frame(), Focus.window(). 2005-11-10 Zack Cerza * dogtail/tree.py: Add Node.position, Node.size, and Node.grabFocus() for those Nodes wrapping an AccessibleComponent. 2005-11-10 Zack Cerza * dogtail/tc.py: Don't initialize TC's self.{baseline,undertest,result} so that TC.compare() defaults to failing. Make sure to assign self.{baseline,undertest} to the value of the arguments passed to compare(). Fix several unit tests to specify their strings as being Unicode-encoded. Strings comparisons now fail properly. Thanks to Muktha Narayan for reporting the bug and attaching a proposed patch. Closes: #321151. 2005-11-10 Zack Cerza * dogtail/tc.py: s@scripts/pound@examples/data@g to reflect the directory naming that occured a while back. 2005-11-08 Zack Cerza * dogtail/tree.py: Fix bogus logic in Node.__getattr__() where if, for example, a Node had multiple actions such that node.actions[0].name == 'activate' and node.actions[1].name == 'menu', calling node.menu() would fail. 2005-11-08 Zack Cerza * dogtail/tree.py: Remove "various wrapper/helper search methods": Node.{child, menu, menuItem, textentry, button, childLabelled, childNamed, tab}(). Node.menu() conflicted with the other Node.menu(), which is needed to execute the AccessibleAction's 'menu' action. 2005-11-08 Zack Cerza * dogtail/procedural.py: Fix "maximum recursion depth exceeded" error in FocusBase.__getattr__() when 'help(dogtail.procedural)' is called right after 'import dogtail.procedural'. 2005-11-08 Zack Cerza * Makefile: Build RPMs as a normal user. Thanks again to Bastien Nocera for the patch! Closes: #320983. 2005-11-08 Zack Cerza * dogtail.spec: Make __os_install_post macro one line to fix building RPMs on RHEL4 and earlier. Thanks to Bastien Nocera for the patch. Closes: #320978. 2005-11-03 Zack Cerza * dogtail/distro.py: Add PackageDb.getMoFiles(), returning (mostly) all the .mo files in the distro, optionally for just one locale. Move all PackageDb subclasses into the top level of the module, and prefix their names with a '_' so it is possible to subclass them. Add UbuntuAptPackageDb to make it possible for getMoFiles() to search in /usr/share/locale and /usr/share/locale-langpack. Add JHBuild class, a subclass of Distro, and JhBuildPackageDB class, a subclass of PackageDb. JhBuildPackageDB.getFiles() and JhBuildPackageDB.getVersion() are not implemented yet, as I haven't figured out just how they should work yet. 2005-11-03 Zack Cerza * dogtail/distro.py: Make AptPackageDb only call apt_pkg.GetCache() once and save the cache object, to save time in the case where multiple calls to getVersion() or getDependencies() need to be made. 2005-11-03 Zack Cerza * dogtail/distro.py: Move *PackageDb class declarations and instantiations into their Distro classes' __init__() methods. Remove all __make*PackageDb() functions. Make dogtail.distro.packageDb a reference to dogtail.distro.distro.packageDb, where the second 'distro' is the Distro subclass instance to preserve the API. 2005-11-02 Zack Cerza * dogtail.spec: Bump version number. 2005-11-02 Zack Cerza * scripts/dogtail-run-headless: Save the value of the GConf key /desktop/gnome/interface/accessibility, then set it to true before running the test. After the test is run, restore the old value. Closes: #320548. 2005-11-02 Zack Cerza * scripts/dogtail-run-headless: Preserve the return code of the script being exectued. Closes: #320535. 2005-11-01 David Malcolm * dogtail/apps/wrappers/evolution.py: added setBogusGConfAccountData() function as a way of bypassing the first-time wizard 2005-11-01 Zack Cerza * dogtail/config.py: Make config.ensureSensitivity False by default. * dogtail/tree.py: Make Action.do() spit out a warning if the Node instance is not sensitive and config.ensureSensitivity is False. 2005-11-01 Zack Cerza * dogtail/tree.py: Implement a proper NotSensitiveError exception. Make Action.do() raise it instead of a string literal when the action's Node instance is not sensitive, and config.ensureSensitivity is True. 2005-11-01 Zack Cerza * dogtail/logging.py: Change format of TimeStamp.fileStamp() to filename_YYYYMMDD-hhmmss or just filename_YYYYMMDD, controlled by the new optional argument 'long'. Fix docstrings of TimeStamp.fileStamp() and TimeStamp.entryStamp(). * dogtail/utils.py: Rewrite most of screenshot(), and add assertions that config.scratchDir exists before the screenshot is taken, and that the screenshot file exists before the function returns. Generate screenshot filenames using logging.TimeStamp.entryStamp() by default. A call like "screenshot(file='foo') will name the file 'foo_YYYYMMDD-hhmmss.png', but the timestamping can be disabled with a call like "screenshot(file='foo', timeStamp=False)", naming the file just 'foo.png'. Additionally, the file argument may specify an extension, e.g. 'foo.png' or 'foo.jpg'. Make screenshot() return the full path to the screenshot taken. Clarify screenshot()'s docstring. 2005-10-31 Zack Cerza * dogtail/i18n.py: Add URL to bug filed at bugzilla.redhat.com to the note about popt making gettext.dgettext() bail. 2005-10-31 Zack Cerza * dogtail/distro.py: Add an Ubuntu subclass of Debian, since Ubuntu in some cases does things wildly different. For a good example, see dogtail/i18n.py. * dogtail/i18n.py: Make automatic translation work on Ubuntu with a special-case: If a script needing automatic translation is running on Ubuntu, look for translations in the language-pack-gnome-$LANGUAGE package before the actual package under test. Note that this makes automatic translation significantly slower. Also, remove the 'dogtail' prefix from references to dogtail.config and dogtail.distro. 2005-10-31 Zack Cerza * dogtail/tree.py: Only be verbose about setting a Node.text attribute if config.debugSearching is turned on. 2005-10-31 Zack Cerza * examples/gedit-test-utf8-procedural-api.py: Add automatic translation support (it's a one-liner!). Don't set $LANG from inside the script. Add a line for config.debugTranslation, but comment both debug lines out. 2005-10-28 Zack Cerza * dogtail/distro.py: Implement proper Exceptions PackageNotFoundError and DistributionNotSupportedError. 2005-10-28 Zack Cerza * dogtail/distro.py: Implement getFiles() and getDependencies() in AptPackageDb. 2005-10-28 David Malcolm * dogtail/i18n.py: New file, providing i18n facilities. * dogtail/config.py: Add debugTranslation boolean * examples/i18n-test.py: New example, demonstrating the i18n hooks * dogtail/tree.py: Add a getUserVisibleStrings() method to Node * dogtail/predicate.py: Rewrite all string matching within predicates to use the dogtail.i18n.TranslatableString class, and rewriting as necessary. * dogtail/apps/wrappers/evolution.py: Use the evolution-connector package's translations by default. * examples/evolution-test-switching-components.py: Use the evolution package's translations 2005-10-28 David Malcolm * dogtail/distro.py: Use correct exceptions for unimplemented functions. Add getFiles and getDependencies methods to PackageDb; implement them for RpmPackageDb. Fix the PATCH_MESSAGE to give the mailing list address. 2005-10-27 Zack Cerza * dogtail/config.py: Rewrite. Rename the Config class to _Config, and provide a singleton instance named config. It is no longer necessary to instantiate a _Config instance in any script, whereas previously it was necessary to do so if any values needed to be changed. Also rename logdir to logDir, scratch to scratchDir, and data to dataDir. When the *Dir values are changed, the new directory is created instantly if it does not exist. NOTE: this is an API change. Updating any scripts should be as simple as replacing 'Config' with 'config', 'logdir' with 'logDir', 'scratch' with 'scratchDir', 'data' with 'dataDir' and removing any instantiations of the old Config class. Also, the old way of loading configuration values from a file is no longer supported. The current preferred method is to create a Python file that contains something like: "from dogtail.config import config; config.logDir = '../logs/'; config.debugSearching = True" and then import that file from whatever scripts need it. * dogtail/logging.py, dogtail/tc.py, dogtail/tree.py, examples/firefox-test-browsing-local-html-file.py, examples/gcalctool-test-fibonacci.py, examples/gedit-test-utf8-procedural-api.py, examples/gnome-panel-test-starting-every-app.py, examples/google-search.py, examples/nautilus-test-icon-view-collage.py, examples/data/sample.cfg: Update to new config API. * dogtail/procedural.py: Update to new config API. Also, change "def run(application, arguments = None, appName = None):" to "def run(application, arguments = '', appName = ''):". * dogtail/utils.py: Update to new config API. Also, make run()'s appName argument default to "''" instead of "None", just like dogtail.procedural.run(). Also, make run() use doDelay() instead of sleep() to get logging for free via the config.config.debugSearching option. 2005-10-24 Zack Cerza * dogtail/utils.py: Don't break if a filename is passed to screenshot(). 2005-10-24 Zack Cerza * dogtail/__init__.py, dogtail/apps/__init__.py, dogtail/apps/wrappers/__init__.py, setup.py: Post-release version bump. 2005-10-24 Zack Cerza Released 0.4.3. 2005-10-24 Zack Cerza * NEWS: Created with major changes since 0.4.2. * dogtail/__init__.py, dogtail/apps/__init__.py, dogtail/apps/wrappers/__init__.py, setup.py: Bump version numbers. * dogtail.spec, debian/changelog: Add changelog entries for 0.4.3. 2005-10-24 Zack Cerza * icons/dogtail-head-48.png, icons/dogtail-head.svg, icons/dogtail-tail-48.png, icons/dogtail-tail.svg: Add icons. * MANIFEST.in, setup.py: Add new icons to distribution. * dogtail/logging.py: Make IconLogger use the dogtail-tail icon. * sniff/sniff: Use the dogtail-head icon for the application. Use the theme's desktop icon for the root node in the TreeView. If the then has an icon for any application displayed in the TreeView, use it. * sniff/sniff.desktop: Use the dogtail-head icon in the menu. * dogtail/trayicon.py: Fix typo. 2005-10-24 Zack Cerza * dogtail/procedural.py: Implement the FocusError exception properly. 2005-10-21 Zack Cerza * dogtail/trayicon.py: Instead of totally bailing out if Python is older than 2.4, just make TrayIcon useless, as is currently the case if TrayIcon._runZenity() fails. What should really happen is for TrayIcon to actually work under older Python versions, I think. 2005-10-20 Zack Cerza * dogtail/apps/wrappers/epiphany.py: Use the package name 'epiphany-browser' on Debian-based distributions. Thanks to Andrew Beresford for the patch. 2005-10-19 Zack Cerza * dogtail.spec, setup.py, debian/changelog, debian/copyright: Correct website URL. 2005-10-19 Zack Cerza * sniff/icons: Create directory. * sniff/*.xpm, sniff/icons/*.xpm: Move icons. * MANIFEST.in, setup.py, sniff/sniff: Update the icon locations. 2005-10-19 Zack Cerza * MANIFEST.in: Add sniff/*.xpm. 2005-10-19 Zack Cerza * dogtail/procedural.py: Add table() and tableCell() to Action and FocusWidget. Allow run() to use utils.run()'s optional 'appName' argument, for cases where the application's name is different from the binary's name. 2005-10-17 David Malcolm * dogtail/apps/wrappers/yelp.py: new file, wrapper for Yelp. 2005-10-17 David Malcolm * dogtail/apps/wrappers/predicate.py: add IsATabNamed class * dogtail/tree.py: add Node.tab() method * dogtail/apps/wrappers/evolution.py: add Evolution.createMeeting() method; add AppointmentWindow and MeetingWindow subclasses 2005-10-14 David Malcolm * dogtail/apps/wrappers/evolution.py: Refactor createAccount, add functions to run the first-time wizard (sharing code with account creation). Add a blowAwayEvolution function * examples/evolution-test-first-time-wizard.py: New example, testing the above. Blows away your evolution configuration, so use with caution... 2005-10-14 David Malcolm * dogtail/apps/wrappers/evolution.py: Fix wrapper so that it does not require a window, in preparation for wrapping the first-time assistant. 2005-10-13 Zack Cerza * dogtail/trayicon.py: Added from jhbuild's jhbuild/utils/. * dogtail/logging.py: Add IconLogger class, a stripped-down version of jhbuild's TerminalBuildScript in jhbuild/frontends/terminal.py. Modify the DebugLogger class to additionally write messages to the IconLogger's tooltip. If zenity is not installed or not working properly, the DebugLogger should work exactly as before. 2005-10-13 David Malcolm * examples/no-help-at-all.py: script which generates parodies of bad end-user documentation by simply converting reading the UI back in DocBook form. 2005-10-12 David Malcolm * dogtail/apps/wrappers/gedit.py: Initial version, taken from Paolo Borelli's home page. 2005-10-11 David Malcolm * dogtail/tree.py: Replaced a stray reference to "Search" with "Predicate" 2005-10-11 Zack Cerza * dogtail/procedural.py: rename open() to openItem() to avoid namespace collision. * scripts/dogtail-run-headless: Ignore errors when restoring ~/.xinitrc. If there was no previous ~/.xinitrc, make sure the temporary one isn't left behind. Increase hardcoded sleep delay. 2005-10-11 Zack Cerza * MANIFEST.in: Add scripts/ to tarball distribution. * setup.py: Install scripts/* . * setup.cfg: Add xorg-x11-Xvfb to requires for RPMs. * Makefile: Make 'deb' target run 'fakeroot debian/rules clean' before building a package. * debian/control: Add xvfb to Recommends for debs. 2005-10-11 Zack Cerza * scripts/dogtail-run-headless: Correct typo. 2005-10-11 Zack Cerza * scripts/dogtail-run-headless: Restore the original ~/.xinitrc after the X server terminates. 2005-10-11 Zack Cerza * dogtail/procedural.py: Add focus.desktop (just a copy of tree.root). Later, I'll probably want to replace it with a FocusDesktop instance, but for now it's really just to make FocusApplication not break as horribly when there are no applications running. * scripts/: Created to hold scripts to be installed into /usr/bin/. * scripts/dogtail-detect-session: Add a script to detect if an accessibility-enabled session is currently running. Right now it obviously only looks for a GNOME session, but KDE support can be added when available. * scripts/dogtail-run-headless: Add a script to run a given dogtail script in a headless setup. Currently launches a minimalistic GNOME "session" under Xvfb, calls dogtail-detect-session, and if a session is detected, executes the dogtail script. Once the script terminates, so does the X server - Boom! Closes: #318537 2005-10-11 Zack Cerza * dogtail/procedural.py: Add Focus.icon(). Add open() for opening icons on nautilus' desktop. 2005-10-10 David Malcolm * sniff/sniff: Move initial expansion from main to resetModel, so that we get the sane one-deep expansion on a Refresh, as well as when sniff starts. 2005-10-10 Zack Cerza * examples/filechooser-stress-test.py: Port to procedural API. 2005-10-10 David Malcolm * examples/filechooser-stress-test.py: New file: repeatedly open and close the filechooser, for testing perfomance/profiling 2005-10-09 David Malcolm * examples/gcalctool-test-fibonacci.py: New file: another test example. Drives gcalctool to calculate the Fibonacci series. * dogtail/apps/wrappers/gcalctool.py: New file: wrapper to help when writing gcalctool tests 2005-10-09 Zack Cerza * setup.py: Include sniff/*.xpm in the distribution. * sniff/sniff: modify loadIcon() to first search the current directory, then /usr/share/dogtail/icons. If distributions need to put them elsewhere, we can certainly accommodate them. 2005-10-09 Zack Cerza * dogtail/tree.py: Revert API change - rename Action.doThisAction() to Action.do(). Add documentation in Node.__getattr__(), the only place it is used, to make it clear what is happening. 2005-10-09 Zack Cerza * sniff/sniff.glade: Give the TreeView default focus. Really remove the Edit menu. 2005-10-09 Zack Cerza * sniff/sniff: Add Dave as an author. Correct the website URL and remove the License button in the About box. Remove 'Role name and 'Description' fields from the TreeView. Change the packing on the TreeView to disable expanding. Stop importing pdb without using it. * sniff/sniff.glade: Change the 'Snoop' menu item name to 'Sniff' - sniff used to be called snoop. Add keyboard shortcuts to all menu items. Remove Edit menu. 2005-10-09 David Malcolm * dogtail/tree.py: added role attribute to Node (forgot to commit this earlier) 2005-10-09 David Malcolm * sniff/button.xpm, sniff/checkbutton.xpm, sniff/checkmenuitem.xpm, sniff/colorselection.xpm, sniff/combo.xpm, sniff/dialog.xpm, sniff/image.xpm, sniff/label.xpm, sniff/menubar.xpm, sniff/menuitem.xpm, sniff/notebook.xpm, sniff/scrolledwindow.xpm, sniff/spinbutton.xpm, sniff/statusbar.xpm, sniff/table.xpm, sniff/text.xpm, sniff/toolbar.xpm, sniff/tree.xpm, sniff/treeitem.xpm, sniff/unknown.xpm, sniff/viewport.xpm, sniff/vscrollbar.xpm, sniff/vseparator.xpm, sniff/window.xpm: Add icons, copied from at-poke * sniff/sniff: add constants to represent the tree model columns; use icons for nodes, based on code in at-poke. Requires up-to-date pyspi (for the role attribute) 2005-10-08 David Malcolm * examples/recorder.py: Fix the imports so this works again. 2005-10-08 David Malcolm * examples/evolution-test-switching-components.py: New example which repeatedly cycles through Evolution's components. 2005-10-08 Zack Cerza * sniff/sniff: Expand the root node of the TreeView on startup. 2005-10-08 Zack Cerza * dogtail.spec: Added, from Jeremy Katz. * MANIFEST.in: Add dogtail.spec. * Makefile: Rework 'rpm' target to call the 'tarball' target first, then build the rpm using dogtail.spec, using sudo if necessary. Also, make the 'tarball' target call the 'clean' target. 2005-10-08 Zack Cerza * examples/google-search.py: Fix call to findChildren(). Fix reference to predicate.GenericPredicate. 2005-10-08 Zack Cerza * examples/firefox-test-browsing-local-html-file.py, examples/gnome-panel-test-starting-every-app.py, examples/google-search.py, examples/nautilus-test-icon-view-collage.py: s/dogtail\.Config/dogtail.config.Config/ 2005-10-17 Zack Cerza * dogtail/distro.py: Add patch from Tim Gerla, adding Conary support. Closes: #318224 2005-10-07 Zack Cerza * dogtail/distro.py: Add patch from Brent Smith, adding Gentoo support. Closes: #318225 2005-10-07 Zack Cerza * ChangeLog: Add history since import, tweak formatting. Also add commit times (maybe we don't actually want them though, I'm not sure I care). 2005-10-07 David Malcolm * examples/gedit-test-utf8-tree-api.py: Fix the example against changes to the API. 2005-10-07 David Malcolm * dogtail/tree.py: Provide a better warning for when import wnck fails (it's a warning, not an error). Make Root.application() return an Application instance, rather than a mere Node. * ChangeLog: Created. 2005-10-07 Zack Cerza * dogtail/distro.py: Minor indentation fix. 2005-10-06 Zack Cerza Released 0.4.2. 2005-10-06 Zack Cerza * setup.py, debian/changelog, dogtail/__init__.py, dogtail/apps/__init__.py, dogtail/apps/wrappers/__init__.py: Bump version numbers. 2005-10-06 Zack Cerza * MANIFEST.in: add MANIFEST.in. (Why do I have to do this again?) * setup.py: Add more examples and example data to distribution. * Makefile: Add 'tarball' target. Fix 'deb' target. 2005-10-06 Zack Cerza Released 0.4.1. 2005-10-06 Zack Cerza * setup.py, debian/changelog, dogtail/__init__.py, dogtail/apps/__init__.py, dogtail/apps/wrappers/__init__.py: Bump version numbers. * MANIFEST.in: Add missing files. 2005-10-06 Zack Cerza Released 0.4.0. 2005-10-06 Zack Cerza * setup.py, debian/changelog, dogtail/__init__.py, dogtail/apps/__init__.py, dogtail/apps/wrappers/__init__.py: Minor corrections to version numbers. 2005-10-06 Zack Cerza * dogtail/utils.py: Make screenshot() log the filename of the screenshot via the debug logger. 2005-10-06 Zack Cerza * dogtail/logging.py: Add TimeStamp.zeroPad() to pad integers with zeroes to a specified width. Make TimeStamp.{file,entry}Stamp use the new zeroPad() function. 2005-10-05 Zack Cerza * MAINTAINERS: Created. 2005-10-05 Zack Cerza * COPYING, HACKING, INSTALL, MANIFEST.in, Makefile, setup.cfg, setup.py, debian/changelog, debian/compat, debian/control, debian/copyright, debian/python-dogtail.install, debian/rules, dogtail/__init__.py, dogtail/config.py, dogtail/distro.py, dogtail/dump.py, dogtail/path.py, dogtail/predicate.py, dogtail/procedural.py, dogtail/rawinput.py, dogtail/tree.py, dogtail/utils.py, dogtail/version.py, dogtail/logging.py, dogtail/tc.py, dogtail/apps/__init__.py, dogtail/apps/categories.py, dogtail/apps/wrappers/__init__.py, dogtail/apps/wrappers/epiphany.py, dogtail/apps/wrappers/evolution.py, dogtail/apps/wrappers/gnomepanel.py, dogtail/apps/wrappers/kicker.py, dogtail/apps/wrappers/konqueror.py, dogtail/apps/wrappers/mozilla.py, dogtail/apps/wrappers/nautilus.py, examples/COPYING, examples/abiword-test.py, examples/crack.py, examples/evolution-test-composing-html.py, examples/evolution-test-configuring-exchange.py, examples/evolution-test-configuring-imap-smtp.py, examples/evolution-test-sending-email.py, examples/evolution-test-survives-email-CAN-2005-0806.py, examples/firefox-test-browsing-local-html-file.py, examples/gedit-test-utf8-procedural-api.py, examples/gedit-test-utf8-tree-api.py, examples/gnome-panel-test-starting-every-app.py, examples/google-search.py, examples/nautilus-test-icon-view-collage.py, examples/recorder.py, examples/rhythmbox-test.py, examples/test-events.py, examples/data/UTF-8-demo.txt, examples/data/sample.cfg, sniff/sniff, sniff/sniff.desktop, sniff/sniff.glade, examples/data/CAN-2005-0806.mbox, examples/data/g-star.png, examples/data/g-star1.png, examples/data/GNOME-Street.png, examples/data/GNOME-Street1.png: Initial Import dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/COPYING000066400000000000000000000431331520163156700220570ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/FAQ.md000066400000000000000000000063311520163156700217540ustar00rootroot00000000000000# FAQ, lessons learned and expected usage, for example based on Issues filed. ### Why is the find_ancestor (findAncestor) method not compatible. - In dogtail-1.x.y this was a simple function that returned None if nothing was discovered when traversing the tree upwards. - Now to keep the same logic you have to use `find_ancestor(, require_result=False, retry=False)` since it will attempt to find positive result the same way the find_child function will. ### Why was procedure.py removed? - This is no longer supported way to use the API. - Confusing usage which I have removed in favor of tree based approach. ### Why was sniff removed? - There are better alternatives out there - We have no reason to keep our own version with as limited resources as we have. - Try to use the `.tree()` method over your Accessible objects to find what you are searching for. ### Why was X/Y removed? - If there is a module or function that was frequently used and it broke your code, file an Issue. - I will either explain why it happened and what are the alternatives. - Or I will make sure to re-implement the feature back, since my approach to refactoring dogtail to 2.x.y was to remove anything I have never touched and deemed obsolete. ### We do support long key presses: https://gitlab.com/dogtail/dogtail/-/issues/42 - Documentation is not complete, try to first go through the API on your own to see if the function exists. Most likely you will want to look through two files. - tree.py - rawinput.py ### Assistive technology can appear as if it was not enabled and failing on it: https://gitlab.com/dogtail/dogtail/-/issues/44 - If your distribution has a different way to enable Accessibility than we are checking, turn of the check that dogtail does so you are not blocked: - Expected in dogtail-1.x - needs to be worked around. - Does not happen with dogtail-2.x - no such check present. - `config.check_for_a11y = False` ### The `get_accessible_id` is supported in dogtail-2.x.y: https://gitlab.com/dogtail/dogtail/-/issues/45 - Based on user need I have added the logic, but I am not backporting it to dogtail-1.x.y - We do accept contributions though, if required you can open MR with this logic for the dogtail-1.x.y branch. ### Not all keyboard key events are in a format you would expect: https://gitlab.com/dogtail/dogtail/-/issues/48 - We are not making our own key aliases usually, but it is possible. - If some format is not working, try to go through the Issue to look how the keys in GDK look and try to use those. - We can create aliases if required. ### Version from __version__ might not be accurate when building with setuptools-scm: https://gitlab.com/dogtail/dogtail/-/issues/36 - I will try to find a solution if this problem remains after official release. - I had issues trying to make this work with devX versions. ### The role name `push button` no longer works: https://gitlab.com/dogtail/dogtail/-/issues/35 - This was changed some time ago, now the role name is named `button`. ### You are getting NotImplementedErrorL https://gitlab.com/dogtail/dogtail/-/issues/34 - Try to see if your object has this interface implemented, as this is most likely reason why this happens. dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/MAINTAINERS000066400000000000000000000003611520163156700225150ustar00rootroot00000000000000Vitezslav Humpa Email: vhumpa@redhat.com Userid: vhumpa Michal Odehnal Email: modehnal@redhat.com Userid: modehnal #### Preferably use our https://gitlab.com/dogtail/dogtail home for communication instead of writing us directly, thank you!dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/Makefile000066400000000000000000000025241520163156700224630ustar00rootroot00000000000000# dogtail *development* Makefile # TODO all: install3 install: clean python setup.py build python setup.py install --root=$(DESTDIR) install3: clean3 python3 setup.py build python3 setup.py install --root=$(DESTDIR) clean: rm -rf api_docs/ rm -f MANIFEST rm -rf build dist find . -name '*.pyc' -exec rm {} \; clean3: clean python3 setup.py clean check: pep8 --max-line-length=120 dogtail/*.py tests/*.py scripts/* sniff/sniff test: LC_ALL=C nose2 tests tarball: python3 setup.py sdist rpm: tarball # Build using the custom rpmrc in the rpms/ sub-dir rpmbuild -tb dist/dogtail-*.tar.gz # Move the source and binary RPMs to dist/ mv ~/rpmbuild/RPMS/noarch/* dist/ srpm: rpm_prep # Build using the custom rpmrc in the rpms/ sub-dir rpmbuild --rcfile /usr/lib/rpm/rpmrc:/usr/lib/rpm/redhat/rpmrc:`pwd`/rpms/tmp.rpmrc -ts dist/dogtail-*.tar.gz # Move the source and binary RPMs to dist/ mv rpms/SRPMS/* dist/ rm -rf rpms/ apidocs: apidocs_html apidocs_pdf apidocs_html: epydoc --html --config epydoc.conf apidocs_pdf: epydoc --pdf --config epydoc.conf mv api_docs/api.pdf api_docs/dogtail.pdf update_apidocs: apidocs # Sadly, I'm still the only one who can update the API docs. ssh zmc@fedorapeople.org rm -rf \~/public_html/dogtail/epydoc/* scp api_docs/*.{html,css,png,pdf} zmc@fedorapeople.org:~/public_html/dogtail/epydoc/ dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/NEWS000066400000000000000000000704721520163156700215310ustar00rootroot00000000000000dogtail-2.0 =========== - Tracked in Issue #29 - Remove obsolete and legacy code that I have not observed in usage for several years, working with dogtail and automation in general. - Improve Wayland support, iron out some wrinkles in very specific corner cases. - Improving code structure and moving specific parts to its own files to improve readability in high level Node. - Remove all pyatspi2 usage, switching to only Atspi usage. - Adding accessibility classes, which were heavily inspired by Orca. - Logging was rewritten to be more useful. - The ponytail handling was rewritten and added to its own file, so it is not implemented in several files. - Printing Accessibility Structure Representation with dump and tree was improved in readability and efficiency. - Configuration handling was improved and ability to have config file per project was added. - Changing casing to snake_case, but still have backwards compatibility to camelCase. - Custom keys and values - users can define their own config values in .ini file. - Type checking: users will get ValueError when trying to set e.g. bool to value defined as float in config.ini. - Unittests were rewritten and their execution have been validated with testing-farm. These will now execute before every MR. - Examples were updated. - Removing Sniff, I will not release dogtail with Sniff in a broken state. It is not working well under Wayland and effort to rewrite it would be great. If someone is willing to rewrite it and improve it I will accept MR with it. Until then there are better tools that fullfil the same role. - Preparation for Newton was added to Accessible classes, currently switched off. - Preparation for documentation is also present as docstring in class methods, but not executed upon yet. For now one of my colleagues said he will take a look, if not, I will do it myself later. - README file was updated with up to date information. dogtail 1.0 =========== After more than five years of continuous experimentation and development, we are excited to finally release Dogtail 1.0—a Wayland-enabled version of Dogtail! How did we achieve this? It was made possible by the `gnome-ponytail-daemon`, originally crafted by Olivier Fourdan: https://gitlab.gnome.org/ofourdan/gnome-ponytail-daemon. This tool allows us to perform actions in a Wayland GNOME session in a similar way to how we have been doing so with X functions. ## How does it work in brief? The core functionality relies on the Screen Cast and Remote Desktop API, enabling us to "connect" to either a specific window or the entire screen to simulate user input actions like key presses, typing, and—most importantly—mouse actions on specific coordinates. Ponytail uses the window list from `org.gnome.Shell.Introspect` to identify windows. Dogtail then ensures the correct window is connected via the Remote Desktop API, allowing input actions to be accurately executed within it. On the AT-SPI tree and accessibility's "introspection" side, not much has changed—input has primarily been the challenge on Wayland. The main difference is that only "local" window coordinates are known for UI items. To address this, we always connect to the specific window we're working with, and Ponytail's `connectWindow` ensures these local coordinates are translated to global ones internally. ## What does this mean for users? Dogtail handles all of this logic seamlessly, so in practical use, the user doesn't need to worry about coordinate types or window connections. In fact, the vast majority of our tests work identically in both X and Wayland sessions using the Dogtail API. When running on an X session, Dogtail will use the traditional X functions to handle input and operate as it always has. ## A long journey to 1.0 We began working on the Wayland-enabled version over five years ago (originally available in the `devel/wayland` branch). It has taken all this time to ensure the solution is robust and reliable enough to warrant a 1.0 release. This version includes all the Wayland-related developments and other changes (like the GTK4 support tweaks mentioned below) and has been extensively tested. Importantly, it is backward compatible with the entire Dogtail API that has been available so far. This release includes all modules from the 0.9.x series, most notably the "procedural" module, which we plan to deprecate in favor of a completely "tree-based" approach in Dogtail 2.0. That release will also involve significant cleanup, major code refactoring, and a transition from `pyatspi` to directly introspected `atspi`. It will also contain up date code examples and dogtail's unittests should pass with gitlab pipelines re-enabled. For more details on these upcoming changes, see issue #29. ## Important: Handling GTK4 Application Windows in Dogtail For GTK4 applications, disabling window shadows is essential to ensure accurate positioning by Dogtail. With shadows disabled, we encounters a consistent coordinate offset, which we've preconfigured as `dogtail.config.gtk4Offset`. In case of working with fullscreen windows, the offset is 0, and we manage to detect that on-the-fly both in x11 and wayland sessions. However this process requires `python-xlib`, even for Wayland sessions, leveraging Xwayland to ascertain resolution information, as no more direct method we've found currently available for Wayland. When window shadows are active, the perceived offset can vary significantly, influenced by factors such as the specific application, window size, and scaling settings. To ensure consistent behavior across applications, disabling shadows is recommended. Disabling Shadows in GTK4: To disable window shadows, add the following CSS to your GTK4 configuration (`~/.config/gtk-4.0/gtk.css`): window, .popover, .tooltip { box-shadow: none; } dogtail 0.9.10 ============== There has been a good deal of fixes and improvements since last year's release. We're not really ready for 1.0 yet, which is why we release this important cumulative release as 0.9.10. We have also moved our homesite to gitlab (fedoraproject has been discontinued). For the list of changes, simply go to: https://gitlab.com/dogtail/dogtail/commits/master dogtail 0.9.9 ============= A 1.0 pre-release. Contains a big number of fixes and changes done since 0.9.0. Most important of these is framework's compatibility with Python3. We've modified the code everywhere to run with both Python 2.7 and 3.3+. This means that from now on we maintain the same codebase and are releasing the same tarball, which can be used to package both py2 and py3 installations downstream. A complete list of fixes and changes will be present here, when we release 1.0 after 0.9.9 has been thouroughly tested. Though considered a pre-release, don'worry! 0.9.9 is considered stable and have gone through some rigorious testing already. Thanks to newly extended unit-test-set, it is actually the most self-tested release so far. dogtail 0.9.0 ============= Over the last year we've made a good number of fixes, added some new features and got overall stability to the point where we can make a new 'major minor' release. With 0.9 we're finally getting only a step away from what we'd like to get done with 1.0. * Added a fix for situations when a previously crashed application still registered with at-spi blocks search ('Broken' session problem) Fixed for root.application(), root.applications() and for whenever GeneralPredicate is used (.child()). Also done for Sniff. RHBZ 972257 * Added a support for gnome-continuous build-api (#36) * Added a direct lambda support in the findChildren method, that allows using non-predicate lambdas to create search properties. These can be all pure dogtail Node properties (i.e. showing), not limiting to just pyatspi ones as is when using Predicate. * Added a GnomeShell 'helper' class into utils module allowing to work with the new top-panel application menu. * With GTK3.10+ Gdk changes, needed to tune the keyNameToKeyCode function so that it provides valid keySym in as many cases possible - with both pre-GTK3.10 as well as GTK3.11+ having the same behavior in typeText and pressKey * Sniff ui update: the info pane now stays down on resizing * Got rid of some deprecated GObject calls * Several updates to the dogtail-run-headless-next - Any kind of session can now be used in any combination with both gdm/kdm --session can be used to specify any xsession desktop file, 'kde' defaults to 'kde-plasma' too keep pre-updated compatibility; default is now the previous user session --dm - new param to pick either gdm (default) or kdm --session-binary - new param to specify any in-session binary to grab script env from - to be used with non gnome/kde session (lxde, ubuntu) - do not confuse with session execs like gnome-session and starkde, that may actually exit after session loadup; it should be everpresent (kwin, shell...) - Added a --restart switch to force restart the previously running (often left-over) session before proceeding - More logging of script execution (start/stop/PID), and some cleanups * Trap GErrors in findAllDescendants and retry if it occurs: This fixes crashes in findChildren if elements are being added/removed quickly * Unicode handling updates: - Make sure safeDecode is not skipping any strings - Don't crash when logging message contains unicode * Error Handling for Attribute Error * Fixed utils.run issue when having whitespace containing binary by using shlex shell-like parsing split. * Removed an always true if condition in i18n * Log child description when clicking. The log will now print the element description when it will be clicked * Trap TypeError in findAllDescendants: Sometimes pytaspi returns None as a Node if children are being removed too fast. We should retry search in this case * Extended the default TypingDelay from 0.075 to 0.1 seconds (helps working with certain high unicode charactes) * Fixed an bug in sniff discovered by jmolet that made sniff crash when run from /bin/sniff not /usr/bin/sniff * Generate a better script method call for labels * Fix for failing 'focus.application.node' seting * Derive all classes from object explicitely * Changed icons used in sniff that got moved to the gnome-icon-theme-legacy recently for the ones from non-legacy package. * Unified the 'isSth' vs. 'sth' properties for only 'sth' - added 'selected', but also introduced 'isChecked' property for 0.7.x compatibility * Added instructions on how to enable accessibility into utils.bailBecauseA11yIsDisabled() * Added a 'retry' option to the tree.root.application() * Fixed the concurrent creation of sniff_refresh lock from both tree and procedural * Switched to use environ value to get user in config. os.getlogin breaks when run scheduled from a testing system with no terminal. * Patched the mouse related rawinput methods to prevent using negative coordinates * Updated and added more unittests dogtail 0.8.2 ============= Second update to the 0.8 series containing several fixes and improvements * Added the dogtail-run-headless-next to replace the dogtail-run-headless in future. -next, uses a diplay manager (kdm or gdm) for session management, should be used instead of older headless on systemd systems already * Unittests vastly improved and updated * Fixed a missing reset of FocusWindow to None on application refocus * Fixed the dogtail-logout to use gnome-shell instead of old gnome-panel dogtail 0.8.1 ============= A first update to the new GNOME3 / KDE4 compatible release containing several fixes: * Sniff's autorefresh made togglable, to help avoid collisions with potential running dogtail scripts. Sniff checks at startup whether some script is running and sets up the autorefresh off if it is. * Added a locking mechanism into utils that can be used to solve situations when multiple dogtail processes/thread are running (applied in tree/procedural and sniff already) * Removed the deprecated dependency on CORBA * Icons no longer use absolute pathing in sniff (thx roignac) * Deprecated .has_key() operator replaced with 'in'. * Removed .svg inside sniff's .desktop file dogtail 0.8.0 ============= Finally a big release updating dogtail after more than 2 years ! Dogtail is developed and maintained now again. Simply put, ported to be compatible with the new GNOME 3 and to work well in major GNOME 3 (GTK3) distributions. We'd like to make the 0.8.x version onward the GNOME 3 compatible branch usable for Fedora, RHEL7 and others, while keeping 0.7.x releases for fixes in older GNOME2 systems. --- What was done --- Notably, Sniff's UI needed to ported completely to GTK3, yet there are also several places in dogtail 'itself' that needed to be rewritten to go ahead with the new technologies. Those were all the modules where the old pygtk was used for various reasons (rawinput, utils, tree...). Incompatibilites were also present due to the new version of pyatspi (notably with the doAction method). Release highlights: * A great number of fixes everywhere related to GTK/GNOME/At-Spi updates * The dogtail-recorder was dropped for now, but might return in RC or 0.8.1 * Thanks to the qt-at-spi project, dogtail now works out-of-the-box for QT! * Headless supports KDE sessions * Headless working with full 3D-Accelerated GNOME session (no fallback anymore) * Highlight used in sniff completely re-written and made toggleable (of by default) * Makefile updated, 'make run' builds packages in homedir/rpmbuld now * Several examples fixed for updated gedit * Added tree.isChild() convenience method * Renamed doAction to doActionNamed due to conflict with doAction inside pyatspi namespace * Delay made configurable in absoluteMotion and relativeMotion functions * Changed at-spi activation to use DConf instead of GConf * A bit of code clean-up and modernization (using @property now etc.) * More error checking added * A couple of unit tests for Node added * Node.findChildren() updated to be faster dogtail 0.7.0 ============= This release has too many changes to list. Here are some of the major ones: * Dogtail has been ported to pyatspi. This means: - Faster performance. - Errors will be more clear. - No more segfaulting or myserious crashes. * There is now useful generated API documentation: http://fedorapeople.org/~zmc/dogtail/epydoc/ * Inline documentation has been greatly improved. * Headless execution support was completely rewritten. * Sniff was completely rewritten. It is now: - Faster. - More reliable. - Self-updating via AT-SPI events. - Slightly prettier. - Able to tell you about certain states and relations. * Image Comparison (TCImage) was rewritten. No more ImageMagick! Note: tests/Node.py's TestNodeAttributes is failing due to GNOME bugs #498557 and #498563 dogtail 0.6.1 ============= Features: * dogtail-recorder has a Play button now. * dogtail-recorder now records right-clicks. That it didn't previously was an oversight. Note that there is some inconsistency when dealing with context menus, as the events often get fired in the wrong order. * New logging mechanism: - Debug output is now written to a file. To disable this, set dogtail.config.config.logDebugToFile to False. - The procedural API defaults to not raising FocusErrors, and instead issuing warnings via the debug logging mechanism (which prints to standard out along with writing to a file). To re-enable FocusErrors, set dogtail.config.config.fatalErrors to True. * Brian Cameron submitted a patch to make dogtail.distro correctly detect that it is running on Solaris. Bugfixes: * dogtail-recorder no longer offers the broken Object-Oriented backend as a choice. If it gets fixed, it will be offered again. * dogtail-recorder can be properly exited by clicking on the window manager close button. In fact, the Quit button was removed also. * dogtail-recorder's syntax hilighting works again, thanks to a patch from Dave Malcolm. * Searching for nodes with parentheses in their names will no longer fail. * The various pressKey() functions were blowing up on punctuation and newlines. Fixed. * Predicate subclasses in dogtail.predicate which were missing debugNames have gotten them back. * Warnings are no longer issued when dogtail encounters invalid Unicode. * Where several of our example scripts were broken, Michal Babej noticed and submitted patches fixing them. * dogtail's tarball is 90% smaller! Some overly large test images were replaced with very, very small ones that still do the job. dogtail 0.6.0 ============= Features: * Keystroke recording support in dogtail-recorder: - Normal keypresses (e.g. typing 'foo bar baz') will be recorded as "type('foo bar baz')" in the procedural API. dogtail.tree.Node has a new method, typeText(), which is called by dogtail.procedural.type(). - Nonprintable keypresses (e.g. typing Control-Shift-c) will be recorded as "keyCombo('c')" in the procedural API. dogtail.tree.Node has a new method, keyCombo(), which is called by dogtail.procedural.keyCombo(). - The now-unnecessary 'Input Text' button has been removed. * dogtail.tree.Node has new attributes and a new method: - grabFocus(): Attempts to cause the node to grab the keyboard focus. - focusable: Whether the node is able to have keyboard focus. - focused: Whether the node currently has keyboard focus. - checked: Whether the node is a checkbox that is currently checked. * Dogtail now checks if accessibility is enabled before it does anything. sniff and dogtail-recorder will prompt to enable it via a dialog, and scripts themselves will just refuse to run. * Dogtail no longer uses the IconLogger (in the notification area) by default. Set dogtail.config.config.useIconLogger to True to reenable it. * dogtail.utils.screenshot() was rewritten to use GDK instead of ImageMagick. Bugfixes: * Dogtail is now much more robust in dealing with possibly-invalid Unicode strings passed to it via AT-SPI. (GNOME bug #354515) * Dogtail will not crash when it receives an SpiException caused by a CORBA COMM_FAILURE, unless the source of that error is the application under test. * While exceptions occuring inside callbacks used in dogtail-recorder will still not properly be raised, they are now printed to standard out. * SuSe support in dogtail.distro is fixed. (GNOME bug #353601) * sniff and dogtail-recorder will now find their glade files, even if they're installed into a nonstandard prefix. (GNOME bug #353719, #353731) * dogtail-run-headless had a few bugs fixed. dogtail 0.5.2 ============= Features: * A much-improved recorder: - A procedural script writer, which is now default. - An 'Input Text' feature, which allows you to set the text attribute of any visible Node via the recorder. Coming soon is proper keystroke recording. - The script view now uses syntax hilighting where available. (GNOME bug #345374) * A completely-rewritten dogtail-run-headless, which now offers two session types: minimal GNOME and metacity. The syntax has changed, so use 'dogtail-run-headless --help' to get started. (GNOME bug #320548) * All 'name' arguments in dogtail.tree and dogtail.procedural now accept regular expressions. * Support for AccessibleSelections, providing a clean way to select page tabs and combo box items. (GNOME bug #336562) * Support for building in a jhbuild environment. (GNOME bug #318535) * Far more extensive unit tests for dogtail.tree.Node. Bugfixes: * The source is now indented with spaces and not tabs! :) (GNOME bug #318833) * Pyspi and dogtail no longer mysteriously stop working. (GNOME bug #321273) * Sniff no longer crashes when an application being poked disappears. (GNOME bug #318135) dogtail 0.5.1 ============= Features: * An improved build system for RPMs. * dogtail.procedural's FocusErrors are now more informative. Bugfixes: * examples/recorder.py was broken; a bug in dogtail.predicate.makeCamel() was breaking the predicates' makeScriptVariableName() calls. It's fixed. dogtail 0.5.0 ============= Features: * Automatic runtime translation into the current locale using the application under test's own translations. See examples/gedit-test-utf8-procedural-api.py for an example. * dogtail.distro.packageDb gained new methods: getFiles(), getMoFiles(), and getDependencies(). * dogtail.distro.packageDb now has support for JHBuild environments. * dogtail.distro has gained exceptions: - PackageNotFoundError: raised when the specified package is not found. - DistributionNotSupportedError: raised when support for the current distribution is not currently implemented in dogtail.distro. This does not mean that dogtail will not work at all; only that distribution-specific functionality, like automatic runtime translation, will not. * dogtail.errors is a new module for very general exceptions, such as DependencyNotFoundError, which is raised when the use of a feature that depends on some missing piece of software is attempted. * sniff will now cause the selected node to blink onscreen. * dogtail.tree has a new Node subclass, Link. Link nodes have a roleName of 'hyper link' and an action called 'jump'. Their main purpose is to allow us to follow links in web pages. * dogtail.tree.Node has new properties and methods: - position: the onscreen position of the node, if it wraps an atspi.Component. - size: the onscreen size of the node, if it wraps an atspi.Component. - blink(): causes the node to blink onscreen. - grabFocus(): causes the node to grab the keyboard focus, if it wraps an atspi.Component. - rawClick(): synthesizes a raw mouse click on the node, if it wraps an atspi.Component. - rawType(): synthesizes raw keyboard events to type text into the node, if it wraps an atspi.Component. * dogtail.procedural has new helper methods: - menu(): executes the 'menu' action on the focused widget. - focus.frame(): a shortcut to focus.widget(roleName='frame' ...) - focus.window(): a shortcut to focus.widget(roleName='window' ...) * dogtail.procedural.click() can now synthesize raw mouse events to perform "raw" clicks. * dogtail.rawinput has gained new methods: - pressKey(): synthesizes a raw keyboard event, pressing the key whose name is specified. - typeText(): types a specified string, one key at a time, using raw keyboard events. * dogtail.config was rewritten. * dogtail.config.config has new parameters: - ensureSensitivity: controls whether a NotSensitiveError is raised when an attempt is made to execute an action belonging to a Node instance that is not sensitive, or if a warning is simply printed instead (the default). - debugTranslation: Controls whether details of the autotranslation process will be outputted to the debug logger. * dogtail.config now creates its scratchDir, dataDir, and logDir as soon as the values are set. * dogtail.utils.screenshot(), by default, whether using a custom filename or not, now appends a timestamp to the filename. That may be disabled by passing 'timeStamp = False' to the method. For this reason, it also prints the full path to the screenshot. * dogtail.logging.TimeStamp.fileStamp()'s format has changed, to filename_YYYYMMDD-hhmmss or just filename_YYYYMMDD, controlled by the new optional argument 'addTime'. * dogtail prints the full path to any logfiles it creates. * dogtail-run-headless will now turn accessibility on before executing the script under test, and also restore the old value after the the script has finished executing (GNOME bug #320548). Bugfixes: * sniff will correctly show the text of a non-editable text node again (GNOME bug #321564). * dogtail.tree.screenshot() was mostly rewritten, and no longer breaks when given a custom filename. * dogtail.procedural no longer causes PyDoc to fail miserably (or at all). * dogtail will no longer warn about missing wnck bindings, because they're not actually used anyway. * dogtail.procedural.focus.dialog() no longer searches recursively (GNOME bug #321624). * dogtail.procedural.focus.widget() and its shortcut functions now properly raise a FocusError if their search fails. * dogtail.tc.TCString.compare() will now fail properly again, thanks to Muktha Narayan (GNOME bug #321151). * dogtail.tc.TCImage.compare() now works with the newest version of ImageMagick. That version had changed its behavior, breaking the function. (GNOME bug #321431). * Attempting to instantiate a dogtail.tc.TCImage object while ImageMagick is not installed will now give a comprehensible error message, thanks to the new dogtail.errors.DependencyNotFoundError exception. * Calling dogtail.utils.screenshot() while ImageMagick is not installed will also raise a dogtail.errors.DependencyNotFoundError. * Attempting to execute an action belonging to a Node instance that isn't sensitive no longer fails by default, but prints a warning. This behavior is controlled by the ensureSensitivity configuration parameter. * dogtail-run-headless now correctly returns the exit code of the script being executed (GNOME bug #320535). * dogtail.distro's APT support implementation now only calls apt_pkg.GetCache() once, improving performance significantly when multiple calls to packageDb.get{Version,Dependencies}() are needed. * Building RPM packages from the supplied dogtail.spec works again on distros that are as old or older than RHEL4. * Building RPM packages with 'make rpm' works again without root privileges. API breaks: * Action execution via dogtail.tree.Node instances was forced to change interfaces from the "node.open()" model to a "node.doAction('open')" model. To retain backward compatibility with the application wrappers, the "click" action is still available as "node.click()". The dogtail.procedural interface was able to remain unchanged. * dogtail.config was rewritten to be more robust, and its interface was changed slightly. To port, First, remove any instantiations of the old Config class, and then make the following parameter replacements: - Config => config - logdir => logDir - scratch => scratchDir - data => dataDir dogtail 0.4.3 ============= Features: * Headless support has been added. Use dogtail-run-headless to spawn an X server, a minimal "Nat/Jeff" GNOME session, and execute a given dogtail script before exiting. The dogtail-detect-session script detects a running GNOME session, but KDE support can be dropped in once KDE has accessibility support. * dogtail now uses a notification icon while it is running. Its tooltip is equal to the last message relayed by the debug logger. Most of the code was taken from jhbuild. * Sniff, the AT-SPI browser that uses the dogtail libraries, has gained icons for both applications and specific widgets (based on the role name, taken from at-poke) to make browsing easier. * Sniff also gained a proper dogtail icon. * The procedural API has been given more convenience functions for 'focusing' different types of widgets. * The procedural API has also been given an openItem() method, which works just like click(), for opening icons on nautilus' desktop. * dogtail can now detect Gentoo and Conary distributions, thanks to Brent Smith and Tim Gerla, respectively. * The Node class in dogtail.tree now exports the 'role' attribute in addition to the 'role name'. * Wrappers for gcalctool and yelp were added, thanks to Dave Malcolm. * A wrapper for gedit was added, thanks to Paolo Borelli. * New examples for Evolution, gcalctool and the GTK file chooser were added, thanks to Dave Malcolm. * Dave Malcolm also enhanced the Evolution wrapper, adding support for creating new meetings. * A new example that generates fake documentation for a given application has been added, thanks to Dave Malcolm. * dogtail.spec has been added, thanks to Jeremy Katz, to improve the quality of the RPM packages. Bugfixes: * several examples had not been updated to reflect API changes before the last release. This has been corrected. * The epiphany wrapper now uses the package name 'epiphany-browser' on Debian-based distributions to detect the package version, thanks to Andrew Beresford. dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/README.md000066400000000000000000000130421520163156700222770ustar00rootroot00000000000000# dogtail **dogtail** is a GUI testing and UI automation framework written in Python. It uses Accessibility (AT-SPI) technologies to interact with desktop applications, allowing you to automate real user workflows reliably. Dogtail scripts are standard Python programs and can be run like any other Python code. It works especially well with: * **behave** (BDD testing) * **qecore** (extends behave + dogtail) Dogtail supports both **Xorg** and **Wayland GNOME** environments. > For Wayland usage details, see: [https://fedoramagazine.org/automation-through-accessibility/](https://fedoramagazine.org/automation-through-accessibility/) --- ## Table of Contents * [News](#news) * [FAQ](#faq) * [Installation](#installation) * [Dependencies](#dependencies) * [Wayland Support](#wayland-support) * [GTK4 Notes](#gtk4-notes) * [Usage](#usage) * [API References](#api-references) * [Bugs & Support](#bugs--support) * [Contributing](#contributing) --- ## News ### dogtail 2.x.y Details: [https://gitlab.com/dogtail/dogtail/-/issues/29](https://gitlab.com/dogtail/dogtail/-/issues/29) The project was fully refactored to: * Clean up years of technical debt * Improve maintainability and readability * Prepare for future accessibility backends (e.g., AccessKit/Newton) Rather than continuing to patch dogtail 1.x, the codebase was rebuilt to make future evolution easier and safer. ### dogtail 1.x.y The legacy branch is still maintained as needed: [https://gitlab.com/dogtail/dogtail/-/tree/dogtail-1.x](https://gitlab.com/dogtail/dogtail/-/tree/dogtail-1.x) ## FAQ [https://gitlab.com/dogtail/dogtail/-/blob/master/FAQ.md](https://gitlab.com/dogtail/dogtail/-/blob/master/FAQ.md) --- ## Installation ### From GitLab (build locally) ```bash git clone https://gitlab.com/dogtail/dogtail.git cd dogtail python3 -m build sudo pip3 install dist/dogtail-2.*-py3-none-any.whl ``` ### From GitLab (pip) ```bash sudo dnf install python3-pip sudo python3 -m pip install git+https://gitlab.com/dogtail/dogtail@master ``` ### From PyPI ```bash sudo python3 -m pip install dogtail ``` ### From GitLab Package Registry Packages are built automatically by CI: [https://gitlab.com/dogtail/dogtail/-/packages/](https://gitlab.com/dogtail/dogtail/-/packages/) --- ## Dependencies Core requirements: * Python bindings for your distro (e.g., `python-apt`, `rpm-python`) * GNOME Python libraries Test targets typically include GNOME applications: [https://www.gnome.org/](https://www.gnome.org/) ### Wayland-specific dependency To run on Wayland GNOME, install **gnome-ponytail-daemon**: [https://gitlab.gnome.org/ofourdan/gnome-ponytail-daemon](https://gitlab.gnome.org/ofourdan/gnome-ponytail-daemon) The `gnome-ponytail-daemon` is packaged in Fedora distribution: ``` dnf install -y gnome-ponytail-daemon dnf install -y python3-gnome-ponytail-daemon ``` Building from source: ```bash sudo dnf install meson gcc glib2-devel git clone https://gitlab.gnome.org/ofourdan/gnome-ponytail-daemon.git cd gnome-ponytail-daemon sudo meson setup build sudo ninja -C build sudo ninja -C build install ``` --- ## Wayland Support Wayland input automation is enabled through **gnome-ponytail-daemon**, which uses: * Screen Cast API * Remote Desktop API * GNOME Shell Introspection This allows Dogtail to: * Connect to specific windows * Translate local UI coordinates to global screen positions * Simulate keyboard and mouse input reliably ### What this means in practice Dogtail handles the complexity internally. Most test scripts run identically on: * Xorg sessions * Wayland GNOME sessions On Xorg, traditional X input APIs are used automatically. --- ## GTK4 Notes For GTK4 applications, **window shadows must be disabled** for accurate coordinate handling. Add the following to: ```text ~/.config/gtk-4.0/gtk.css ``` ```css window, .popover, .tooltip { box-shadow: none; } ``` ### Why this is necessary * Shadows introduce variable coordinate offsets * Offsets differ per application, size, and scaling * Disabling shadows ensures consistent automation behavior Dogtail automatically manages known offsets using `dogtail.config.gtk4_offset`. --- ## Usage Currently supported: * GNOME and GTK applications * Xorg and Wayland sessions * Limited Qt support via qt-at-spi (not officially maintained) ### Enable accessibility ```bash gsettings set org.gnome.desktop.interface toolkit-accessibility true ``` Restart applications (or log out/in) after enabling. Headless environments using `dogtail-headless` or `qecore-headless` enable this automatically. --- ## API References * [https://lazka.github.io/pgi-docs/#Atspi-2.0](https://lazka.github.io/pgi-docs/#Atspi-2.0) * [https://docs.gtk.org/atspi2/index.html](https://docs.gtk.org/atspi2/index.html) --- ## Bugs & Support Report issues here: [https://gitlab.com/dogtail/dogtail/issues](https://gitlab.com/dogtail/dogtail/issues) Project home: [https://gitlab.com/dogtail/dogtail/](https://gitlab.com/dogtail/dogtail/) API docs (incomplete for both versions, in progress for 2.x): [http://fedorapeople.org/~vhumpa/dogtail/epydoc/](http://fedorapeople.org/~vhumpa/dogtail/epydoc/) Mailing lists and IRC are deprecated — please use GitLab. --- ## Contributing ### Bug reports * Search existing issues first * Include steps to reproduce and environment details ### Feature ideas * Open an issue to discuss before coding ### Code style guidelines * Use docstrings for all public functions and methods * Follow `snake_case` naming * Prefer descriptive variable names * Write full-sentence comments with proper punctuation Your contributions are welcome! 🚀 dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/configure000077500000000000000000000002631520163156700227300ustar00rootroot00000000000000#!/bin/bash # http://people.gnome.org/~walters/docs/build-api.txt # We don't support separate builddir when building from git #buildapi-variable-no-builddir exec python3 -m build dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail.spec000066400000000000000000000302621520163156700233220ustar00rootroot00000000000000%if 0%{?fedora} || 0%{?rhel} > 9 %global with_python3 1 %endif Summary: GUI test tool and automation framework Name: dogtail Version: 2.0.0 Release: %autorelease License: GPLv2 URL: https://gitlab.com/dogtail/dogtail/ Source0: https://gitlab.com/dogtail/dogtail/raw/released/%{name}-%{version}.tar.gz BuildArch: noarch %global _description\ GUI test tool and automation framework that uses assistive technologies to\ communicate with desktop applications. %description %_description # %package -n python2-dogtail # Summary: %summary # BuildRequires: python2-devel # BuildRequires: python2-setuptools # BuildRequires: desktop-file-utils # Requires: python2-pyatspi # Requires: python2-gobject # Requires: python2-cairo # Requires: xorg-x11-xinit # Requires: hicolor-icon-theme # %{?python_provide:%python_provide python2-dogtail} # # %description -n python2-dogtail %_description %if 0%{?with_python3} %package -n python3-dogtail Summary: GUI test tool and automation framework BuildRequires: python3-devel BuildRequires: python3-setuptools BuildRequires: desktop-file-utils Requires: python3-pyatspi Requires: gnome-ponytail-daemon Requires: python3-gobject Requires: python3-cairo #Requires: xorg-x11-xinit Requires: hicolor-icon-theme %description -n python3-dogtail GUI test tool and automation framework that uses assistive technologies to communicate with desktop applications. %package -n python3-dogtail-scripts Summary: Sniff and other scripts for use with Dogtail framework BuildRequires: python3-devel BuildRequires: python3-setuptools BuildRequires: desktop-file-utils Requires: python3-pyatspi Requires: python3-gobject Requires: python3-cairo #Requires: xorg-x11-xinit Requires: hicolor-icon-theme Requires: python3-dogtail >= 2.0.0 %description -n python3-dogtail-scripts GUI test tool and automation framework that uses assistive technologies to communicate with desktop applications. This subpackage contains Sniff, the a11y exploration tool as well dogtail-run-headless scripts to start session to run tests in. %endif # with_python3 %prep %setup -q -n %{name}-%{version} %if 0%{?with_python3} rm -rf %{py3dir} cp -a . %{py3dir} %endif # with_python3 # %build # %{__python2} setup.py build %if 0%{?with_python3} pushd %{py3dir} %{__python3} setup.py build popd %endif # with_python3 # %install # %{__python2} ./setup.py install -O2 --root=$RPM_BUILD_ROOT --record=%{name}.files # rm -rf $RPM_BUILD_ROOT/%{_docdir}/dogtail # rm -rf $RPM_BUILD_ROOT/%{python2_sitelib}/%{name}-%{version}-py%{python2_version}.egg-info %if 0%{?with_python3} pushd %{py3dir} %{__python3} ./setup.py install -O2 --root=$RPM_BUILD_ROOT --record=%{name}.files rm -rf $RPM_BUILD_ROOT/%{_docdir}/dogtail rm -rf $RPM_BUILD_ROOT/%{python3_sitelib}/%{name}-%{version}-py%{python3_version}.egg-info popd %endif # with_python3 find examples -type f -exec chmod 0644 \{\} \; desktop-file-install $RPM_BUILD_ROOT/%{_datadir}/applications/sniff.desktop \ --dir=$RPM_BUILD_ROOT/%{_datadir}/applications \ %post /bin/touch --no-create %{_datadir}/icons/hicolor &>/dev/null || : %postun if [ $1 -eq 0 ] ; then /bin/touch --no-create %{_datadir}/icons/hicolor &>/dev/null /usr/bin/gtk-update-icon-cache %{_datadir}/icons/hicolor &>/dev/null || : fi %posttrans /usr/bin/gtk-update-icon-cache %{_datadir}/icons/hicolor &>/dev/null || : # %files -n python2-dogtail # %{python2_sitelib}/dogtail/ # %{_datadir}/dogtail/ # %{_datadir}/icons/hicolor/*/apps/%{name}*.* # %doc COPYING # %doc README.md # %doc NEWS # %doc examples/ %if 0%{?with_python3} %files -n python3-dogtail %{python3_sitelib}/dogtail/ %{_datadir}/dogtail/ %{_datadir}/icons/hicolor/*/apps/%{name}*.* %doc COPYING %doc README.md %doc NEWS %doc examples/ %files -n python3-dogtail-scripts %{_bindir}/* %{_datadir}/applications/* %doc COPYING %doc README.md %doc NEWS %endif # with_python3 %changelog * Tue Feb 17 2026 Michal Odehnal - 2.0.0 - Update to upstream version 2.0.0 * Fri Nov 09 2018 Vitezslav Humpa - 0.9.11-1 - Update to upstream version 0.9.10 * Mon Jul 16 2018 Vitezslav Humpa - 0.9.10-1 - Update to upstream version 0.9.10 * Thu Jul 12 2018 Fedora Release Engineering - 0.9.9-12 - Rebuilt for https://fedoraproject.org/wiki/Fedora_29_Mass_Rebuild * Tue Jun 19 2018 Miro Hrončok - 0.9.9-11 - Rebuilt for Python 3.7 * Wed Feb 07 2018 Iryna Shcherbina - 0.9.9-10 - Update Python 2 dependency declarations to new packaging standards (See https://fedoraproject.org/wiki/FinalizingFedoraSwitchtoPython3) * Wed Feb 07 2018 Fedora Release Engineering - 0.9.9-9 - Rebuilt for https://fedoraproject.org/wiki/Fedora_28_Mass_Rebuild * Tue Jan 09 2018 Troy Dawson - 0.9.9-8 - Update conditional * Sat Aug 19 2017 Zbigniew Jędrzejewski-Szmek - 0.9.9-7 - Python 2 binary package renamed to python2-dogtail See https://fedoraproject.org/wiki/FinalizingFedoraSwitchtoPython3 * Wed Jul 26 2017 Fedora Release Engineering - 0.9.9-6 - Rebuilt for https://fedoraproject.org/wiki/Fedora_27_Mass_Rebuild * Fri Feb 10 2017 Fedora Release Engineering - 0.9.9-5 - Rebuilt for https://fedoraproject.org/wiki/Fedora_26_Mass_Rebuild * Mon Dec 19 2016 Miro Hrončok - 0.9.9-4 - Rebuild for Python 3.6 * Tue Jul 19 2016 Fedora Release Engineering - 0.9.9-3 - https://fedoraproject.org/wiki/Changes/Automatic_Provides_for_Python_RPM_Packages * Wed Feb 03 2016 Fedora Release Engineering - 0.9.9-2 - Rebuilt for https://fedoraproject.org/wiki/Fedora_24_Mass_Rebuild * Mon Jan 18 2016 Vitezslav Humpa - 0.9.9-1 - Update to upstream version 0.9.9 - Upstream now supports Python 3, built as python3-dogtail sub-package * Wed Jun 17 2015 Fedora Release Engineering - 0.9.0-4 - Rebuilt for https://fedoraproject.org/wiki/Fedora_23_Mass_Rebuild * Mon Dec 08 2014 David King - 0.9.0-3 - Depend on hicolor-icon-theme for icon directory ownership (#1171906) * Sat Jun 07 2014 Fedora Release Engineering - 0.9.0-2 - Rebuilt for https://fedoraproject.org/wiki/Fedora_21_Mass_Rebuild * Wed Apr 16 2014 Vitezslav Humpa - 0.9.0-1 - Update to upstream version 0.9.0 * Sat Aug 03 2013 Fedora Release Engineering - 0.8.2-2 - Rebuilt for https://fedoraproject.org/wiki/Fedora_20_Mass_Rebuild * Tue Jul 9 2013 Vitezslav Humpa - 0.8.2-1 - Update to upstream version 0.8.2 * Sat Feb 23 2013 Rahul Sundaram - 0.8.1-5 - drop unversioned obsolete * Sat Feb 23 2013 Rahul Sundaram - 0.8.1-4 - remove vendor tag from desktop file. https://fedorahosted.org/fpc/ticket/247 - clean up spec to follow current guidelines * Wed Feb 13 2013 Fedora Release Engineering - 0.8.1-3 - Rebuilt for https://fedoraproject.org/wiki/Fedora_19_Mass_Rebuild * Thu Oct 18 2012 Vitezslav Humpa - 0.8.1-2 - Respin * Tue Oct 16 2012 Vitezslav Humpa - 0.8.1-1 - New upstream release * Wed Jul 18 2012 Fedora Release Engineering - 0.8.0-3 - Rebuilt for https://fedoraproject.org/wiki/Fedora_18_Mass_Rebuild * Mon Jun 04 2012 Jaroslav Reznik - 0.8.0-2 - respin * Thu May 31 2012 Jaroslav Reznik - 0.8.0-1 - Update to 0.8.0 Final - New upstream release * Mon Apr 16 2012 Jaroslav Reznik - 0.8.0-0.5.beta5 - Update to 0.8.0 beta 5 * Mon Apr 02 2012 Jaroslav Reznik - 0.8.0-0.2.beta2 - Update to 0.8.0 beta 2 * Mon Mar 19 2012 Jaroslav Reznik - 0.8.0-0.1.beta1 - Update to 0.8.0 beta 1 * Fri Jan 13 2012 Fedora Release Engineering - 0.7.0-4 - Rebuilt for https://fedoraproject.org/wiki/Fedora_17_Mass_Rebuild * Tue Feb 08 2011 Fedora Release Engineering - 0.7.0-3 - Rebuilt for https://fedoraproject.org/wiki/Fedora_15_Mass_Rebuild * Wed Jul 21 2010 David Malcolm - 0.7.0-2 - Rebuilt for https://fedoraproject.org/wiki/Features/Python_2.7/MassRebuild * Thu Oct 08 2009 Zack Cerza - 0.7.0-1 - New upstream release. - Drop Requires on xorg-x11-server-Xvfb. - Update URL and Source0. - Ship NEWS file. * Fri Jul 24 2009 Fedora Release Engineering - 0.6.90-4.401 - Rebuilt for https://fedoraproject.org/wiki/Fedora_12_Mass_Rebuild * Tue Feb 24 2009 Fedora Release Engineering - 0.6.90-3.401 - Rebuilt for https://fedoraproject.org/wiki/Fedora_11_Mass_Rebuild * Sat Nov 29 2008 Ignacio Vazquez-Abrams - 0.6.90-2.401 - Rebuild for Python 2.6 * Tue Aug 12 2008 Zack Cerza - 0.6.90-1.401 - New upstream snapshot. - Require python-imaging * Tue Aug 12 2008 Zack Cerza - 0.6.90-1.381.2 - Really fix license tag. * Wed Jul 16 2008 Tom "spot" Callaway - 0.6.90-1.381.1 - fix license tag * Thu Jan 31 2008 Zack Cerza - 0.6.90-1.381 - New upstream snapshot. - Obsolete pyspi; Require at-spi-python. - Require pygtk2-libglade. - Don't ship the .egg-info file. * Wed Jan 3 2007 Zack Cerza - 0.6.1-1 - New upstream release. * Thu Dec 7 2006 Jeremy Katz - 0.6.0-2 - build for python 2.5 - BR python-devel * Wed Sep 13 2006 Zack Cerza - 0.6.0-1 - New upstream release. - Add Requires for xorg-x11-xinit. - Add Requires for gnome-python2-gconf. - Bump pyspi Requires. - Remove upstreamed patches. * Fri Aug 18 2006 Zack Cerza - 0.5.2-3 - Add Requires for xorg-x11-xinit. Closes: #203189. * Fri Aug 11 2006 Zack Cerza - 0.5.2-2 - Added headless-gconf.patch to use the python gconf bindings. - Added desktop-file-categories.patch to put sniff and dogtail-recorder under the 'Programming' menu. * Tue Aug 01 2006 Zack Cerza - 0.5.2-1 - New upstream release. - Update Requires from Xvfb to xorg-x11-server-Xvfb. - Bump pyspi Requires. - Remove ImageMagick Requires. - Escape post-macro in changelog-macro. * Mon Apr 17 2006 Zack Cerza - 0.5.1-3 - Fix the URL field. * Tue Mar 21 2006 Zack Cerza - 0.5.1-2 - Fix URL and Source0 fields. - Fix desktop-file-utils magic; use desktop-file-install. * Fri Feb 24 2006 Zack Cerza - 0.5.1-1 - Remove BuildRequires on at-spi-devel. Added one on python. - Use macros instead of absolute paths. - Touch _datadir/icons/hicolor/ before running gtk-update-icon-cache. - Require and use desktop-file-utils. - postun = post. - Shorten BuildArchitectures to BuildArch. The former worked, but even vim's hilighting hated it. - Put each *Requires on a separate line. - Remove __os_install_post definition. - Use Fedora Extras BuildRoot. - Instead of _libdir, which kills the build if it's /usr/lib64, use a python macro to define python_sitelib and use that. - Remove the executable bit on the examples in install scriptlet. - Remove call to /bin/rm in post scriptlet. - Use dist in Release. * Fri Feb 17 2006 Zack Cerza - 0.5.0-2 - It looks like xorg-x11-Xvfb changed names. Require 'Xvfb' instead. - Remove Requires on python-elementtree, since RHEL4 didn't have it. The functionality it provides is probably never used anyway, and will most likely be removed in the future. - Don't run gtk-update-icon-cache if it doesn't exist. * Fri Feb 3 2006 Zack Cerza - 0.5.0-1 - New upstream release. - Added missing BuildRequires on at-spi-devel. - Added Requires on pyspi >= 0.5.3. - Added Requires on rpm-python, pygtk2, ImageMagick, xorg-x11-Xvfb, python-elementtree. - Moved documentation (including examples) to the correct place. - Make sure /usr/share/doc/dogtail is removed. - Added 'gtk-update-icon-cache' to %%post. * Mon Oct 24 2005 Zack Cerza - 0.4.3-1 - New upstream release. * Sat Oct 8 2005 Jeremy Katz - 0.4.2-1 - Initial build. dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/000077500000000000000000000000001520163156700224435ustar00rootroot00000000000000dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/__init__.py000066400000000000000000000014411520163156700245540ustar00rootroot00000000000000#!/usr/bin/python3 """ GUI test tool and automation framework that uses Accessibility (a11y) technologies to communicate with desktop applications. """ __author__ = """ Michal Odehnal , Vita Humpa , Zack Cerza , Ed Rousseau , David Malcolm , """ try: from ._version import __version__ as imported_version __version__ = imported_version except Exception: __version__ = "Unavailable" __url__ = "https://gitlab.com/dogtail/dogtail" __copyright__ = "Copyright © 2005-2026 Red Hat, Inc." __license__ = "GPLv2" __all__ = ( "config", "distro", "dump", "errors", "logging", "ponytail_helper", "predicate", "rawinput", "tree", "utils", "version", ) dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/accessibles/000077500000000000000000000000001520163156700247235ustar00rootroot00000000000000dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/accessibles/__init__.py000066400000000000000000000000001520163156700270220ustar00rootroot00000000000000dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/accessibles/accessible_actions.py000066400000000000000000000163171520163156700311220ustar00rootroot00000000000000#!/usr/bin/python3 """ Handling for Accessible Action. """ # pylint: disable=broad-exception-caught # pylint: disable=wrong-import-position # pylint: disable=wrong-import-order # pylint: disable=import-error # pylint: disable=no-name-in-module # ruff: noqa: E402 # ruff: noqa: E501 import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi from dogtail.logging import logging_class LOGGING = logging_class.logger from dogtail.config import config from dogtail.utils import do_delay from dogtail.accessibles.accessible_object import AccessibleObject class ActionNotSupported(Exception): """ The widget does not support the requested action. """ def __init__(self, message=None): LOGGING.debug("Action Not Supported Exception") self.message = message def __str__(self): return self.message @logging_class.log_a_class class AccessibleActions: """ Action class. Heavily inspired by Orca's classes. """ @staticmethod def do_action(acc_object, index): """ Invoke the action indicated by index on the object implementing Atspi.Action. """ if not AccessibleObject.is_action(acc_object): LOGGING.debug("Accessible object does not have an Action interface.") return False if AccessibleObject.is_newton(acc_object): return acc_object.do_action(acc_object, index) action_result = Atspi.Action.do_action(acc_object, index) do_delay(config.action_delay) return action_result @staticmethod def get_action_description(acc_object, index): """ Get the description of the index-th Action on the object implementing Atspi.Action. """ if not AccessibleObject.is_action(acc_object): LOGGING.debug("Accessible object does not have an Action interface.") return None try: if AccessibleObject.is_newton(acc_object): invocable_actions = acc_object.get_action_description(index) else: invocable_actions = Atspi.Action.get_action_description(acc_object, index) except Exception as error: LOGGING.debug(f"Exception getting actions: '{error}'") raise ActionNotSupported(f"Issue with get_action_description on object '{acc_object}'") return invocable_actions @staticmethod def get_action_name(acc_object, index): """ Get the name of the index-th Action on the object implementing Atspi.Action. """ if not AccessibleObject.is_action(acc_object): LOGGING.debug("Accessible object does not have an Action interface.") return None try: if AccessibleObject.is_newton(acc_object): invocable_actions = acc_object.get_action_name(index) else: invocable_actions = Atspi.Action.get_action_name(acc_object, index) except Exception as error: LOGGING.debug(f"Exception getting actions: '{error}'") raise ActionNotSupported(f"Issue with get_action_name on object '{acc_object}'") return invocable_actions @staticmethod def get_key_binding(acc_object, action_index): """ Get the keybindings of the index-th Action on the object implementing Atspi.Action. """ if not AccessibleObject.is_action(acc_object): LOGGING.debug("Accessible object does not have an Action interface.") return None try: if AccessibleObject.is_newton(acc_object): action_key_bind = acc_object.get_key_binding(action_index) else: action_key_bind = Atspi.Action.get_key_binding(acc_object, action_index) except Exception as error: LOGGING.debug(f"Exception getting action key bind: '{error}'") raise ActionNotSupported(f"Issue with get_key_binding on object '{acc_object}'") return action_key_bind @staticmethod def get_localized_name(acc_object, index): """ Get the localized name of the index-th Action on the object implementing Atspi.Action. """ if not AccessibleObject.is_action(acc_object): LOGGING.debug("Accessible object does not have an Action interface.") return None try: if AccessibleObject.is_newton(acc_object): invocable_actions = acc_object.get_localized_name(index) else: invocable_actions = Atspi.Action.get_localized_name(acc_object, index) except Exception as error: LOGGING.debug(f"Exception getting actions: '{error}'") raise ActionNotSupported(f"Issue with get_localized_name on object '{acc_object}'") return invocable_actions @staticmethod def get_n_actions(acc_object): """ Get the number of the actions invocable on the object implementing Atspi.Action. """ if not AccessibleObject.is_action(acc_object): LOGGING.debug("Accessible object does not have an Action interface.") return None try: if AccessibleObject.is_newton(acc_object): invocable_actions = acc_object.get_n_actions() else: invocable_actions = Atspi.Action.get_n_actions(acc_object) except Exception as error: LOGGING.debug(f"Exception getting actions: '{error}'") raise ActionNotSupported(f"Issue with get_n_actions on object '{acc_object}'") return invocable_actions @staticmethod def get_actions(acc_object): """ Get dictionary of actions. """ actions = {} try: for action_index in range(AccessibleActions.get_n_actions(acc_object)): action_name = AccessibleActions.get_action_name(acc_object, action_index) actions[action_name] = action_index except Exception as error: LOGGING.debug(f"Exception when getting actions dictionary: '{error}'") return actions @staticmethod def do_action_named(acc_object, named_action): """ Invoke the action indicated by its name on the object implementing Atspi.Action. """ if not isinstance(named_action, str): LOGGING.debug("This function expects action name string.") return False if not AccessibleObject.is_action(acc_object): LOGGING.debug("Accessible object does not have an Action interface.") return False if named_action not in AccessibleActions.get_actions(acc_object): raise ActionNotSupported(f"Wanted action '{named_action}' is not supported on object '{acc_object}'.") try: action_to_do = AccessibleActions.get_actions(acc_object)[named_action] if AccessibleObject.is_newton(acc_object): return acc_object.do_action(acc_object, action_to_do) action_result = AccessibleActions.do_action(acc_object, action_to_do) do_delay(config.search_back_off_delay) return action_result except Exception as error: raise ActionNotSupported(f"Exception when executing a named action: '{error}'") dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/accessibles/accessible_component.py000066400000000000000000000425511520163156700314630ustar00rootroot00000000000000#!/usr/bin/python3 """ Handling for Accessible Component. """ # pylint: disable=broad-exception-caught # pylint: disable=wrong-import-position # pylint: disable=wrong-import-order # pylint: disable=import-outside-toplevel # pylint: disable=invalid-name # pylint: disable=import-error # pylint: disable=no-name-in-module # ruff: noqa: E402 # ruff: noqa: E501 import os import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi from dogtail.config import config from dogtail.logging import logging_class LOGGING = logging_class.logger from dogtail.accessibles.accessible_object import AccessibleObject from dogtail.ponytail_helper import ponytail_helper SESSION_TYPE = "x11" if "XDG_SESSION_TYPE" in os.environ and "wayland" in os.environ["XDG_SESSION_TYPE"]: SESSION_TYPE = "wayland" COORD_TYPE_SCREEN = 0 COORD_TYPE_WINDOW = 1 COORD_TYPE_PARENT = 2 @logging_class.log_a_class class AccessibleComponent: """ Component class. Heavily inspired by Orca's classes. """ @staticmethod def contains(acc_object, x_coordinate, y_coordinate, coordinate_type=None): """ Queries whether a given Atspi.Component contains a particular point. Atspi.CoordType.SCREEN = 0 Atspi.CoordType.WINDOW = 1 Atspi.CoordType.PARENT = 2 """ if not AccessibleObject.is_component(acc_object): LOGGING.debug("Accessible object does not have a Component interface.") return False if coordinate_type not in (0, 1, 2, None): LOGGING.debug(f"Not a valid coordinate type '{coordinate_type}'.") return False # Provide an option for user to set their own coord type. if coordinate_type: valid_coord_type = coordinate_type else: if SESSION_TYPE == "x11": valid_coord_type = coordinate_type if coordinate_type else COORD_TYPE_SCREEN if SESSION_TYPE == "wayland": valid_coord_type = coordinate_type if coordinate_type else COORD_TYPE_WINDOW if not valid_coord_type: LOGGING.debug("Unexpected session type and coordinate type not provided.") return False if AccessibleObject.is_newton(acc_object): return acc_object.contains( acc_object, x_coordinate, y_coordinate, valid_coord_type ) action_result = Atspi.Component.contains( acc_object, x_coordinate, y_coordinate, valid_coord_type ) return action_result @staticmethod def get_accessible_at_point(acc_object, x_coordinate, y_coordinate, coordinate_type=None): """ Gets the accessible child at a given coordinate within an Atspi.Component. Atspi.CoordType.SCREEN = 0 Atspi.CoordType.WINDOW = 1 Atspi.CoordType.PARENT = 2 """ if not AccessibleObject.is_component(acc_object): LOGGING.debug("Accessible object does not have a Component interface.") return False if coordinate_type not in (0, 1, 2, None): LOGGING.debug(f"Not a valid coordinate type '{coordinate_type}'.") return False # Provide an option for user to set their own coord type. if coordinate_type: valid_coord_type = coordinate_type else: if SESSION_TYPE == "x11": valid_coord_type = coordinate_type if coordinate_type else COORD_TYPE_SCREEN if SESSION_TYPE == "wayland": valid_coord_type = coordinate_type if coordinate_type else COORD_TYPE_WINDOW if not valid_coord_type: LOGGING.debug("Unexpected session type and coordinate type not provided.") return False if AccessibleObject.is_newton(acc_object): return acc_object.get_accessible_at_point( acc_object, x_coordinate, y_coordinate, valid_coord_type ) action_result = Atspi.Component.get_accessible_at_point( acc_object, x_coordinate, y_coordinate, valid_coord_type ) return action_result @staticmethod def get_alpha(acc_object): """ Gets the opacity/alpha value of a component, if alpha blending is in use. """ if not AccessibleObject.is_component(acc_object): LOGGING.debug("Accessible object does not have a Component interface.") return False if AccessibleObject.is_newton(acc_object): return acc_object.get_alpha() return Atspi.Component.get_alpha(acc_object) @staticmethod def get_extents(acc_object): """ Gets the bounding box of the specified Atspi.Component. The returned values are meaningful only if the Component has both STATE_VISIBLE and STATE_SHOWING. """ if not AccessibleObject.is_component(acc_object): return None # Atspi.CoordType.SCREEN = 0 # Atspi.CoordType.WINDOW = 1 try: # First lets check if screen coordinates are valid. if AccessibleObject.is_newton(acc_object): acc_rect = acc_object.get_extents(COORD_TYPE_SCREEN) else: acc_rect = Atspi.Component.get_extents(acc_object, Atspi.CoordType.SCREEN) # If not, usually by being 0, 0 attempt to get window coordinates. if (acc_rect.x, acc_rect.y) == (0, 0): if AccessibleObject.is_newton(acc_object): acc_rect = acc_object.get_extents(COORD_TYPE_WINDOW) else: acc_rect = Atspi.Component.get_extents(acc_object, Atspi.CoordType.WINDOW) # Return the tuple of 4 value from Atspi.Rect return (acc_rect.x, acc_rect.y, acc_rect.width, acc_rect.height) except Exception as error: LOGGING.debug(f"Exception when getting position or size: '{error}'") return None @staticmethod def get_layer(acc_object): """ Queries which layer the component is painted into, to help determine its visibility in terms of stacking order. """ if not AccessibleObject.is_component(acc_object): LOGGING.debug("Accessible object does not have a Component interface.") return False if AccessibleObject.is_newton(acc_object): return acc_object.get_layer() return Atspi.Component.get_layer(acc_object) @staticmethod def get_mdi_z_order(acc_object): """ Queries the z stacking order of a component which is in the MDI or window layer. (Bigger z-order numbers mean nearer the top) """ if not AccessibleObject.is_component(acc_object): LOGGING.debug("Accessible object does not have a Component interface.") return False if AccessibleObject.is_newton(acc_object): return acc_object.get_mdi_z_order() return Atspi.Component.get_mdi_z_order(acc_object) @staticmethod def get_position(acc_object): """ A tuple containing the position of the Accessible: (x, y) """ # TODO verbose logging? This will flood logs, perhaps too much even for verbose if not AccessibleObject.is_component(acc_object): LOGGING.debug("Accessible object does not have a Component interface.") return (-1, -1) # Atspi.CoordType.SCREEN = 0 # Atspi.CoordType.WINDOW = 1 # The variable 'position' is of type Atspi.Point. if AccessibleObject.is_newton(acc_object): position = acc_object.get_position(COORD_TYPE_SCREEN) else: position = Atspi.Component.get_position(acc_object, Atspi.CoordType.SCREEN) # Check if the toolkit is GTK4. gtk_value = acc_object.toolkit.get("GTK", None) if gtk_value and gtk_value[0] == "4": #gtk4_offset = (12, 12) gtk4_offset = config.gtk4Offset # Determine if *this* node is a part of a full-screen frame fullscreen_offset = (0, 0) # No offset fullscreen, shadows or not! node = acc_object # Assuming 'acc_object' is the current node try: from dogtail.utils import get_screen_resolution # Check resolution result first. Can be None with caching. resolution_result = get_screen_resolution() if resolution_result: screen_width, _ = resolution_result while node: if node.role_name == "frame": frame_width, _ = node.size if frame_width == screen_width: gtk4_offset = fullscreen_offset break node = node.parent # Traverse up to check for frame ancestors. except Exception: pass # For x11 session, calculate position using WINDOW_COORDS, # add up with window position and finally apply offset if not fullscreen. if SESSION_TYPE == "x11": # The variable position is of type Atspi.Point. if AccessibleObject.is_newton(acc_object): position = acc_object.get_position(COORD_TYPE_WINDOW) else: position = Atspi.Component.get_position(acc_object, Atspi.CoordType.WINDOW) from dogtail.utils import get_current_x_window_position base_x, base_y = get_current_x_window_position() # Add both gtk4_offset and the current x window position # Inserting values in init for point has been deprecated and ignored. atspi_point = Atspi.Point() atspi_point.x = position.x + base_x + gtk4_offset[0] atspi_point.y = position.y + base_y + gtk4_offset[1] position = atspi_point # For wayland. # If it's still (0, 0) return that otherwise do offset directly. if SESSION_TYPE == "wayland": if AccessibleObject.is_newton(acc_object): position = acc_object.get_position(COORD_TYPE_WINDOW) else: position = Atspi.Component.get_position(acc_object, Atspi.CoordType.WINDOW) if (position.x, position.y) != (0, 0): # Inserting values in init for point has been deprecated and ignored. atspi_point = Atspi.Point() atspi_point.x = position.x + gtk4_offset[0] atspi_point.y = position.y + gtk4_offset[1] position = atspi_point return (position.x, position.y) @staticmethod def get_size(acc_object): """ A tuple containing the size of the Accessible: (w, h) """ if not AccessibleObject.is_component(acc_object): LOGGING.debug("Accessible object does not have a Component interface.") return (-1, -1) if SESSION_TYPE == "wayland": if acc_object.role_name in ("window", "dialog", "frame"): ponytail_interface = ponytail_helper.get_ponytail_interface() window_list = ponytail_interface.window_list window_id = acc_object.window_id for window in window_list: if window["id"] == window_id: return (int(window["width"]), int(window["height"])) if AccessibleObject.is_newton(acc_object): atspi_point = acc_object.get_size() else: atspi_point = Atspi.Component.get_size(acc_object) return (atspi_point.x, atspi_point.y) @staticmethod def grab_focus(acc_object): """ Grab focus. """ if not AccessibleObject.is_component(acc_object): LOGGING.debug("Accessible object does not have a Component interface.") return False try: if AccessibleObject.is_newton(acc_object): return acc_object.grab_focus() return Atspi.Component.grab_focus(acc_object) except Exception as error: LOGGING.debug(f"Exception when grabbing focus: '{error}'") return False @staticmethod def scroll_to(acc_object, scroll_type): """ Scrolls whatever container of the Atspi.Component object so it becomes visible on the screen. Atspi.ScrollType.TOP_LEFT = 0 Atspi.ScrollType.BOTTOM_RIGHT = 1 Atspi.ScrollType.TOP_EDGE = 2 Atspi.ScrollType.BOTTOM_EDGE = 3 Atspi.ScrollType.LEFT_EDGE = 4 Atspi.ScrollType.RIGHT_EDGE = 5 Atspi.ScrollType.ANYWHERE = 6 """ if not AccessibleObject.is_component(acc_object): LOGGING.debug("Accessible object does not have a Component interface.") return False if scroll_type not in (0, 1, 2, 3, 4, 5, 6): LOGGING.debug(f"Not a valid scroll type '{scroll_type}'.") return False if AccessibleObject.is_newton(acc_object): return acc_object.scroll_to(scroll_type) return Atspi.Component.scroll_to(acc_object, scroll_type) @staticmethod def scroll_to_point(acc_object, coordinate_type, x_coordinate, y_coordinate): """ Scrolls whatever container of the Atspi.Component object so it becomes visible on the screen at a given position. Atspi.CoordType.SCREEN = 0 Atspi.CoordType.WINDOW = 1 Atspi.CoordType.PARENT = 2 """ if not AccessibleObject.is_component(acc_object): LOGGING.debug("Accessible object does not have a Component interface.") return False if coordinate_type not in (0, 1, 2): LOGGING.debug(f"Not a valid coordinate type '{coordinate_type}'.") return False if AccessibleObject.is_newton(acc_object): return acc_object.scroll_to_point(coordinate_type, x_coordinate, y_coordinate) return Atspi.Component.scroll_to_point(acc_object, coordinate_type, x_coordinate, y_coordinate) @staticmethod def set_extents(acc_object, x_coordinate, y_coordinate, width, height, coordinate_type): """ Moves and resizes the specified component. Atspi.CoordType.SCREEN = 0 Atspi.CoordType.WINDOW = 1 Atspi.CoordType.PARENT = 2 """ if not AccessibleObject.is_component(acc_object): LOGGING.debug("Accessible object does not have a Component interface.") return False if coordinate_type not in (0, 1, 2): LOGGING.debug(f"Not a valid coordinate type '{coordinate_type}'.") return False if AccessibleObject.is_newton(acc_object): return acc_object.set_extents( x_coordinate, y_coordinate, width, height, coordinate_type ) return Atspi.Component.set_extents( acc_object, x_coordinate, y_coordinate, width, height, coordinate_type ) @staticmethod def set_position(acc_object, x_coordinate, y_coordinate, coordinate_type): """ Moves the component to the specified position. Atspi.CoordType.SCREEN = 0 Atspi.CoordType.WINDOW = 1 Atspi.CoordType.PARENT = 2 """ if not AccessibleObject.is_component(acc_object): LOGGING.debug("Accessible object does not have a Component interface.") return False if coordinate_type not in (0, 1, 2): LOGGING.debug(f"Not a valid coordinate type '{coordinate_type}'.") return False if AccessibleObject.is_newton(acc_object): return acc_object.set_position( x_coordinate, y_coordinate, coordinate_type ) return Atspi.Component.set_position( acc_object, x_coordinate, y_coordinate, coordinate_type ) @staticmethod def set_size(acc_object, width, height): """ Resizes the specified component to the given pixel dimensions. """ if not AccessibleObject.is_component(acc_object): LOGGING.debug("Accessible object does not have a Component interface.") return False if AccessibleObject.is_newton(acc_object): return acc_object.set_extents(width, height) return Atspi.Component.set_extents(acc_object, width, height) @staticmethod def get_center(acc_object): """ A tuple containing the the center of the Accessible: (x, y) """ if not AccessibleObject.is_component(acc_object): LOGGING.debug("Accessible object does not have a Component interface.") return (-1, -1) # Getting it this way already checks for Newton, no check here needed. accessible_position = AccessibleComponent.get_position(acc_object) accessible_size = AccessibleComponent.get_size(acc_object) atspi_point = Atspi.Point() atspi_point.x = int(accessible_position[0] + accessible_size[0] / 2) atspi_point.y = int(accessible_position[1] + accessible_size[1] / 2) return (atspi_point.x, atspi_point.y) accessible_editable_text.py000066400000000000000000000163121520163156700322130ustar00rootroot00000000000000dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/accessibles#!/usr/bin/python3 """ Handling for Accessible EditableText. """ # pylint: disable=broad-exception-caught # pylint: disable=wrong-import-position # pylint: disable=wrong-import-order # pylint: disable=import-error # pylint: disable=no-name-in-module # ruff: noqa: E402 # ruff: noqa: E501 import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi from dogtail.logging import logging_class LOGGING = logging_class.logger from dogtail.accessibles.accessible_object import AccessibleObject @logging_class.log_a_class class AccessibleEditableText: """ Editable Text class. Heavily inspired by Orca's classes. """ @staticmethod def copy_text(acc_object, start_position=None, end_position=None): """ Copies text from an Atspi.EditableText object into the system clipboard. """ if not AccessibleObject.is_valid(acc_object): return False if not AccessibleObject.is_text(acc_object): LOGGING.debug("This object does not have Atspi.Text interface.") return False if not AccessibleObject.is_editable_text(acc_object): LOGGING.debug("This object does not have Atspi.EditableText interface.") return False if AccessibleObject.is_newton(acc_object): text_length = acc_object.get_character_count() else: text_length = Atspi.Text.get_character_count(acc_object) if isinstance(start_position, int) and isinstance(end_position, int): validated_start = start_position validated_end = end_position else: validated_start = 0 validated_end = text_length if validated_start == validated_end: LOGGING.debug("There is nothing to copy, start and end are equal.") return False if AccessibleObject.is_newton(acc_object): return acc_object.copy_text(validated_start, validated_end) return Atspi.EditableText.copy_text(acc_object, validated_start, validated_end) @staticmethod def cut_text(acc_object, start_position=None, end_position=None): """ Deletes text from an Atspi.EditableText object, copying the excised portion into the system clipboard. """ if not AccessibleObject.is_valid(acc_object): return False if not AccessibleObject.is_text(acc_object): LOGGING.debug("This object does not have Atspi.Text interface.") return False if not AccessibleObject.is_editable_text(acc_object): LOGGING.debug("This object does not have Atspi.EditableText interface.") return False if AccessibleObject.is_newton(acc_object): text_length = acc_object.get_character_count() else: text_length = Atspi.Text.get_character_count(acc_object) if isinstance(start_position, int) and isinstance(end_position, int): validated_start = start_position validated_end = end_position else: validated_start = 0 validated_end = text_length if validated_start == validated_end: LOGGING.debug("There is nothing to copy, start and end are equal.") return False if AccessibleObject.is_newton(acc_object): return acc_object.cut_text(validated_start, validated_end) return Atspi.EditableText.cut_text(acc_object, validated_start, validated_end) @staticmethod def delete_text(acc_object, start_position=None, end_position=None): """ Deletes text from an Atspi.EditableText object, without copying the excised portion into the system clipboard. """ if not AccessibleObject.is_valid(acc_object): return False if not AccessibleObject.is_text(acc_object): LOGGING.debug("This object does not have Atspi.Text interface.") return False if not AccessibleObject.is_editable_text(acc_object): LOGGING.debug("This object does not have Atspi.EditableText interface.") return False if AccessibleObject.is_newton(acc_object): text_length = acc_object.get_character_count() else: text_length = Atspi.Text.get_character_count(acc_object) if isinstance(start_position, int) and isinstance(end_position, int): validated_start = start_position validated_end = end_position else: validated_start = 0 validated_end = text_length if validated_start == validated_end: LOGGING.debug("There is nothing to copy, start and end are equal.") return False if AccessibleObject.is_newton(acc_object): return acc_object.delete_text(validated_start, validated_end) return Atspi.EditableText.delete_text(acc_object, validated_start, validated_end) @staticmethod def insert_text(acc_object, text, position=0, length=0): """ Inserts text into an Atspi.EditableText object. As with all character offsets, the specified position may not be the same as the resulting byte offset, since the text is in a variable-width encoding. Deliberately moving parameters around so that the text is first. """ if not AccessibleObject.is_valid(acc_object): return False if not AccessibleObject.is_editable_text(acc_object): LOGGING.debug("This object does not have Atspi.EditableText interface.") return False if AccessibleObject.is_newton(acc_object): return acc_object.insert_text(position, text, length) return Atspi.EditableText.insert_text(acc_object, position, text, length) @staticmethod def paste_text(acc_object, position=None): """ Inserts text from the system clipboard into an Atspi.EditableText object. As with all character offsets, the specified position may not be the same as the resulting byte offset, since the text is in a variable-width encoding. """ if not AccessibleObject.is_valid(acc_object): return False if not AccessibleObject.is_editable_text(acc_object): LOGGING.debug("This object does not have Atspi.EditableText interface.") return False if isinstance(position, int): validated_position = position else: validated_position = 0 if AccessibleObject.is_newton(acc_object): return acc_object.paste_text(validated_position) return Atspi.EditableText.paste_text(acc_object, validated_position) @staticmethod def set_text_contents(acc_object, new_contents): """ Replace the entire text contents of an Atspi.EditableText object. """ if not AccessibleObject.is_valid(acc_object): return False if not AccessibleObject.is_editable_text(acc_object): LOGGING.debug("This object does not have Atspi.EditableText interface.") return False if AccessibleObject.is_newton(acc_object): return acc_object.set_text_contents(new_contents) return Atspi.EditableText.set_text_contents(acc_object, new_contents) dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/accessibles/accessible_hypertext.py000066400000000000000000000037721520163156700315170ustar00rootroot00000000000000#!/usr/bin/python3 """ Handling for Accessible Hypertext. """ # pylint: disable=import-error # pylint: disable=wrong-import-position # pylint: disable=wrong-import-order # pylint: disable=no-name-in-module # ruff: noqa: E402 import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi from dogtail.logging import logging_class LOGGING = logging_class.logger from dogtail.accessibles.accessible_object import AccessibleObject @logging_class.log_a_class class AccessibleHypertext: """ Hypertext class. Heavily inspired by Orca's classes. """ @staticmethod def get_link(acc_object, link_index): """ Gets the Atspi.Hyperlink object at a specified index. """ if not AccessibleObject.is_hypertext(acc_object): LOGGING.debug("Object is not valid Accessible.") return False if AccessibleObject.is_newton(acc_object): return acc_object.get_link(link_index) return Atspi.Hypertext.get_link(acc_object, link_index) @staticmethod def get_link_index(acc_object, character_offset): """ Gets the index of the Atspi.Hyperlink object at a specified character offset. """ if not AccessibleObject.is_hypertext(acc_object): LOGGING.debug("Object is not valid Accessible.") return False if AccessibleObject.is_newton(acc_object): return acc_object.get_link_index(character_offset) return Atspi.Hypertext.get_link_index(acc_object, character_offset) @staticmethod def get_n_links(acc_object): """ Gets the total number of Atspi.Hyperlink objects that an Atspi.Hypertext implementor has. """ if not AccessibleObject.is_hypertext(acc_object): LOGGING.debug("Object is not valid Accessible.") return False if AccessibleObject.is_newton(acc_object): return acc_object.get_n_links() return Atspi.Hypertext.get_n_links(acc_object) dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/accessibles/accessible_image.py000066400000000000000000000122431520163156700305360ustar00rootroot00000000000000#!/usr/bin/python3 """ Handling for Accessible Image. """ # pylint: disable=broad-exception-caught # pylint: disable=wrong-import-position # pylint: disable=wrong-import-order # pylint: disable=import-error # pylint: disable=no-name-in-module # ruff: noqa: E402 # ruff: noqa: E501 import os import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi from dogtail.logging import logging_class LOGGING = logging_class.logger from dogtail.accessibles.accessible_object import AccessibleObject SESSION_TYPE = "x11" if "XDG_SESSION_TYPE" in os.environ and "wayland" in os.environ["XDG_SESSION_TYPE"]: SESSION_TYPE = "wayland" COORD_TYPE_SCREEN = 0 COORD_TYPE_WINDOW = 1 COORD_TYPE_PARENT = 2 @logging_class.log_a_class class AccessibleImage: """ Image class. Heavily inspired by Orca's classes. """ @staticmethod def get_image_description(acc_object): """ Gets the description of the image displayed in an Atspi.Image object. """ if not AccessibleObject.is_valid(acc_object): return None if not AccessibleObject.is_image(acc_object): LOGGING.debug("This object does not have Atspi.Image interface.") return None if AccessibleObject.is_newton(acc_object): return acc_object.get_image_description() return Atspi.Image.get_image_description(acc_object) @staticmethod def get_image_extents(acc_object, coordinate_type=None): """ Gets the bounding box of the image displayed in a specified Atspi.Image implementor. The returned values are meaningful only if the Image has both STATE_VISIBLE and STATE_SHOWING. """ if not AccessibleObject.is_valid(acc_object): return None if not AccessibleObject.is_image(acc_object): LOGGING.debug("This object does not have Atspi.Image interface.") return None if coordinate_type not in (0, 1, 2, None): LOGGING.debug(f"Not a valid coordinate type '{coordinate_type}'.") return False # Provide an option for user to set their own coord type. if coordinate_type: valid_coord_type = coordinate_type else: if SESSION_TYPE == "x11": valid_coord_type = coordinate_type if coordinate_type else COORD_TYPE_SCREEN if SESSION_TYPE == "wayland": valid_coord_type = coordinate_type if coordinate_type else COORD_TYPE_WINDOW if AccessibleObject.is_newton(acc_object): return acc_object.get_image_extents(valid_coord_type) return Atspi.Image.get_image_extents(acc_object, valid_coord_type) @staticmethod def get_image_locale(acc_object): """ Gets the locale associated with an image and its textual representation. """ if not AccessibleObject.is_valid(acc_object): return None if not AccessibleObject.is_image(acc_object): LOGGING.debug("This object does not have Atspi.Image interface.") return None if AccessibleObject.is_newton(acc_object): return acc_object.get_image_locale() return Atspi.Image.get_image_locale(acc_object) @staticmethod def get_image_position(acc_object, coordinate_type=None): """ Gets the minimum x and y coordinates of the image displayed in a specified Atspi.Image implementor. The returned values are meaningful only if the Image has both STATE_VISIBLE and STATE_SHOWING. """ if not AccessibleObject.is_valid(acc_object): return None if not AccessibleObject.is_image(acc_object): LOGGING.debug("This object does not have Atspi.Image interface.") return None if coordinate_type not in (0, 1, 2, None): LOGGING.debug(f"Not a valid coordinate type '{coordinate_type}'.") return False # Provide an option for user to set their own coord type. if coordinate_type: valid_coord_type = coordinate_type else: if SESSION_TYPE == "x11": valid_coord_type = coordinate_type if coordinate_type else COORD_TYPE_SCREEN if SESSION_TYPE == "wayland": valid_coord_type = coordinate_type if coordinate_type else COORD_TYPE_WINDOW if AccessibleObject.is_newton(acc_object): return acc_object.get_image_position(valid_coord_type) return Atspi.Image.get_image_position(acc_object, valid_coord_type) @staticmethod def get_image_size(acc_object): """ Gets the size of the image displayed in a specified Atspi.Image object. The returned values are meaningful only if the Image has both STATE_VISIBLE and STATE_SHOWING. """ if not AccessibleObject.is_valid(acc_object): return None if not AccessibleObject.is_image(acc_object): LOGGING.debug("This object does not have Atspi.Image interface.") return None if AccessibleObject.is_newton(acc_object): return acc_object.get_image_size() return Atspi.Image.get_image_size(acc_object) dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/accessibles/accessible_object.py000066400000000000000000000772551520163156700307400ustar00rootroot00000000000000#!/usr/bin/python3 """ Handling for Accessibles, extending with automation specific use cases. """ # pylint: disable=broad-exception-caught # pylint: disable=wrong-import-position # pylint: disable=wrong-import-order # pylint: disable=import-error # pylint: disable=line-too-long # pylint: disable=no-name-in-module # ruff: noqa: E402 # ruff: noqa: E501 import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi from gi.repository import GLib from types import LambdaType import inspect from dogtail.predicate import GenericPredicate from dogtail.config import config from dogtail.utils import do_delay from dogtail.logging import logging_class LOGGING = logging_class.logger # TODO think about logging and how much of it we need to not get flooded. # Most likely will do 2 levels, debug and debug_verbose and check method by method. class SearchError(Exception): """ The widget was not found. """ @logging_class.log_a_class class AccessibleObject: """ Utility class for obtaining information about Accessible Objects. Heavily inspired by Orca's AXObject. """ # Too verbose, do not log. @staticmethod @logging_class.do_not_log def is_newton(acc_object): # pylint: disable=unused-argument """ Returns True if an accessible object is Newton. For now lets just prepare it before having hands on experience. """ return False #return isinstance(acc_object, newton_atspi_compat.Accessible) @staticmethod def is_dead(acc_object): """ Returns True if we know for certain this object is not valid. """ # Either empty or not Atspi.Accessible type. if not AccessibleObject.is_valid(acc_object): return True # Good indicator of dead object, no children and no parent. empty_children_list = AccessibleObject.get_children(acc_object) == [] no_parent = AccessibleObject.get_parent(acc_object) is None if empty_children_list and no_parent: return True return False # Too verbose, do not log. @staticmethod @logging_class.do_not_log def is_valid(acc_object): """ Returns False if we know for certain this object is invalid. """ return acc_object is not None and isinstance(acc_object, Atspi.Accessible) @staticmethod def is_action(acc_object): """ Return True if Atspi object is an Action else False. """ if AccessibleObject.is_newton(acc_object): return acc_object.get_action_iface() is not None return Atspi.Accessible.get_action_iface(acc_object) is not None @staticmethod def is_text(acc_object): """ Return True if Atspi object is an Text else False. """ if AccessibleObject.is_newton(acc_object): return acc_object.get_text_iface() is not None return Atspi.Accessible.get_text_iface(acc_object) is not None @staticmethod def is_editable_text(acc_object): """ Return True if Atspi object is an EditableText else False. """ if AccessibleObject.is_newton(acc_object): return acc_object.get_editable_text_iface() is not None return Atspi.Accessible.get_editable_text_iface(acc_object) is not None @staticmethod def is_hypertext(acc_object): """ Return True if Atspi object is an Hypertext else False. """ if AccessibleObject.is_newton(acc_object): return acc_object.get_hypertext_iface() is not None return Atspi.Accessible.get_hypertext_iface(acc_object) is not None @staticmethod def is_image(acc_object): """ Return True if Atspi object is an Image else False. """ if AccessibleObject.is_newton(acc_object): return acc_object.get_image_iface() is not None return Atspi.Accessible.get_image_iface(acc_object) is not None @staticmethod def is_component(acc_object): """ Return True if Atspi object is a Component else False. """ if AccessibleObject.is_newton(acc_object): return acc_object.get_component_iface() is not None return Atspi.Accessible.get_component_iface(acc_object) is not None @staticmethod def is_selection(acc_object): """ Return True if Atspi object is an Selection else False. """ if AccessibleObject.is_newton(acc_object): return acc_object.get_selection_iface() is not None return Atspi.Accessible.get_selection_iface(acc_object) is not None @staticmethod def is_table(acc_object): """ Return True if Atspi object is an Table else False. """ if AccessibleObject.is_newton(acc_object): return acc_object.get_table_iface() is not None return Atspi.Accessible.get_table_iface(acc_object) is not None @staticmethod def is_value(acc_object): """ Return True if Atspi object is an Value else False. """ if AccessibleObject.is_newton(acc_object): return acc_object.get_value_iface() is not None return Atspi.Accessible.get_value_iface(acc_object) is not None @staticmethod def iter_children(acc_object, pred=None): """ Generator to iterate through object's children. If the function pred is specified, children for which pred is False will be skipped. """ if not AccessibleObject.is_valid(acc_object): return child_count = AccessibleObject.get_child_count(acc_object) for index in range(child_count): child = AccessibleObject.get_child_at_index(acc_object, index) if child is not None and (pred is None or pred(child)): yield child # Too verbose, do not log. @staticmethod @logging_class.do_not_log def get_child_at_index(acc_object, index): """ Returns the nth child of obj. """ if not AccessibleObject.is_valid(acc_object): return None n_children = AccessibleObject.get_child_count(acc_object) if n_children <= 0: return None if index == -1: index = n_children - 1 if not 0 <= index < n_children: return None try: if AccessibleObject.is_newton(acc_object): child = acc_object.get_child_at_index(index) else: child = Atspi.Accessible.get_child_at_index(acc_object, index) except Exception as error: LOGGING.debug(f"Exception when getting child at index: '{error}'") return None if child == acc_object: LOGGING.debug(f"Object '{acc_object}' claims to be its own child.") return None return child # Too verbose, do not log. @staticmethod @logging_class.do_not_log def get_child_count(acc_object): """ Returns number of children in given object. """ if not AccessibleObject.is_valid(acc_object): return 0 try: if AccessibleObject.is_newton(acc_object): count = acc_object.get_child_count() else: count = Atspi.Accessible.get_child_count(acc_object) except Exception as error: LOGGING.debug(f"Exception when getting child count: '{error}'") return 0 return count # Too verbose, do not log. @staticmethod @logging_class.do_not_log def _find_all_descendants(acc_object, predicate, matches): """ Returns all descendants that match a predicate. """ if not AccessibleObject.is_valid(acc_object): return child_count = AccessibleObject.get_child_count(acc_object) for index in range(child_count): child = AccessibleObject.get_child_at_index(acc_object, index) try: if predicate and predicate(child): matches.append(child) except Exception as error: LOGGING.debug(f"Exception occurred in predicate match '{error}'.") AccessibleObject._find_all_descendants(child, predicate, matches) @staticmethod def find_all_descendants(acc_object, given_predicate, **kwargs): """ Returns all descendants that match a predicate. """ # Pull values from kwargs here. validated_recursive = True validated_showing_only = config.search_showing_only # =================================================== allowed_kwargs = {"recursive", "showingOnly", "showing_only"} for key, val in kwargs.items(): if "recursive" == str(key): validated_recursive = val elif str(key) in ("showingOnly", "showing_only"): validated_showing_only = val # Handle possible leftovers and raise TypeError on unexpected value. else: unexpected_arguments = ", ".join(set(kwargs) - allowed_kwargs) function_name = AccessibleObject.find_all_descendants.__name__ raise TypeError( f"{function_name}() got an unexpected keyword argument(s): {unexpected_arguments}" ) # Arguments were handled, do not pass them to another method. for allowed_argument in allowed_kwargs: kwargs.pop(allowed_argument, False) # =================================================== # User is using lambdas. if isinstance(given_predicate, LambdaType): predicate = given_predicate # Showing only handling. if validated_showing_only: def extend_predicate(x): return predicate(x) and x.showing predicate = extend_predicate # User is using a GenericPredicate. elif isinstance(given_predicate, type(GenericPredicate())): # Showing only handling for predicates. given_predicate.showing_only = validated_showing_only predicate = given_predicate.satisfied_by_node # Unknown/unsupported way to compare nodes. else: LOGGING.info("Unknown/unsupported way to compare nodes.") return None matches = [] if not validated_recursive: list(filter(predicate, acc_object.children)) else: AccessibleObject._find_all_descendants(acc_object, predicate, matches) return matches @staticmethod def find_descendant(acc_object, given_predicate, depth_first=True, **kwargs): """ Find a single descendant node satisfying the predicate. """ # Pull values from kwargs here. validated_retry = True validated_recursive = True validated_require_result = True validated_showing_only = config.search_showing_only # =================================================== allowed_kwargs = {"retry", "recursive", "requireResult", "require_result", "showingOnly", "showing_only"} for key, val in kwargs.items(): if "retry" == str(key): validated_retry = val elif "recursive" == str(key): validated_recursive = val elif str(key) in ("requireResult", "require_result"): validated_require_result = val elif str(key) in ("showingOnly", "showing_only"): validated_showing_only = val # Handle possible leftovers and raise TypeError on unexpected value. else: unexpected_arguments = ", ".join(set(kwargs) - allowed_kwargs) function_name = AccessibleObject.find_descendant.__name__ raise TypeError( f"{function_name}() got an unexpected keyword argument(s): {unexpected_arguments}" ) # Arguments were handled, do not pass them to another method. for allowed_argument in allowed_kwargs: kwargs.pop(allowed_argument, False) # =================================================== # User is using lambdas. if isinstance(given_predicate, LambdaType): predicate = given_predicate try: # Text of the source code, which in this case is lambda. source = inspect.getsource(predicate).strip("\n") description_to_use = source.replace(" ", "").replace("\n", " ") except OSError: description_to_use = "child satisfying a custom lambda function" # Showing only handling. if validated_showing_only: def extend_predicate(x): return predicate(x) and x.showing predicate = extend_predicate description_to_use += " and x.showing" # User is using a GenericPredicate. elif isinstance(given_predicate, type(GenericPredicate())): # Showing only handling for predicates. given_predicate.showing_only = validated_showing_only predicate = given_predicate.satisfied_by_node description_to_use = str(given_predicate) # Unknown/unsupported way to compare nodes. else: predicate_type = type(given_predicate) LOGGING.info(f"Unknown/unsupported way '{predicate_type}' to compare nodes.") raise SearchError("No match.") # Debug description of what user is searching for. LOGGING.debug(f"Searching for '{description_to_use}'") for number_of_attempts in range(1, config.search_cut_off_limit + 1): if number_of_attempts >= config.search_warning_threshold or config.debug_searching: LOGGING.info(f"Searching again (attempt '{number_of_attempts}')") try: if not validated_recursive: success_match = next(filter(predicate, acc_object.children), None) elif depth_first: success_match = AccessibleObject._find_descendant_depth_first(acc_object, predicate) else: # breadth_first success_match = AccessibleObject._find_descendant_breadth_first(acc_object, predicate) if success_match: return success_match if not validated_retry: break except GLib.GError as error: if number_of_attempts == config.search_cut_off_limit: LOGGING.info(f"GLib Error caught from a11y tree: '{error}'") raise RuntimeError(" ".join(( "Error: Session has probably broken a11y!", "Exiting allowing session re-runs", ))) from error do_delay(config.search_back_off_delay) except TypeError as error: LOGGING.info(f"Unexpected TypeError from a11y tree search: '{error}'") do_delay(config.search_back_off_delay) do_delay(config.search_back_off_delay) if validated_require_result: raise SearchError(f"No match for '{description_to_use}'") # Too verbose, do not log. @staticmethod @logging_class.do_not_log def _find_descendant_depth_first(acc_object, pred): """ Find a single descendant node satisfying the predicate, searching depth first. """ try: if pred(acc_object): return acc_object except Exception: pass for child in acc_object: try: success_match = AccessibleObject._find_descendant_depth_first(child, pred) except Exception: success_match = None if success_match is not None: return success_match return None # Too verbose, do not log. @staticmethod @logging_class.do_not_log def _find_descendant_breadth_first(acc_object, pred): """ Find a single descendant node satisfying the predicate, searching breadth first. """ for child in acc_object: try: if pred(acc_object): return acc_object except Exception: pass for child in acc_object: try: success_match = AccessibleObject._find_descendant_breadth_first(child, pred) except Exception: success_match = None if success_match is not None: return success_match return None @staticmethod def find_ancestor(acc_object, given_predicate, **kwargs): """ Find a single ancestor satisfying the predicate. """ if not AccessibleObject.is_valid(acc_object): return None # Pull values from kwargs here. validated_retry = True validated_require_result = True validated_showing_only = config.search_showing_only # =================================================== allowed_kwargs = {"retry", "requireResult", "require_result", "showingOnly", "showing_only"} for key, val in kwargs.items(): if "retry" == str(key): validated_retry = val elif str(key) in ("requireResult", "require_result"): validated_require_result = val elif str(key) in ("showingOnly", "showing_only"): validated_showing_only = val # Handle possible leftovers and raise TypeError on unexpected value. else: unexpected_arguments = ", ".join(set(kwargs) - allowed_kwargs) function_name = AccessibleObject.find_ancestor.__name__ raise TypeError( f"{function_name}() got an unexpected keyword argument(s): {unexpected_arguments}" ) # Arguments were handled, do not pass them to another method. for allowed_argument in allowed_kwargs: kwargs.pop(allowed_argument, False) # =================================================== # User is using lambdas. if isinstance(given_predicate, LambdaType): predicate = given_predicate try: # Text of the source code, which in this case is lambda. source = inspect.getsource(predicate).strip("\n") description_to_use = source.replace(" ", "").replace("\n", " ") except OSError: description_to_use = "parent satisfying a custom lambda function" # Showing only handling. if validated_showing_only: def extend_predicate(x): return predicate(x) and x.showing predicate = extend_predicate description_to_use += " and x.showing" # User is using a GenericPredicate. elif isinstance(given_predicate, type(GenericPredicate())): # Showing only handling for predicates. given_predicate.showing_only = validated_showing_only predicate = given_predicate.satisfied_by_node description_to_use = str(given_predicate) # Unknown/unsupported way to compare nodes. else: predicate_type = type(given_predicate) LOGGING.info(f"Unknown/unsupported way '{predicate_type}' to compare nodes.") raise SearchError("No match.") # Debug description of what user is searching for. LOGGING.debug(f"Searching for '{description_to_use}'") # Keep track of objects we've encountered in order to handle broken trees. for number_of_attempts in range(1, config.search_cut_off_limit + 1): if number_of_attempts >= config.search_warning_threshold or config.debug_searching: LOGGING.info(f"Searching again (attempt '{number_of_attempts}')") try: objects = [acc_object] parent = AccessibleObject.get_parent_checked(acc_object) while parent: if parent in objects: LOGGING.debug(f"Circular tree suspected, {parent} was found already.") return None if predicate(parent): return parent objects.append(parent) parent = AccessibleObject.get_parent_checked(parent) if not validated_retry: break except GLib.GError as error: if number_of_attempts == config.search_cut_off_limit: LOGGING.info(f"GLib Error caught from a11y tree: '{error}'") raise RuntimeError(" ".join(( "Error: Session has probably broken a11y!", "Exiting allowing session re-runs", ))) from error do_delay(config.search_back_off_delay) except TypeError as error: LOGGING.info(f"Unexpected TypeError from a11y tree search: '{error}'") do_delay(config.search_back_off_delay) do_delay(config.search_back_off_delay) if validated_require_result: raise SearchError(f"No match for '{description_to_use}'") return None @staticmethod def get_parent_checked(acc_object): """ Returns the parent of obj, doing checks for tree validity. """ if not AccessibleObject.is_valid(acc_object): return None if AccessibleObject.is_newton(acc_object): role = Atspi.Role(acc_object.get_role()) else: role = Atspi.Accessible.get_role(acc_object) role = acc_object.get_role if role in [Atspi.Role.INVALID, Atspi.Role.APPLICATION]: return None parent = acc_object.parent if parent is None: return None return parent @staticmethod def get_process_id(acc_object): """ Returns the pid associated with the obj. """ if not AccessibleObject.is_valid(acc_object): return -1 try: if AccessibleObject.is_newton(acc_object): process_id = acc_object.get_process_id() else: process_id = Atspi.Accessible.get_process_id(acc_object) except Exception as error: LOGGING.debug(f"Exception when getting process id: '{error}'") return -1 return process_id # Too verbose, do not log. @staticmethod @logging_class.do_not_log def get_id(acc_object): """ Returns a 'id' for Accessible object. """ if not AccessibleObject.is_valid(acc_object): return None if AccessibleObject.is_newton(acc_object): return acc_object.get_id() try: return Atspi.Accessible.get_id(acc_object) except Exception as error: raise RuntimeError("Get Id function failed.") from error @staticmethod def get_accessible_id(acc_object): """ Returns an 'accessible_id' for Accessible object. """ if not AccessibleObject.is_valid(acc_object): return None if AccessibleObject.is_newton(acc_object): return acc_object.get_accessible_id() try: return Atspi.Accessible.get_accessible_id(acc_object) except Exception as error: raise RuntimeError("Get Accessible Id function failed.") from error # Too verbose, do not log. @staticmethod @logging_class.do_not_log def get_name(acc_object): """ Returns a 'name' for Accessible object. """ if not AccessibleObject.is_valid(acc_object): return None if AccessibleObject.is_newton(acc_object): return acc_object.get_name() try: return Atspi.Accessible.get_name(acc_object) except Exception as error: raise RuntimeError("Get Name function failed.") from error # Too verbose, do not log. @staticmethod @logging_class.do_not_log def get_role(acc_object): """ Returns a 'role' for Accessible object. """ if not AccessibleObject.is_valid(acc_object): return None if AccessibleObject.is_newton(acc_object): return acc_object.get_role() return Atspi.Accessible.get_role(acc_object) # Too verbose, do not log. @staticmethod @logging_class.do_not_log def get_role_name(acc_object): """ Returns a 'role_name' for Accessible object. """ if not AccessibleObject.is_valid(acc_object): return None if AccessibleObject.is_newton(acc_object): return acc_object.get_role_name() return Atspi.Accessible.get_role_name(acc_object) # Too verbose, do not log. @staticmethod @logging_class.do_not_log def get_description(acc_object): """ Returns a 'description' for Accessible object. """ if not AccessibleObject.is_valid(acc_object): return None if AccessibleObject.is_newton(acc_object): return acc_object.get_description() return Atspi.Accessible.get_description(acc_object) # Too verbose, do not log. @staticmethod @logging_class.do_not_log def get_parent(acc_object): """ Returns a 'parent' for Accessible object. """ if not AccessibleObject.is_valid(acc_object): return None if AccessibleObject.is_newton(acc_object): return acc_object.get_parent() return Atspi.Accessible.get_parent(acc_object) @staticmethod def get_index_in_parent(acc_object): """ Returns a 'index_in_parent' for Accessible object. """ if not AccessibleObject.is_valid(acc_object): return None # Default value if object has no containing parent or exception happened. final_index = -1 if AccessibleObject.is_newton(acc_object): final_index = acc_object.get_index_in_parent() else: final_index = Atspi.Accessible.get_index_in_parent(acc_object) # Not working as expected sometimes. Working around it. if final_index == -1: LOGGING.debug("Error in get_index_in_parent. Attempting to work around.") for index, child in enumerate(acc_object.parent.children): if child.parent.get_child_at_index(index) == acc_object: LOGGING.debug(f"Success, index was: '{index}'") return index return final_index @staticmethod def get_attributes(acc_object): """ Returns 'attributes' for Accessible object. """ if not AccessibleObject.is_valid(acc_object): return None if AccessibleObject.is_newton(acc_object): attributes = acc_object.get_attributes() else: attributes = Atspi.Accessible.get_attributes(acc_object) return attributes @staticmethod def get_attributes_as_array(acc_object): """ Returns 'attributes_as_array' for Accessible object. """ if not AccessibleObject.is_valid(acc_object): return None if AccessibleObject.is_newton(acc_object): attributes_as_array = acc_object.get_attributes_as_array() else: attributes_as_array = Atspi.Accessible.get_attributes_as_array(acc_object) return attributes_as_array @staticmethod def get_children(acc_object): """ Returns a 'parent' for Accessible object. """ if not AccessibleObject.is_valid(acc_object): return None if AccessibleObject.is_newton(acc_object): # TODO Newton return [] else: children_list = [] for index in range(AccessibleObject.get_child_count(acc_object)): child = AccessibleObject.get_child_at_index(acc_object, index) if child: children_list.append(child) return children_list @staticmethod def is_last_child(acc_object): """ Returns a 'parent' for Accessible object. """ if not AccessibleObject.is_valid(acc_object): return None if AccessibleObject.is_newton(acc_object): return False # TODO else: does_not_have_parent = not AccessibleObject.get_parent(acc_object) index_in_parent_is_not_valid = AccessibleObject.get_index_in_parent(acc_object) is None index_in_parent = AccessibleObject.get_index_in_parent(acc_object) child_count = AccessibleObject.get_parent(acc_object).get_child_count(acc_object) - 1 is_last_index = index_in_parent == child_count return does_not_have_parent or index_in_parent_is_not_valid or is_last_index # Too verbose, do not log. @staticmethod @logging_class.do_not_log def get_labeler(acc_object): """ 'labeller' (read-only list of Node instances): The node(s) that is/are a label for this node. Generated from 'relations'. Return possibilities: None - if no labeler is found. Atspi.Accessible - if a single labeler is found. [Atspi.Accessible, Atspi.Accessible, ...] - if multiple labelers are found. """ if not AccessibleObject.is_valid(acc_object): return None targets = set() if AccessibleObject.is_newton(acc_object): relation_set = acc_object.get_relation_set() else: relation_set = Atspi.Accessible.get_relation_set(acc_object) for relation in relation_set: if AccessibleObject.is_newton(acc_object): relation_type = None # TODO, learn how Newton defines it. else: relation_type = Atspi.RelationType.LABELLED_BY if relation.get_relation_type() == relation_type: if relation.get_n_targets() == 1: return relation.get_target(0) # Using set to avoid duplicates. for i in range(relation.get_n_targets()): targets.add(relation.get_target(i)) return list(targets) if targets else None # Convert set to list. return list(targets) if targets else None @staticmethod def get_labelee(acc_object): """ 'labelee' (read-only list of Node instances): The node(s) that this node is a label for. Generated from 'relations'. Return possibilities: None - if no labelee is found. Atspi.Accessible - if a single labelee is found. [Atspi.Accessible, Atspi.Accessible, ...] - if multiple labelees are found. """ if not AccessibleObject.is_valid(acc_object): return None targets = set() if AccessibleObject.is_newton(acc_object): relation_set = acc_object.get_relation_set() else: relation_set = Atspi.Accessible.get_relation_set(acc_object) for relation in relation_set: if AccessibleObject.is_newton(acc_object): relation_type = None # TODO, learn how Newton defines it. else: relation_type = Atspi.RelationType.LABEL_FOR if relation.get_relation_type() == relation_type: if relation.get_n_targets() == 1: return relation.get_target(0) # Using set to avoid duplicates. for i in range(relation.get_n_targets()): targets.add(relation.get_target(i)) return list(targets) if targets else None # Convert set to list. return list(targets) if targets else None dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/accessibles/accessible_root.py000066400000000000000000000041001520163156700304300ustar00rootroot00000000000000#!/usr/bin/python3 """ Handling for Accessibles, extending with automation specific use cases, in this case handling root object. """ # pylint: disable=import-error # pylint: disable=wrong-import-position # pylint: disable=wrong-import-order # pylint: disable=no-name-in-module # ruff: noqa: E402 from dogtail.logging import logging_class LOGGING = logging_class.logger from dogtail.accessibles.accessible_utilities import AccessibleUtilities @logging_class.log_a_class class AccessibleRoot: """ Root class. Heavily inspired by Orca's classes. """ @staticmethod def applications(): """ Get all applications. """ return AccessibleUtilities.get_all_applications() @staticmethod def application(application_name, **kwargs): """ Gets an application by name, returning an Application instance or raising an exception. """ validated_retry = True validated_application_name = application_name # === Backward compatibility for 'appName' usage === allowed_kwargs = {"appName", "retry"} for key, val in kwargs.items(): if "appName" in str(key): validated_application_name = val elif "retry" in str(key): validated_retry = val # Handle possible leftovers and raise TypeError on unexpected value. else: unexpected_arguments = ", ".join(set(kwargs) - allowed_kwargs) function_name = AccessibleRoot.application.__name__ raise TypeError( f"{function_name}() got an unexpected keyword argument(s): {unexpected_arguments}" ) # Arguments were handled, do not pass them to another method. for allowed_argument in allowed_kwargs: kwargs.pop(allowed_argument, False) # =================================================== return AccessibleUtilities.get_application_named( application_name=validated_application_name, retry=validated_retry ) dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/accessibles/accessible_selection.py000066400000000000000000000150361520163156700314440ustar00rootroot00000000000000#!/usr/bin/python3 """ Handling for Accessible Selection. """ # pylint: disable=broad-exception-caught # pylint: disable=wrong-import-position # pylint: disable=import-error # pylint: disable=no-name-in-module # ruff: noqa: E402 import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi from dogtail.logging import logging_class LOGGING = logging_class.logger from dogtail.accessibles.accessible_object import AccessibleObject @logging_class.log_a_class class AccessibleSelection: """ Selection class. Heavily inspired by Orca's classes. """ @staticmethod def clear_selection(acc_object): """ Clears the current selection, removing all selected children from the specified Atspi.Selection implementor's selection list. """ if not AccessibleObject.is_selection(acc_object): LOGGING.debug("This object does not have Atspi.Selection interface.") if AccessibleObject.is_newton(acc_object): return acc_object.clear_selection() return Atspi.Selection.clear_selection(acc_object) @staticmethod def deselect_child(acc_object, child_index): """ Deselects a specific child of an Atspi.Selection. Note that child_index is the index of the child in the parent container. """ if not AccessibleObject.is_selection(acc_object): LOGGING.debug("This object does not have Atspi.Selection interface.") if AccessibleObject.is_newton(acc_object): return acc_object.deselect_child(child_index) return Atspi.Selection.deselect_child(acc_object, child_index) @staticmethod def deselect_selected_child(acc_object, selected_child_index): """ Removes a child from the selected children list of an Atspi.Selection. Note that selected_child_index is the index in the selected-children list, not the index in the parent container. selected_child_index in this method, and child_index in Atspi.Selection.select_child are asymmetric. """ if not AccessibleObject.is_selection(acc_object): LOGGING.debug("This object does not have Atspi.Selection interface.") if AccessibleObject.is_newton(acc_object): return acc_object.deselect_selected_child(selected_child_index) return Atspi.Selection.deselect_selected_child(acc_object, selected_child_index) @staticmethod def get_n_selected_children(acc_object): """ Gets the number of children of an Atspi.Selection implementor which are currently selected. """ if not AccessibleObject.is_selection(acc_object): LOGGING.debug("This object does not have Atspi.Selection interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_n_selected_children() return Atspi.Selection.get_n_selected_children(acc_object) @staticmethod def get_selected_child(acc_object, selected_child_index): """ Gets the i-th selected Atspi.Accessible child of an Atspi.Selection. Note that selected_child_index refers to the index in the list of 'selected' children and generally differs from that used in Atspi.Accessible.get_child_at_index or returned by Atspi.Accessible.get_index_in_parent. selected_child_index must lie between 0 and Atspi.Selection.get_n_selected_children - 1, inclusive. """ if not AccessibleObject.is_selection(acc_object): LOGGING.debug("This object does not have Atspi.Selection interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_selected_child(selected_child_index) return Atspi.Selection.get_selected_child(acc_object, selected_child_index) @staticmethod def is_child_selected(acc_object, child_index): """ Determines whether a particular child of an Atspi.Selection implementor is currently selected. Note that child_index is the index into the standard Atspi.Accessible container's list of children. """ if not AccessibleObject.is_selection(acc_object): LOGGING.debug("This object does not have Atspi.Selection interface.") if AccessibleObject.is_newton(acc_object): return acc_object.is_child_selected(child_index) return Atspi.Selection.is_child_selected(acc_object, child_index) @staticmethod def select_all(acc_object): """ Attempts to select all of the children of an Atspi.Selection implementor. Not all Atspi.Selection implementors support this operation. """ if not AccessibleObject.is_selection(acc_object): LOGGING.debug("This object does not have Atspi.Selection interface.") if AccessibleObject.is_newton(acc_object): return acc_object.select_all() return Atspi.Selection.select_all(acc_object) @staticmethod def select_child(acc_object, child_index): """ Adds a child to the selected children list of an Atspi.Selection. For Atspi.Selection implementors that only allow single selections, this may replace the (single) current selection. """ if not AccessibleObject.is_selection(acc_object): LOGGING.debug("This object does not have Atspi.Selection interface.") if AccessibleObject.is_newton(acc_object): return acc_object.select_child(child_index) return Atspi.Selection.select_child(acc_object, child_index) @staticmethod def select(acc_object): """ Selects a child. """ if not AccessibleObject.is_selection(acc_object): LOGGING.debug("This object does not have Atspi.Selection interface.") parent = AccessibleObject.get_parent_checked(acc_object) if AccessibleObject.is_newton(acc_object): return acc_object.select_child(parent, acc_object.get_index_in_parent()) return Atspi.Selection.select_child(parent, acc_object.get_index_in_parent()) @staticmethod def deselect(acc_object): """ Deselects a child. """ if not AccessibleObject.is_selection(acc_object): LOGGING.debug("This object does not have Atspi.Selection interface.") parent = AccessibleObject.get_parent_checked(acc_object) if AccessibleObject.is_newton(acc_object): return acc_object.deselect_child(parent, acc_object.get_index_in_parent()) return Atspi.Selection.deselect_child(parent, acc_object.get_index_in_parent()) dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/accessibles/accessible_state.py000066400000000000000000000256141520163156700306020ustar00rootroot00000000000000#!/usr/bin/python3 """ Handling for Accessible State. """ # pylint: disable=broad-exception-caught # pylint: disable=wrong-import-position # pylint: disable=wrong-import-order # pylint: disable=import-error # pylint: disable=no-name-in-module # ruff: noqa: E402 # ruff: noqa: E501 import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi from dogtail.logging import logging_class LOGGING = logging_class.logger from dogtail.accessibles.accessible_object import AccessibleObject @logging_class.log_a_class class AccessibleState: """ State class. Heavily inspired by Orca's classes. """ @staticmethod def get_active(acc_object): """ Get active state. """ if not AccessibleObject.is_valid(acc_object): LOGGING.debug("Object is not valid Accessible.") return False if AccessibleObject.is_newton(acc_object): # return acc_object.get_state_set(acc_object).contains(Newton.StateType.ACTIVE) return False # TODO, figure out how Newton defines it. return Atspi.Accessible.get_state_set(acc_object).contains(Atspi.StateType.ACTIVE) @staticmethod def get_focusable(acc_object): """ Get focusable state. """ if not AccessibleObject.is_valid(acc_object): LOGGING.debug("Object is not valid Accessible.") return False if AccessibleObject.is_newton(acc_object): # return acc_object.get_state_set(acc_object).contains(Newton.StateType.FOCUSABLE) return False # TODO, figure out how Newton defines it. return Atspi.Accessible.get_state_set(acc_object).contains(Atspi.StateType.FOCUSABLE) @staticmethod def get_focused(acc_object): """ Get focused state. """ if not AccessibleObject.is_valid(acc_object): LOGGING.debug("Object is not valid Accessible.") return False if AccessibleObject.is_newton(acc_object): # return acc_object.get_state_set(acc_object).contains(Newton.StateType.FOCUSED) return False # TODO, figure out how Newton defines it. return Atspi.Accessible.get_state_set(acc_object).contains(Atspi.StateType.FOCUSED) @staticmethod def get_pressed(acc_object): """ Get pressed state. """ if not AccessibleObject.is_valid(acc_object): LOGGING.debug("Object is not valid Accessible.") return False if AccessibleObject.is_newton(acc_object): # return acc_object.get_state_set(acc_object).contains(Newton.StateType.PRESSED) return False # TODO, figure out how Newton defines it. return Atspi.Accessible.get_state_set(acc_object).contains(Atspi.StateType.PRESSED) @staticmethod def get_resizable(acc_object): """ Get resizable state. """ if not AccessibleObject.is_valid(acc_object): LOGGING.debug("Object is not valid Accessible.") return False if AccessibleObject.is_newton(acc_object): # return acc_object.get_state_set(acc_object).contains(Newton.StateType.RESIZABLE) return False # TODO, figure out how Newton defines it. return Atspi.Accessible.get_state_set(acc_object).contains(Atspi.StateType.RESIZABLE) @staticmethod def get_selected(acc_object): """ Get selected state. """ if not AccessibleObject.is_valid(acc_object): LOGGING.debug("Object is not valid Accessible.") return False if AccessibleObject.is_newton(acc_object): # return acc_object.get_state_set(acc_object).contains(Newton.StateType.SELECTED) return False # TODO, figure out how Newton defines it. return Atspi.Accessible.get_state_set(acc_object).contains(Atspi.StateType.SELECTED) @staticmethod def get_selectable(acc_object): """ Get selectable state. """ if not AccessibleObject.is_valid(acc_object): LOGGING.debug("Object is not valid Accessible.") return False if AccessibleObject.is_newton(acc_object): # return acc_object.get_state_set(acc_object).contains(Newton.StateType.SELECTABLE) return False # TODO, figure out how Newton defines it. return Atspi.Accessible.get_state_set(acc_object).contains(Atspi.StateType.SELECTABLE) @staticmethod def get_sensitive(acc_object): """ Get sensitive state. """ if not AccessibleObject.is_valid(acc_object): LOGGING.debug("Object is not valid Accessible.") return False if AccessibleObject.is_newton(acc_object): # return acc_object.get_state_set(acc_object).contains(Newton.StateType.SENSITIVE) return False # TODO, figure out how Newton defines it. return Atspi.Accessible.get_state_set(acc_object).contains(Atspi.StateType.SENSITIVE) @staticmethod def get_showing(acc_object): """ Get showing state. """ if not AccessibleObject.is_valid(acc_object): LOGGING.debug("Object is not valid Accessible.") return False if AccessibleObject.is_newton(acc_object): # return acc_object.get_state_set(acc_object).contains(Newton.StateType.SHOWING) return False # TODO, figure out how Newton defines it. return Atspi.Accessible.get_state_set(acc_object).contains(Atspi.StateType.SHOWING) @staticmethod def get_visible(acc_object): """ Get visible state. """ if not AccessibleObject.is_valid(acc_object): LOGGING.debug("Object is not valid Accessible.") return False if AccessibleObject.is_newton(acc_object): # return acc_object.get_state_set(acc_object).contains(Newton.StateType.VISIBLE) return False # TODO, figure out how Newton defines it. return Atspi.Accessible.get_state_set(acc_object).contains(Atspi.StateType.VISIBLE) @staticmethod def get_checked(acc_object): """ Get checked state. """ if not AccessibleObject.is_valid(acc_object): LOGGING.debug("Object is not valid Accessible.") return False if AccessibleObject.is_newton(acc_object): # return acc_object.get_state_set(acc_object).contains(Newton.StateType.CHECKED) return False # TODO, figure out how Newton defines it. return Atspi.Accessible.get_state_set(acc_object).contains(Atspi.StateType.CHECKED) @staticmethod def get_checkable(acc_object): """ Get checkable state. """ if not AccessibleObject.is_valid(acc_object): LOGGING.debug("Object is not valid Accessible.") return False if AccessibleObject.is_newton(acc_object): # return acc_object.get_state_set(acc_object).contains(Newton.StateType.CHECKABLE) return False # TODO, figure out how Newton defines it. return Atspi.Accessible.get_state_set(acc_object).contains(Atspi.StateType.CHECKABLE) @staticmethod def get_read_only(acc_object): """ Get read_only state. """ if not AccessibleObject.is_valid(acc_object): LOGGING.debug("Object is not valid Accessible.") return False if AccessibleObject.is_newton(acc_object): # return acc_object.get_state_set(acc_object).contains(Newton.StateType.READ_ONLY) return False # TODO, figure out how Newton defines it. return Atspi.Accessible.get_state_set(acc_object).contains(Atspi.StateType.READ_ONLY) @staticmethod def get_collapsed(acc_object): """ Get collapsed state. """ if not AccessibleObject.is_valid(acc_object): LOGGING.debug("Object is not valid Accessible.") return False if AccessibleObject.is_newton(acc_object): # return acc_object.get_state_set(acc_object).contains(Newton.StateType.COLLAPSED) return False # TODO, figure out how Newton defines it. return Atspi.Accessible.get_state_set(acc_object).contains(Atspi.StateType.COLLAPSED) @staticmethod def get_editable(acc_object): """ Get editable state. """ if not AccessibleObject.is_valid(acc_object): LOGGING.debug("Object is not valid Accessible.") return False if AccessibleObject.is_newton(acc_object): # return acc_object.get_state_set(acc_object).contains(Newton.StateType.EDITABLE) return False # TODO, figure out how Newton defines it. return Atspi.Accessible.get_state_set(acc_object).contains(Atspi.StateType.EDITABLE) @staticmethod def get_armed(acc_object): """ Get armed state. """ if not AccessibleObject.is_valid(acc_object): LOGGING.debug("Object is not valid Accessible.") return False if AccessibleObject.is_newton(acc_object): # return acc_object.get_state_set(acc_object).contains(Newton.StateType.ARMED) return False # TODO, figure out how Newton defines it. return Atspi.Accessible.get_state_set(acc_object).contains(Atspi.StateType.ARMED) @staticmethod def get_invalid(acc_object): """ Get invalid state. """ if not AccessibleObject.is_valid(acc_object): LOGGING.debug("Object is not valid Accessible.") return False if AccessibleObject.is_newton(acc_object): # return acc_object.get_state_set(acc_object).contains(Newton.StateType.INVALID) return False # TODO, figure out how Newton defines it. return Atspi.Accessible.get_state_set(acc_object).contains(Atspi.StateType.INVALID) @staticmethod def get_accessible_states(acc_object): """ Get states as Accessible objects. """ if not AccessibleObject.is_valid(acc_object): LOGGING.debug("Object is not valid Accessible.") return False if AccessibleObject.is_newton(acc_object): # return acc_object.get_state_set(acc_object).contains(Newton.StateType.EDITABLE) return False # TODO, figure out how Newton defines it. return Atspi.StateSet.get_states(Atspi.Accessible.get_state_set(acc_object)) @staticmethod def get_string_states(acc_object): """ Get states as strings. """ if not AccessibleObject.is_valid(acc_object): LOGGING.debug("Object is not valid Accessible.") return False if AccessibleObject.is_newton(acc_object): # return acc_object.get_state_set(acc_object).contains(Newton.StateType.EDITABLE) return False # TODO, figure out how Newton defines it. return "TODO" # TODO, translate state types on object to strings. dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/accessibles/accessible_table.py000066400000000000000000000362361520163156700305530ustar00rootroot00000000000000#!/usr/bin/python3 """ Handling for Accessible Table. """ # pylint: disable=broad-exception-caught # pylint: disable=wrong-import-position # pylint: disable=import-error # pylint: disable=no-name-in-module # ruff: noqa: E402 # ruff: noqa: E501 import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi from dogtail.logging import logging_class LOGGING = logging_class.logger from dogtail.accessibles.accessible_object import AccessibleObject @logging_class.log_a_class class AccessibleTable: """ Table class. Heavily inspired by Orca's classes. """ @staticmethod def add_column_selection(acc_object, column): """ Selects the specified column, adding it to the current column selection. Not all tables support column selection. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.add_column_selection(column) return Atspi.Table.add_column_selection(acc_object, column) @staticmethod def add_row_selection(acc_object, row): """ Selects the specified row, adding it to the current row selection. Not all tables support row selection. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.add_row_selection(row) return Atspi.Table.add_row_selection(acc_object, row) @staticmethod def get_accessible_at(acc_object, row, column): """ Gets the table cell at the specified row and column indices. To get the accessible object at a particular (x, y) screen coordinate, use Atspi.Component.get_accessible_at_point. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_accessible_at(row, column) return Atspi.Table.get_accessible_at(acc_object, row, column) @staticmethod def get_caption(acc_object): """ Gets an accessible representation of the caption for an Atspi.Table. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_caption() return Atspi.Table.get_caption(acc_object) @staticmethod def get_column_at_index(acc_object, index): """ Gets the table column index occupied by the child at a particular 1-D child index. See Atspi.Table.get_index_at, Atspi.Table.get_row_at_index. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_column_at_index(index) return Atspi.Table.get_column_at_index(acc_object, index) @staticmethod def get_column_description(acc_object, column): """ Gets a text description of a particular table column. This differs from Atspi.Table.get_column_header, which returns an #Accessible. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_column_description(column) return Atspi.Table.get_column_description(acc_object, column) @staticmethod def get_column_extent_at(acc_object, row, column): """ Gets the number of columns spanned by the table cell at the specific row and column (some tables can have cells which span multiple rows and/or columns). The returned values are meaningful only if the Table has both STATE_VISIBLE and STATE_SHOWING. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_column_extent_at(row, column) return Atspi.Table.get_column_extent_at(acc_object, row, column) @staticmethod def get_column_header(acc_object, column): """ Gets the header associated with a table column, if available. This differs from Atspi.Table.get_column_description, which returns a string. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_column_header(column) return Atspi.Table.get_column_header(acc_object, column) @staticmethod def get_index_at(acc_object, row, column): """ Gets the 1-D child index corresponding to the specified 2-D row and column indices. To get the accessible object at a particular (x, y) screen coordinate, use Atspi.Component.get_accessible_at_point. See Atspi.Table.get_row_at_index, Atspi.Table.get_column_at_index """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_index_at(row, column) return Atspi.Table.get_index_at(acc_object, row, column) @staticmethod def get_n_columns(acc_object): """ Gets the number of columns in an Atspi.Table, exclusive of any columns that are programmatically hidden, but inclusive of columns that may be outside of the current scrolling window or viewport. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_n_columns() return Atspi.Table.get_n_columns(acc_object) @staticmethod def get_n_rows(acc_object): """ Gets the number of rows in an Atspi.Table, exclusive of any rows that are programmatically hidden, but inclusive of rows that may be outside of the current scrolling window or viewport. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_n_rows() return Atspi.Table.get_n_rows(acc_object) @staticmethod def get_n_selected_columns(acc_object): """ Queries a table to find out how many columns are currently selected. Not all tables support column selection. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_n_selected_columns() return Atspi.Table.get_n_selected_columns(acc_object) @staticmethod def get_n_selected_rows(acc_object): """ Query a table to find out how many rows are currently selected. Not all tables support row selection. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_n_selected_rows() return Atspi.Table.get_n_selected_rows(acc_object) @staticmethod def get_row_at_index(acc_object, index): """ Gets the table row index occupied by the child at a particular 1-D child index. See Atspi.Table.get_index_at, Atspi.Table.get_column_at_index. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_row_at_index(index) return Atspi.Table.get_row_at_index(acc_object, index) @staticmethod def get_row_column_extents_at_index(acc_object, index): """ Given a child index, determines the row and column indices and extents, and whether the cell is currently selected. If the child at index is not a cell (for instance, if it is a summary, caption, etc.), False is returned. The returned values are meaningful only if the Table has both STATE_VISIBLE and STATE_SHOWING. Return type: (bool, row: int, col: int, row_extents: int, col_extents: int, is_selected: bool) Example: If the Atspi.Table child at index '6' extends across columns 5 and 6 of row 2 of an Atspi.Table instance, and is currently selected, then retval = Atspi.Table.get_row_column_extents_at_index (table, 6, row, col, row_extents, col_extents, is_selected); will return True, and after the call row, col, row_extents, col_extents, and is_selected will contain 2, 5, 1, 2, and True, respectively. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_row_column_extents_at_index(index) return Atspi.Table.get_row_column_extents_at_index(acc_object, index) @staticmethod def get_row_description(acc_object, row): """ Gets a text description of a particular table row. This differs from Atspi.Table.get_row_header, which returns an Atspi.Accessible. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_row_description(row) return Atspi.Table.get_row_description(acc_object, row) @staticmethod def get_row_extent_at(acc_object, row, column): """ Gets the number of rows spanned by the table cell at the specific row and column. (some tables can have cells which span multiple rows and/or columns). The returned values are meaningful only if the Table has both STATE_VISIBLE and STATE_SHOWING. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_row_extent_at(row, column) return Atspi.Table.get_row_extent_at(acc_object, row, column) @staticmethod def get_row_header(acc_object, row): """ Gets the header associated with a table row, if available. This differs from Atspi.Table.get_row_description, which returns a string. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_row_header(row) return Atspi.Table.get_row_header(acc_object, row) @staticmethod def get_selected_columns(acc_object): """ Queries a table for a list of indices of columns which are currently selected. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_selected_columns() return Atspi.Table.get_selected_columns(acc_object) @staticmethod def get_selected_rows(acc_object): """ Queries a table for a list of indices of rows which are currently selected. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_selected_rows() return Atspi.Table.get_selected_rows(acc_object) @staticmethod def get_summary(acc_object): """ Gets an accessible object which summarizes the contents of an Atspi.Table. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_summary() return Atspi.Table.get_summary(acc_object) @staticmethod def is_column_selected(acc_object, column): """ Determines whether specified table column is selected. Not all tables support column selection. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.is_column_selected(column) return Atspi.Table.is_column_selected(acc_object, column) @staticmethod def is_row_selected(acc_object, row): """ Determines whether a table row is selected. Not all tables support row selection. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.is_row_selected(row) return Atspi.Table.is_row_selected(acc_object, row) @staticmethod def is_selected(acc_object, row, column): """ Determines whether the cell at a specific row and column is selected. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.is_selected(row, column) return Atspi.Table.is_selected(acc_object, row, column) @staticmethod def remove_column_selection(acc_object, column): """ De-selects the specified column, removing it from the current column selection. Not all tables support column selection. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.remove_column_selection(column) return Atspi.Table.remove_column_selection(acc_object, column) @staticmethod def remove_row_selection(acc_object, row): """ De-selects the specified row, removing it from the current row selection. Not all tables support row selection. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.remove_row_selection(row) return Atspi.Table.remove_row_selection(acc_object, row) dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/accessibles/accessible_table_cell.py000066400000000000000000000105551520163156700315460ustar00rootroot00000000000000#!/usr/bin/python3 """ Handling for Accessible TableCell. """ # pylint: disable=broad-exception-caught # pylint: disable=wrong-import-position # pylint: disable=import-error # pylint: disable=no-name-in-module # ruff: noqa: E402 # ruff: noqa: E501 import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi from dogtail.logging import logging_class LOGGING = logging_class.logger from dogtail.accessibles.accessible_object import AccessibleObject @logging_class.log_a_class class AccessibleTableCell: """ TableCell class. Heavily inspired by Orca's classes. """ @staticmethod def get_column_header_cells(acc_object): """ Returns the column headers as an array of cell accessibles. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_column_header_cells() return Atspi.TableCell.get_column_header_cells(acc_object) @staticmethod def get_column_index(acc_object): """ Returns the column index. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_column_index() return Atspi.TableCell.get_column_index(acc_object) @staticmethod def get_column_span(acc_object): """ Returns the number of columns occupied by this cell accessible. The returned values are meaningful only if the table cell has both STATE_VISIBLE and STATE_SHOWING. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_column_span() return Atspi.TableCell.get_column_span(acc_object) @staticmethod def get_position(acc_object): """ Retrieves the tabular position of this cell. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_position() return Atspi.TableCell.get_position(acc_object) @staticmethod def get_row_column_span(acc_object): """ Gets the row and column indexes and extents of this cell accessible. The returned values are meaningful only if the table cell has both STATE_VISIBLE and STATE_SHOWING. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_row_column_span() return Atspi.TableCell.get_row_column_span(acc_object) @staticmethod def get_row_header_cells(acc_object): """ Returns the row headers as an array of cell accessibles. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_row_header_cells() return Atspi.TableCell.get_row_header_cells(acc_object) @staticmethod def get_row_span(acc_object): """ Returns the number of rows occupied by this cell accessible. The returned values are meaningful only if the table cell has both STATE_VISIBLE and STATE_SHOWING. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_row_span() return Atspi.TableCell.get_row_span(acc_object) @staticmethod def get_table(acc_object): """ Returns a reference to the accessible of the containing table. """ if not AccessibleObject.is_table(acc_object): LOGGING.debug("This object does not have Atspi.Table interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_table() return Atspi.TableCell.get_table(acc_object) dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/accessibles/accessible_text.py000066400000000000000000000410141520163156700304360ustar00rootroot00000000000000#!/usr/bin/python3 """ Handling for Accessible Text. """ # pylint: disable=broad-exception-caught # pylint: disable=wrong-import-position # pylint: disable=wrong-import-order # pylint: disable=import-error # pylint: disable=no-name-in-module # ruff: noqa: E402 # ruff: noqa: E501 import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi from dogtail.logging import logging_class LOGGING = logging_class.logger from dogtail.accessibles.accessible_object import AccessibleObject @logging_class.log_a_class class AccessibleText: """ Text class. Heavily inspired by Orca's classes. """ @staticmethod def add_selection(acc_object, start_offset, end_offset): """ Selects some text (adds a text selection) in an Atspi.Text object. """ if not AccessibleObject.is_text(acc_object): LOGGING.debug("This object does not have Atspi.Text interface.") if AccessibleObject.is_newton(acc_object): return acc_object.add_selection(start_offset, end_offset) return Atspi.Text.add_selection(acc_object, start_offset, end_offset) @staticmethod def get_attribute_run(acc_object, offset, include_defaults): """ Gets a set of attributes applied to a range of text from an Atspi.Text object, optionally including its 'default' attributes. """ if not AccessibleObject.is_text(acc_object): LOGGING.debug("This object does not have Atspi.Text interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_attribute_run(offset, include_defaults) return Atspi.Text.get_attribute_run(acc_object, offset, include_defaults) @staticmethod def get_bounded_ranges(acc_object, x, y, width, height, coord_type, clip_type_x, clip_type_y): """ Gets the ranges of text from an Atspi.Text object which lie within the bounds defined by (x, y) and (x+'width', y+'height'). """ if not AccessibleObject.is_text(acc_object): LOGGING.debug("This object does not have Atspi.Text interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_bounded_ranges( x, y, width, height, coord_type, clip_type_x, clip_type_y ) return Atspi.Text.get_bounded_ranges( acc_object, x, y, width, height, coord_type, clip_type_x, clip_type_y ) @staticmethod def get_caret_offset(acc_object): """ Gets the current offset of the text caret in an Atspi.Text object. """ if not AccessibleObject.is_text(acc_object): LOGGING.debug("This object does not have Atspi.Text interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_caret_offset() return Atspi.Text.get_caret_offset(acc_object) @staticmethod def get_character_at_offset(acc_object, offset): """ Gets the character at a given offset for an Atspi.Text object. """ if not AccessibleObject.is_text(acc_object): LOGGING.debug("This object does not have Atspi.Text interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_character_at_offset(offset) return Atspi.Text.get_character_at_offset(acc_object, offset) @staticmethod def get_character_count(acc_object): """ Gets the character count of an #AccessibleText object. """ if not AccessibleObject.is_text(acc_object): LOGGING.debug("This object does not have Atspi.Text interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_character_count() return Atspi.Text.get_character_count(acc_object) @staticmethod def get_character_extents(acc_object, offset, coord_type): """ Gets a bounding box containing the glyph representing the character at a particular text offset. The returned values are meaningful only if the Text has both STATE_VISIBLE and STATE_SHOWING. """ if not AccessibleObject.is_text(acc_object): LOGGING.debug("This object does not have Atspi.Text interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_character_extents(offset, coord_type) return Atspi.Text.get_character_extents(acc_object, offset, coord_type) @staticmethod def get_default_attributes(acc_object): """ Gets the default attributes applied to an Atspi.Text object. The text attributes correspond to CSS attributes where possible. The combination of this attribute set and the attributes reported by Atspi.Text.get_text_attributes describes the entire set of text attributes over a range. """ if not AccessibleObject.is_text(acc_object): LOGGING.debug("This object does not have Atspi.Text interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_default_attributes() return Atspi.Text.get_default_attributes(acc_object) @staticmethod def get_n_selections(acc_object): """ Gets the number of active non-contiguous selections for an Atspi.Text object. """ if not AccessibleObject.is_text(acc_object): LOGGING.debug("This object does not have Atspi.Text interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_n_selections() return Atspi.Text.get_n_selections(acc_object) @staticmethod def get_offset_at_point(acc_object, x, y, coord_type): """ Gets the character offset into the text at a given point. """ if not AccessibleObject.is_text(acc_object): LOGGING.debug("This object does not have Atspi.Text interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_offset_at_point(x, y, coord_type) return Atspi.Text.get_offset_at_point(acc_object, x, y, coord_type) @staticmethod def get_range_extents(acc_object, start_offset, end_offset, coord_type): """ Gets the bounding box for text within a range in an Atspi.Text object. The returned values are meaningful only if the Text has both STATE_VISIBLE and STATE_SHOWING. """ if not AccessibleObject.is_text(acc_object): LOGGING.debug("This object does not have Atspi.Text interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_range_extents(start_offset, end_offset, coord_type) return Atspi.Text.get_range_extents(acc_object,start_offset, end_offset, coord_type) @staticmethod def get_selection(acc_object, selection_num): """ Gets the bounds of the selection_num-th active text selection for an Atspi.Text object. """ if not AccessibleObject.is_text(acc_object): LOGGING.debug("This object does not have Atspi.Text interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_selection(selection_num) return Atspi.Text.get_selection(acc_object, selection_num) @staticmethod def get_string_at_offset(acc_object, offset, granularity): """ Gets a portion of the text exposed through an Atspi.Text according to a given offset and a specific granularity, along with the start and end offsets defining the boundaries of such a portion of text. If granularity is Atspi.TextGranularity.CHAR (0) the character at the offset is returned. If granularity is Atspi.TextGranularity.WORD (1) the returned string is from the word start at or before the offset to the word start after the offset. The returned string will contain the word at the offset if the offset is inside a word and will contain the word before the offset if the offset is not inside a word. If granularity is Atspi.TextGranularity.SENTENCE (2) the returned string is from the sentence start at or before the offset to the sentence start after the offset. The returned string will contain the sentence at the offset if the offset is inside a sentence and will contain the sentence before the offset if the offset is not inside a sentence. If granularity is Atspi.TextGranularity.LINE (3) the returned string is from the line start at or before the offset to the line start after the offset. If granularity is Atspi.TextGranularity.PARAGRAPH (4) the returned string is from the start of the paragraph at or before the offset to the start of the following paragraph after the offset. New in version 2.9.90. """ if not AccessibleObject.is_text(acc_object): LOGGING.debug("This object does not have Atspi.Text interface.") if AccessibleObject.is_newton(acc_object): return acc_object.get_string_at_offset(offset, granularity) return Atspi.Text.get_string_at_offset(acc_object, offset, granularity) @staticmethod def get_text(acc_object, start_offset, end_offset): """ Gets a range of text from an Atspi.Text object. The number of bytes in the returned string may exceed either end_offset or start_offset, since UTF-8 is a variable-width encoding. """ if not AccessibleObject.is_text(acc_object): LOGGING.debug("This object does not have Atspi.Text interface.") return None if AccessibleObject.is_newton(acc_object): return acc_object.get_text(start_offset, end_offset) return Atspi.Text.get_text(acc_object, start_offset, end_offset) @staticmethod def get_text_after_offset(acc_object, offset, coord_type): """ Gets delimited text from an Atspi.Text object which follows a given text offset. """ if not AccessibleObject.is_text(acc_object): LOGGING.debug("This object does not have Atspi.Text interface.") return None if AccessibleObject.is_newton(acc_object): return acc_object.get_text_after_offset(offset, coord_type) return Atspi.Text.get_text_after_offset(acc_object, offset, coord_type) @staticmethod def get_text_at_offset(acc_object, offset, coord_type): """ Gets delimited text from an Atspi.Text object which includes a given text offset. Deprecated since version 2.10. Use Atspi.Text.get_string_at_offset. """ if not AccessibleObject.is_text(acc_object): LOGGING.debug("This object does not have Atspi.Text interface.") return None if AccessibleObject.is_newton(acc_object): return acc_object.get_text_at_offset(offset, coord_type) return Atspi.Text.get_text_at_offset(acc_object, offset, coord_type) @staticmethod def get_text_attribute_value(acc_object, offset, attribute_name): """ Gets the value of a named attribute at a given offset. Deprecated since version 2.10: Use Atspi.Text.get_text_attribute_value instead. Note: The deprecation note what to use seems wrong. """ if not AccessibleObject.is_text(acc_object): LOGGING.debug("This object does not have Atspi.Text interface.") return None if AccessibleObject.is_newton(acc_object): return acc_object.get_text_attribute_value(offset, attribute_name) return Atspi.Text.get_text_attribute_value(acc_object, offset, attribute_name) @staticmethod def get_text_attributes(acc_object, offset): """ Gets the attributes applied to a range of text from an Atspi.Text object. The text attributes correspond to CSS attributes where possible. Deprecated since version 2.10: Use Atspi.Text.get_text_attributes instead. Note: The deprecation note what to use seems wrong. """ if not AccessibleObject.is_text(acc_object): LOGGING.debug("This object does not have Atspi.Text interface.") return None if AccessibleObject.is_newton(acc_object): return acc_object.get_text_attributes(offset) return Atspi.Text.get_text_attributes(acc_object, offset) @staticmethod def get_text_before_offset(acc_object, offset, coord_type): """ Gets delimited text from an Atspi.Text object which precedes a given text offset. """ if not AccessibleObject.is_text(acc_object): LOGGING.debug("This object does not have Atspi.Text interface.") return None if AccessibleObject.is_newton(acc_object): return acc_object.get_text_before_offset(offset, coord_type) return Atspi.Text.get_text_before_offset(acc_object, offset, coord_type) @staticmethod def remove_selection(acc_object, selection_num): """ De-selects a text selection. """ if not AccessibleObject.is_text(acc_object): LOGGING.debug("This object does not have Atspi.Text interface.") return None if AccessibleObject.is_newton(acc_object): return acc_object.remove_selection(selection_num) return Atspi.Text.remove_selection(acc_object, selection_num) @staticmethod def scroll_substring_to(acc_object, start_offset, end_offset, coord_type): """ Scrolls whatever container of the Atspi.Text text range so it becomes visible on the screen. """ if not AccessibleObject.is_text(acc_object): LOGGING.debug("This object does not have Atspi.Text interface.") return None if AccessibleObject.is_newton(acc_object): return acc_object.scroll_substring_to(start_offset, end_offset, coord_type) return Atspi.Text.scroll_substring_to(acc_object, start_offset, end_offset, coord_type) @staticmethod def scroll_substring_to_point(acc_object, start_offset, end_offset, coord_type, x, y): """ Scrolls whatever container of the Atspi.Text text range so it becomes visible on the screen at a given position. """ if not AccessibleObject.is_text(acc_object): LOGGING.debug("This object does not have Atspi.Text interface.") return None if AccessibleObject.is_newton(acc_object): return acc_object.scroll_substring_to_point( start_offset, end_offset, coord_type, x, y ) return Atspi.Text.scroll_substring_to_point( acc_object, start_offset, end_offset, coord_type, x, y ) @staticmethod def set_caret_offset(acc_object, new_offset): """ Moves the text caret to a given position. """ if not AccessibleObject.is_text(acc_object): LOGGING.debug("This object does not have Atspi.Text interface.") return None if AccessibleObject.is_newton(acc_object): return acc_object.set_caret_offset(new_offset) return Atspi.Text.set_caret_offset(acc_object, new_offset) @staticmethod def set_selection(acc_object, selection_num, start_offset, end_offset): """ Changes the bounds of an existing Atspi.Text text selection. """ if not AccessibleObject.is_text(acc_object): LOGGING.debug("This object does not have Atspi.Text interface.") return None if AccessibleObject.is_newton(acc_object): return acc_object.set_selection(selection_num, start_offset, end_offset) return Atspi.Text.set_selection(acc_object, selection_num, start_offset, end_offset) @staticmethod def get_all_text(acc_object): """ Get a text from Atspi.Text object without a need to specify range. """ if not AccessibleObject.is_text(acc_object): LOGGING.debug("This object does not have Atspi.Text interface.") return None if AccessibleObject.is_newton(acc_object): length = acc_object.get_character_count() else: length = Atspi.Text.get_character_count(acc_object) if not length: return "" return AccessibleText.get_text(acc_object, 0, length) dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/accessibles/accessible_utilities.py000066400000000000000000000124201520163156700314640ustar00rootroot00000000000000#!/usr/bin/python3 """ Handling for Accessibles, extending with automation specific use cases. """ # pylint: disable=broad-exception-caught # pylint: disable=wrong-import-position # pylint: disable=import-error # pylint: disable=no-name-in-module # ruff: noqa: E501 # ruff: noqa: E402 import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi from dogtail.utils import do_delay from dogtail.config import config from dogtail.logging import logging_class LOGGING = logging_class.logger from dogtail.accessibles.accessible_object import AccessibleObject, SearchError @logging_class.log_a_class class AccessibleUtilities: """ Utility class. Heavily inspired by Orca's AXUtilities. """ @staticmethod def get_application(acc_object): """ Returns the application object of an Accessible. """ try: if AccessibleObject.is_newton(acc_object): application = acc_object.get_application() else: application = Atspi.Accessible.get_application(acc_object) except Exception as error: LOGGING.debug(f"Error when getting application node: {error}") return None return application @staticmethod def get_toolkit(acc_object): """ Return string description of the toolkit we are using. """ toolkit = {} application_object = AccessibleUtilities.get_application(acc_object) toolkit_name = AccessibleUtilities.get_toolkit_name(application_object) toolkit_version = AccessibleUtilities.get_toolkit_version(application_object) toolkit[toolkit_name] = toolkit_version return toolkit @staticmethod def get_toolkit_name(acc_object): """ Returns the name of the toolkit. """ try: if AccessibleObject.is_newton(acc_object): toolkit_name = acc_object.get_toolkit_name() else: toolkit_name = Atspi.Accessible.get_toolkit_name(acc_object) except Exception as error: LOGGING.debug(f"Error when getting toolkit name: {error}") return None return toolkit_name @staticmethod def get_toolkit_version(acc_object): """ Returns the name of the toolkit. """ try: if AccessibleObject.is_newton(acc_object): toolkit_version = acc_object.get_toolkit_version() else: toolkit_version = Atspi.Accessible.get_toolkit_version(acc_object) except Exception as error: LOGGING.debug(f"Error when getting toolkit name: {error}") return None return toolkit_version @staticmethod def get_desktop(): """ Returns the accessible desktop. """ try: # TODO, figure out how to tell if Newton is to be used. # For now pass None to is_newton. if AccessibleObject.is_newton(None): #desktop = Newton.get_desktop(0) desktop = None else: desktop = Atspi.get_desktop(0) except Exception as error: LOGGING.debug(f"Error when getting desktop from Atspi: {error}") return None return desktop @staticmethod def get_all_applications(): """ Returns a list of applications known to Atspi. """ desktop = AccessibleUtilities.get_desktop() if desktop is None: return [] return list(AccessibleObject.iter_children(desktop)) @staticmethod def get_application_named(application_name, retry=True): """ Returns a list of applications known to Atspi. """ for number_of_attempts in range(config.search_cut_off_limit): if number_of_attempts >= config.search_warning_threshold: LOGGING.info(f"Searching again (attempt '{number_of_attempts}')") for application in AccessibleUtilities.get_all_applications(): if application.get_name() == application_name: return application if not retry: break do_delay(config.search_back_off_delay) raise SearchError(f"Application '{application_name}' was not found in desktop.") @staticmethod def is_application_in_desktop(application): """ Returns true if app is known to Atspi. """ for child in AccessibleUtilities.get_all_applications(): if child.get_name() == application: return True LOGGING.debug("Application is not in desktop.") return False @staticmethod def get_application_with_pid(pid): """ Returns the accessible application with the specified pid. """ for application_object in AccessibleUtilities.get_all_applications(): if AccessibleObject.is_newton(application_object): if application_object.get_process_id() == pid: return application_object else: if Atspi.Accessible.get_process_id(application_object) == pid: return application_object LOGGING.debug(f"Application with pid '{pid}' not in desktop.") return None dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/accessibles/accessible_value.py000066400000000000000000000070561520163156700305760ustar00rootroot00000000000000#!/usr/bin/python3 """ Handling for Accessible Value. """ # pylint: disable=broad-exception-caught # pylint: disable=wrong-import-position # pylint: disable=import-error # pylint: disable=no-name-in-module # ruff: noqa: E402 import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi from dogtail.logging import logging_class LOGGING = logging_class.logger from dogtail.accessibles.accessible_object import AccessibleObject @logging_class.log_a_class class AccessibleValue: """ Value class. Heavily inspired by Orca's classes. """ @staticmethod def get_current_value(acc_object): """ Gets the current value for an Atspi.Value. """ if not AccessibleObject.is_valid(acc_object): LOGGING.debug("Object is not valid Accessible.") return False if not AccessibleObject.is_value(acc_object): LOGGING.debug("Accessible object does not have a Value interface.") return False if AccessibleObject.is_newton(acc_object): return acc_object.get_current_value() return Atspi.Value.get_current_value(acc_object) @staticmethod def get_maximum_value(acc_object): """ Gets the maximum allowed value for an Atspi.Value. """ if not AccessibleObject.is_valid(acc_object): LOGGING.debug("Object is not valid Accessible.") return False if not AccessibleObject.is_value(acc_object): LOGGING.debug("Accessible object does not have a Value interface.") return False if AccessibleObject.is_newton(acc_object): return acc_object.get_maximum_value() return Atspi.Value.get_maximum_value(acc_object) @staticmethod def get_minimum_increment(acc_object): """ Gets the minimum increment by which an Atspi.Value can be adjusted. """ if not AccessibleObject.is_valid(acc_object): LOGGING.debug("Object is not valid Accessible.") return False if not AccessibleObject.is_value(acc_object): LOGGING.debug("Accessible object does not have a Value interface.") return False if AccessibleObject.is_newton(acc_object): return acc_object.get_minimum_increment() return Atspi.Value.get_minimum_increment(acc_object) @staticmethod def get_minimum_value(acc_object): """ Gets the minimum allowed value for an Atspi.Value. """ if not AccessibleObject.is_valid(acc_object): LOGGING.debug("Object is not valid Accessible.") return False if not AccessibleObject.is_value(acc_object): LOGGING.debug("Accessible object does not have a Value interface.") return False if AccessibleObject.is_newton(acc_object): return acc_object.get_minimum_value() return Atspi.Value.get_minimum_value(acc_object) @staticmethod def set_current_value(acc_object, set_value): """ Set a value to an Accessible Value object. """ if not AccessibleObject.is_valid(acc_object): LOGGING.debug("Object is not valid Accessible.") return False if not AccessibleObject.is_value(acc_object): LOGGING.debug("Accessible object does not have a Value interface.") return False if AccessibleObject.is_newton(acc_object): return acc_object.set_current_value(set_value) return Atspi.Value.set_current_value(acc_object, set_value) dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/config.ini000066400000000000000000000017271520163156700244200ustar00rootroot00000000000000[config] # Timing. action_delay = 1.0 typing_delay = 0.1 default_delay = 0.5 double_click_delay = 0.1 # searchBackoffDuration # Searching. search_back_off_delay = 0.5 search_warning_threshold = 3 search_cut_off_limit = 20 search_showing_only = False # Children. DO we need this? TODO children_limit = 100 # Run from utils. run_interval = 0.5 run_timeout = 30 # Gtk4 gtk4_offset = (12, 12) # Debug values. debug_dogtail = False debug_file = /tmp/dogtail_debug.log debug_file_persistence = False debug_searching = False debug_sleep = False debug_search_paths = False # debug_translation = False ? # Log setting. #log_debug_to_std_out = True # maybe no longer needed with new logging # No idea. absolute_node_paths = False ensure_sensitivity = False # blink_on_actions = False - useless under wayland fatal_errors = False check_for_a11y = True #[files] # Generally useless, lets do a proper logs in logging.py # Never really used it. [user_config] user_value_x = user_value_x dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/config.py000066400000000000000000000332541520163156700242710ustar00rootroot00000000000000#!/usr/bin/python3 """ The configuration module for dogtail. """ # pylint: disable=line-too-long # ruff: noqa: E501 import configparser from typing import get_origin, get_args import copy from dogtail.logging import logging_class LOGGING = logging_class.logger YES_VALUES = ["y", "yes", "t", "true", "True", "1"] NO_VALUES = ["", "n", "no", "f", "false", "False", "0"] CONFIG_SCHEMA = { "config": { "action_delay": ([float, int], 1), "typing_delay": ([float, int], 0.1), "default_delay":([float, int], 0.5), "double_click_delay": ([float, int], 0.1), "search_back_off_delay": ([float, int], 0.5), "search_warning_threshold": (int, 3), "search_cut_off_limit": (int, 20), "search_showing_only": (bool, False), "children_limit": (int, 100), "run_interval": ([float, int], 0.5), "run_timeout": ([float, int], 30), "gtk4_offset": ([list[int], tuple[int]], [12, 12]), "debug_dogtail": (bool, False), "debug_file": (str, "/tmp/dogtail_debug.log"), "debug_file_persistence": (bool, False), "debug_searching": (bool, False), "debug_sleep": (bool, False), "debug_search_paths": (bool, False), "absolute_node_paths": (bool, False), "ensure_sensitivity": (bool, False), "fatal_errors": (bool, False), "check_for_a11y": (bool, True), }, "user_config": { "user_value_x": (object, "user_value_x"), }, } COMPATIBILITY_CONFIG_SCHEMA = { "config": { "scratchDir": (object, None), "dataDir": (object, None), "logDir": (object, None), "scriptName": (object, None), "encoding": (object, None), "configFile": (object, None), "baseFile": (object, None), "searchBackoffDuration": (object, None), "searchCutoffCount": (object, None), "logDebugToStdOut": (object, None), "debugTranslation": (object, None), "blinkOnActions": (object, None), "buttonRoleCompat": (object, None), "logDebugToFile": (object, None), "scratch_dir": (object, None), "data_dir": (object, None), "log_dir": (object, None), "script_name": (object, None), "config_file": (object, None), "base_file": (object, None), "search_backoff_duration": (object, None), "search_cutoff_count": (object, None), "log_debug_to_std_out": (object, None), "debug_translation": (object, None), "blink_on_actions": (object, None), "button_role_compat": (object, None), "log_debug_to_file": (object, None), } } class ConfigValidationError(Exception): pass def cast_basic(value, expected_type): """ Casts primitive types. """ LOGGING.debug(logging_class.get_func_params_and_values()) if isinstance(value, str) and expected_type is bool: lower = value.lower() if lower in YES_VALUES: return True if lower in NO_VALUES: return False LOGGING.debug(f"Invalid boolean: {value}") raise ValueError(f"Invalid boolean: {value}") LOGGING.debug(f"Return value '{value}' as a type '{type(expected_type(value))}'") return expected_type(value) def cast_sequence(value: str, expected_container): """ Casts comma-separated values into list[...] or tuple[...]. """ LOGGING.debug(logging_class.get_func_params_and_values()) origin = get_origin(expected_container) (item_type,) = get_args(expected_container) # Strip optional list/tuple brackets like [1,2,3] or (1,2,3). cleaned = value.strip() if (cleaned.startswith("[") and cleaned.endswith("]")) or \ (cleaned.startswith("(") and cleaned.endswith(")")): cleaned = cleaned[1:-1].strip() # Split by comma. items = [x.strip() for x in cleaned.split(",") if x.strip()] casted = [cast_value(x, item_type) for x in items] if origin is list: return casted if origin is tuple: return tuple(casted) LOGGING.debug(f"Unsupported sequence type: {origin}") raise ValueError(f"Unsupported sequence type: {origin}") def cast_value(value, expected_type): # --- Union-of-types support --- if isinstance(expected_type, list): errors = [] for type_to_iterate in expected_type: try: return cast_value(value, type_to_iterate) except Exception as error: LOGGING.debug(str(error)) errors.append(str(error)) raise ValueError(" | ".join(errors)) origin = get_origin(expected_type) args = get_args(expected_type) # --- Handle sequences --- if origin in (list, tuple): # If value is already the correct container type, just validate inner types LOGGING.debug("Handling sequence.") if isinstance(value, origin): LOGGING.debug(f"Value '{value}' is of type '{origin}'.") if args: item_type = args[0] for i, item in enumerate(value): if not isinstance(item, item_type): LOGGING.debug(f"Element '{i}' in '{value}' is not of type '{item_type}'.") raise ValueError(f"Element '{i}' in '{value}' is not of type '{item_type}'.") LOGGING.debug(f"Returning value '{value}'.") return value # Otherwise parse string into sequence LOGGING.debug("Continuing casting to sequence.") return cast_sequence(value, expected_type) # --- Handle simple types --- # If value is already of correct type, return. if isinstance(value, expected_type): LOGGING.debug(f"Value '{value}' is already of type '{expected_type}'.") return value # If the value is object. elif expected_type is object: LOGGING.debug(f"Expected type is '{expected_type}' with value '{value}'.") return value else: # Otherwise, cast string to type. LOGGING.debug(f"Casting as basic value '{value}' of type '{expected_type}'.") return cast_basic(value, expected_type) def load_and_validate_config(path: str, schema: dict): LOGGING.debug(logging_class.get_func_params_and_values()) config = configparser.ConfigParser() config.read(path) validated = {} # We do not care about config sections in dogtail, just set the variables to a dict. # Step 1: Process all sections found in the file, even if not in schema. LOGGING.debug("Process all variables.") for section in config.sections(): for key, raw_value in config[section].items(): # If schema defines type → validate if section in schema and key in schema[section]: expected_type, default_value = schema[section][key] try: value = cast_value(raw_value, expected_type) except Exception as error: raise ConfigValidationError(f"Type error in [{section}].{key}: {error}") validated[key] = value LOGGING.debug(f"Set Validated key '{key}' to value '{value}'") else: # Unknown key → keep raw value as string validated[key] = raw_value LOGGING.debug(f"Set Validated key '{key}' to raw value '{raw_value}'") LOGGING.debug("Process default variables.") # Step 2: Ensure missing schema sections & keys get default values. for section, fields in schema.items(): for key, (expected_type, default_value) in fields.items(): if key not in validated: validated[key] = default_value LOGGING.debug(f"Set Default key '{key}' to value '{default_value}'") return validated @logging_class.log_a_class class _Config: """ Config class to keep backwards compatibility and to have getters and setters. """ validated_config = load_and_validate_config("dogtail_config.ini", CONFIG_SCHEMA) validated_compatibility_config = load_and_validate_config("dogtail_config.ini", COMPATIBILITY_CONFIG_SCHEMA) LOGGING.debug(f"Validated config: '{validated_config}'") # Create a deep copy instead of shallow copy to keep values for reset method. _default_values_storage = copy.deepcopy(validated_config) # Handle dogtail debug logging. if validated_config["debug_dogtail"]: LOGGING.info("Debugging dogtail to console.") logging_class.debug_to_console() def reset_configuration(self): """ Reset configuration to default values. """ self.validated_config.update(self._default_values_storage) def adjust_casing(self, string_to_fix): """ Transforms a string to snake_case. :param string_to_fix: String to transform to snake_case :type string_to_fix: str :return: Transformed string. :rtype: str """ string_in_snake_case = "" for character in string_to_fix: if character.isalpha and character.isupper(): string_in_snake_case += "_" + character.lower() else: string_in_snake_case += character return string_in_snake_case @logging_class.do_not_log def __setattr__(self, option_id, value_to_set): # Set a variable to use for the setter logic. set_option_id = option_id compatibility_option_id = option_id # Make sure compatibility is kept somewhat to dogtail-1.x, ignore the previously allowed values. if compatibility_option_id in self.validated_compatibility_config: LOGGING.info(f"Attempt to use __setattr__ option '{compatibility_option_id}'.") LOGGING.info("This option is kept only for compatibility purposes and has no effect.") return # Attempt to have some backwards compatibility and support camelCase. if any(char.isupper() for char in set_option_id): set_option_id = self.adjust_casing(set_option_id) LOGGING.debug(f"Config variable was transformed to '{set_option_id}'.") # Set custom user value to use in dogtail run. # Current logic will fail on any attempt not set in config.ini. # Should we allow setting values during a dogtail execution? # if "custom_" in set_option_id: # LOGGING.info(f"Setting a custom user value '{set_option_id}'.") # self.options[set_option_id] = value_to_set # In other cases check that the value exists. if set_option_id not in self.validated_config: raise AttributeError(f"Attempt to use invalid option '{set_option_id}'.") # LOGGING.info(f"Attempt to use invalid option '{set_option_id}'.") # return # Set the value if the value is not already present. if self.validated_config[set_option_id] != value_to_set: validated_expected_type = None # First attempt to get defaults from the schema. for section in CONFIG_SCHEMA.keys(): if set_option_id in CONFIG_SCHEMA[section]: validated_expected_type, default_value = CONFIG_SCHEMA[section][set_option_id] # If unsuccessful mark the expected type as anything. if not validated_expected_type: LOGGING.debug("Setting expected type to object.") validated_expected_type = object try: # Set the value. LOGGING.debug(f"Validating '{value_to_set}' to '{validated_expected_type}'.") self.validated_config[set_option_id] = cast_value(value_to_set, validated_expected_type) except Exception: # Log the message but do not end, use the default. LOGGING.info(" ".join(( f"Attempt to set value of type '{type(value_to_set)}'", f"to key with accepted types '{validated_expected_type}'", ))) # Setting the value. LOGGING.info(f"Fallback of config option '{set_option_id}' to '{default_value}'.") self.validated_config[set_option_id] = default_value if set_option_id == "debug_dogtail" and self.validated_config[set_option_id]: LOGGING.info("Debugging dogtail to console.") logging_class.debug_to_console() if set_option_id == "debug_dogtail" and not self.validated_config[set_option_id]: LOGGING.info("Disabling debugging dogtail to console.") logging_class.disable_debug_to_console() @logging_class.do_not_log def __getattr__(self, option_id): # Set a variable to use for the getter logic. get_option_id = option_id compatibility_get_option_id = option_id # Handling compatibility values in getter. if compatibility_get_option_id in self.validated_compatibility_config: LOGGING.info(f"Attempt to use __getattr__ option '{compatibility_get_option_id}'.") LOGGING.info("This option is kept only for compatibility purposes and has no effect.") return # Attempt to have some backwards compatibility and support camelCase. if any(char.isupper() for char in get_option_id): get_option_id = self.adjust_casing(get_option_id) LOGGING.debug(f"Config variable was transformed to '{get_option_id}'.") LOGGING.debug(f"Its value is '{self.validated_config[get_option_id]}'.") # Handling proper values. try: return self.validated_config[get_option_id] except KeyError as error: raise AttributeError(f"Attempt to use invalid option '{get_option_id}'.") from error # LOGGING.info(f"Attempt to use invalid option '{get_option_id}'.") config = _Config() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/distro.py000066400000000000000000000436571520163156700243400ustar00rootroot00000000000000#!/usr/bin/python3 """ Handles differences between different distributions. """ # pylint: disable=line-too-long # ruff: noqa: E501 import os import re from subprocess import check_output, STDOUT from dogtail.version import Version from dogtail.logging import logging_class LOGGING = logging_class.logger __author__ = """ Dave Malcolm , Zack Cerza """ class DistributionNotSupportedError(Exception): # pragma: no cover """ This distribution is not supported. """ PATCH_MESSAGE = "Please open merge requests at https://gitlab.com/dogtail/dogtail" def __init__(self, distribution): self.distribution = distribution def __str__(self): return self.distribution + ". " + DistributionNotSupportedError.PATCH_MESSAGE class PackageNotFoundError(Exception): """ Error finding the requested package. """ class PackageDb: """ Class to abstract the details of whatever software package database is in use (RPM, APT, etc) """ def __init__(self): self.prefix = "/usr" self.locale_prefixes = [self.prefix + "/share/locale"] def get_version(self, package_name): """ Method to get the version of an installed package as a Version instance (or raise an exception if not found) Note: does not know about distributions' internal revision numbers. """ raise NotImplementedError def get_files(self, package_name): """ Method to get a list of filenames owned by the package, or raise an exception if not found. """ raise NotImplementedError def get_mo_files(self, locale=None): """ Method to get a list of all .mo files on the system, optionally for a specific locale. """ mo_files = {} def append_if_mo_file(mo_files, dir_name, f_names): for f_name in f_names: if re.match("(.*)\\.mo", f_name): mo_files[dir_name + "/" + f_name] = None for locale_prefix in self.locale_prefixes: if locale: locale_prefix = locale_prefix + "/" + locale os.walk(locale_prefix, append_if_mo_file, mo_files) return list(mo_files.keys()) def get_dependencies(self, package_name): """ Method to get a list of unique package names that this package is dependent on, or raise an exception if the package is not found. """ raise NotImplementedError ### Backwards compatibility. @property def localePrefixes(self): # pylint: disable=invalid-name """ Set localePrefixes. Wrapper over locale_prefixes. """ return self.locale_prefixes @localePrefixes.setter def localePrefixes(self, value_to_set): # pylint: disable=invalid-name self.locale_prefixes = value_to_set def getVersion(self, packageName): # pylint: disable=invalid-name """ Method to get the version of an installed package as a Version instance (or raise an exception if not found). Wrapper over get_version. Note: does not know about distributions' internal revision numbers. """ return self.get_version(package_name=packageName) def getFiles(self, packageName): # pylint: disable=invalid-name """ Method to get a list of filenames owned by the package, or raise an exception if not found. Wrapper over get_files. """ return self.get_files(package_name=packageName) def getMoFiles(self, locale=None): # pylint: disable=invalid-name """ Method to get a list of all .mo files on the system, optionally for a specific locale. Wrapper over get_mo_files. """ return self.get_mo_files(locale=locale) def getDependencies(self, packageName): # pylint: disable=invalid-name """ Method to get a list of unique package names that this package is dependent on, or raise an exception if the package is not found. Wrapper over get_dependencies. """ return self.get_dependencies(package_name=packageName) class _RpmPackageDb(PackageDb): """ RPM Package Database Implementation. """ def get_version(self, package_name): import rpm # pylint: disable=import-outside-toplevel,import-error transaction_set = rpm.TransactionSet() for header in transaction_set.dbMatch("name", package_name): return Version.from_string(header["version"]) raise PackageNotFoundError(package_name) def get_files(self, package_name): import rpm # pylint: disable=import-outside-toplevel,import-error transaction_set = rpm.TransactionSet() for header in transaction_set.dbMatch("name", package_name): return header["filenames"] raise PackageNotFoundError(package_name) def get_dependencies(self, package_name): import rpm # pylint: disable=import-outside-toplevel,import-error transaction_set = rpm.TransactionSet() for header in transaction_set.dbMatch("name", package_name): result = {} for requirement in header[rpm.RPMTAG_REQUIRES]: for dependency_package_header in transaction_set.dbMatch("provides", requirement): dependency_name = dependency_package_header["name"] if dependency_name != package_name: result[dependency_name] = None return list(result.keys()) raise PackageNotFoundError(package_name) ### Backwards compatibility. def getVersion(self, packageName): return self.get_version(package_name=packageName) def getFiles(self, packageName): return self.get_files(package_name=packageName) def getDependencies(self, packageName): return self.get_dependencies(package_name=packageName) class _AptPackageDb(PackageDb): """ Apt Package Database Implementation. """ def __init__(self): super().__init__() self.cache = None def get_version(self, package_name): if not self.cache: import apt_pkg # pylint: disable=import-outside-toplevel,import-error apt_pkg.init() self.cache = apt_pkg.Cache() packages = self.cache.packages for package in packages: if package.name == package_name: version_string = re.match(".*Ver:'(.*)-.*' Section:", str(package.current_ver)).group(1) return Version.fromString(version_string) raise PackageNotFoundError(package_name) def get_files(self, package_name): files = [] lines = os.popen(f"dpkg -L {package_name}").readlines() if not lines: raise PackageNotFoundError(package_name) for line in lines: file = line.strip() if file: files.append(file) return files def get_dependencies(self, package_name): result = {} if not self.cache: import apt_pkg # pylint: disable=import-outside-toplevel,import-error apt_pkg.init() self.cache = apt_pkg.Cache() packages = self.cache.packages for package in packages: if package.name == package_name: current = package.current_ver if not current: raise PackageNotFoundError(package_name) depends = current.depends_list dependency_list = depends["Depends"] for dependency in dependency_list: name = dependency[0].target_pkg.name result[name] = None return list(result.keys()) ### Backwards compatibility. def getVersion(self, packageName): return self.get_version(package_name=packageName) def getFiles(self, packageName): return self.get_files(package_name=packageName) def getDependencies(self, packageName): return self.get_dependencies(package_name=packageName) class _UbuntuAptPackageDb(_AptPackageDb): """ Ubuntu Apt Package Database Implementation. """ def __init__(self): _AptPackageDb.__init__(self) self.localePrefixes.append(self.prefix + "/share/locale-langpack") class _PortagePackageDb(PackageDb): """ Portage Package Database Implementation. """ def get_version(self, package_name): # The portage utilities are almost always going to be in /usr/lib/portage/pym import sys # pylint: disable=import-outside-toplevel sys.path.append("/usr/lib/portage/pym") import portage # pylint: disable=import-outside-toplevel,import-error # This takes the first package returned in the list, in the # case that there are slotted packages, and removes the leading # category such as 'sys-apps'. gentoo_package_name = portage.db["/"]["vartree"].dbapi.match(package_name)[0].split("/")[1] # This removes the distribution specific versioning returning only the # upstream version. upstream_version = portage.pkgsplit(gentoo_package_name)[1] # print("Version of package is: " + upstream_version) return Version.fromString(upstream_version) ### Backwards compatibility. def getVersion(self, packageName): return self.get_version(package_name=packageName) class _ConaryPackageDb(PackageDb): """ Conary Package Database Implementation. """ def get_version(self, package_name): from conaryclient import ConaryClient # pylint: disable=import-outside-toplevel,import-error client = ConaryClient() database_versions = client.db.getTroveVersionList(package_name) if not database_versions: raise PackageNotFoundError(package_name) return database_versions[0].trailingRevision().asString().split("-")[0] ### Backwards compatibility. def getVersion(self, packageName): return self.get_version(package_name=packageName) class _SolarisPackageDb(PackageDb): """ Solaris Package Database Implementation. """ # The getVersion not implemented because on Solaris multiple modules are installed # in single packages, so it is hard to tell what version number of a specific # module. class JhBuildPackageDb(PackageDb): """ JH Build Package Database Implementation. """ def __init__(self): PackageDb.__init__(self) prefixes = [] prefixes.append(os.environ["LD_LIBRARY_PATH"]) prefixes.append(os.environ["XDG_CONFIG_DIRS"]) prefixes.append(os.environ["PKG_CONFIG_PATH"]) self.prefix = os.path.commonprefix(prefixes) self.locale_prefixes.append(self.prefix + "/share/locale") def get_dependencies(self, package_name): result = {} lines = os.popen("jhbuild list " + package_name).readlines() for line in lines: if line: result[line.strip()] = None return list(result.keys()) ### Backwards compatibility. def getDependencies(self, packageName): return self.get_dependencies(package_name=packageName) class _ContinuousPackageDb(PackageDb): """ Continuous Package Database Implementation. """ def get_version(self, package_name): return "" def get_files(self, package_name): return check_output( [f"ls -1 /usr/share/locale/*/LC_MESSAGES/{package_name}.mo"], shell=True ).strip().split("\n") def get_dependencies(self, package_name): return [] ### Backwards compatibility. def getVersion(self, packageName): return self.get_version(package_name=packageName) def getFiles(self, packageName): return self.get_files(package_name=packageName) def getDependencies(self, packageName): return self.get_dependencies(package_name=packageName) class _ArchPackageDb(PackageDb): """ Arch Package Database Implementation. """ def _pacman(self, option, packageName): # pylint: disable=invalid-name package_name = packageName if option == "v": command_option = "i %s | grep Version | cut -d: -f2-" elif option == "f": command_option = "l %s | cut -d' ' -f2-" elif option == "d": command_option = "i %s | grep 'Depends On' | cut -d: -f2-" else: command_option = "" command = "LC_ALL=C COLUMNS=1000 pacman -Q" + command_option out = check_output(command % package_name, stderr=STDOUT, shell=True ).decode("UTF-8") if out.startswith("error:"): raise PackageNotFoundError(package_name) return out.strip() def get_version(self, package_name): version = self._pacman("v", package_name) return Version.from_string(version) def get_files(self, package_name): files = self._pacman("f", package_name) return [f for f in files.split() if not f.endswith('/')] def get_dependencies(self, package_name): deps = self._pacman("d", package_name) return [d for d in deps.split() if d != "None"] ### Backwards compatibility. def getVersion(self, packageName): return self.get_version(package_name=packageName) def getFiles(self, packageName): return self.get_files(package_name=packageName) def getDependencies(self, packageName): return self.get_dependencies(package_name=packageName) class Distro: """ Class representing a distribution. Scripts may want to do arbitrary logic based on whichever distro is in use (e.g. handling differences in names of packages, distribution-specific patches, etc.) We can either create methods in the Distro class to handle these, or we can use constructs like isinstance(distro, Ubuntu) to handle this. We can even create hierarchies of distro subclasses to handle this kind of thing (could get messy fast though) """ package_database = None @property def packageDb(self): # pylint: disable=invalid-name """ Set localePrefixes. Wrapper over locale_prefixes. """ return self.package_database @packageDb.setter def packageDb(self, value_to_set): # pylint: disable=invalid-name self.package_database = value_to_set class Fedora(Distro): """ Defining Fedora Distribution. """ def __init__(self): self.package_database = _RpmPackageDb() class RHEL(Fedora): """ Defining RHEL Distribution. """ class Debian(Distro): """ Defining Debian Distribution. """ def __init__(self): self.package_database = _AptPackageDb() class Ubuntu(Debian): """ Defining Ubuntu Distribution based on Debian. """ def __init__(self): super().__init__() self.package_database = _UbuntuAptPackageDb() class Suse(Distro): # pragma: no cover """ Defining Suse Distribution. """ def __init__(self): self.package_database = _RpmPackageDb() class Gentoo(Distro): # pragma: no cover """ Defining Gentoo Distribution. """ def __init__(self): self.package_database = _PortagePackageDb() class Conary(Distro): # pragma: no cover """ Defining Conary Distribution. """ def __init__(self): self.package_database = _ConaryPackageDb() class Solaris(Distro): # pragma: no cover """ Defining Solaris Distribution. """ def __init__(self): self.package_database = _SolarisPackageDb() class JHBuild(Distro): # pragma: no cover """ Defining JHBuild Distribution. """ def __init__(self): self.package_database = JhBuildPackageDb() class GnomeContinuous(Distro): # pragma: no cover """ Defining GnomeContinuous Distribution. """ def __init__(self): self.package_database = _ContinuousPackageDb() class Arch(Distro): # pragma: no cover """ Defining Arch Distribution. """ def __init__(self): self.package_database = _ArchPackageDb() def detect_distribution(): """ Detect Distribution. """ if os.environ.get("CERTIFIED_GNOMIE", "no") == "yes": distribution_detection = JHBuild() elif os.path.exists("/etc/SuSE-release"): distribution_detection = Suse() elif os.path.exists("/etc/fedora-release"): distribution_detection = Fedora() elif os.path.exists("/etc/redhat-release"): distribution_detection = RHEL() elif os.path.exists("/usr/share/doc/ubuntu-minimal"): distribution_detection = Ubuntu() elif os.path.exists("/etc/debian_version"): distribution_detection = Debian() elif os.path.exists("/etc/gentoo-release"): distribution_detection = Gentoo() elif os.path.exists("/etc/slackware-version"): raise DistributionNotSupportedError("Slackware") elif os.path.exists("/var/lib/conarydb/conarydb"): distribution_detection = Conary() elif os.path.exists("/etc/arch-release"): distribution_detection = Arch() elif os.path.exists("/etc/release") and \ re.match(".*Solaris", open("/etc/release", encoding="utf-8").readline()): distribution_detection = Solaris() elif os.path.exists("/etc/os-release") and \ re.match(".*GNOME-Continuous", open("/etc/os-release", encoding="utf-8").readline()): distribution_detection = GnomeContinuous() else: raise DistributionNotSupportedError("Unknown") LOGGING.debug(f"Detecting distribution: {distribution_detection.__class__.__name__}") return distribution_detection distribution = detect_distribution() package_database = distribution.package_database ### Backwards compatibility. def detectDistro(): # pylint: disable=invalid-name """ Detect Distribution. Wrapper over detect_distribution. """ return detect_distribution() distro = distribution packageDb = package_database dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/dump.py000066400000000000000000000126471520163156700237740ustar00rootroot00000000000000#!/usr/bin/python3 """ Utility module to represent the application structure as a tree. """ # pylint: disable=line-too-long # ruff: noqa: E501 class AccessibleStructureRepresentation: """ Accessible Structure Representation from given Accessible Node. """ def __init__(self, accessible_node, structure_format="plain", file_name=None, labels=False): self.spacer = " " self.format = structure_format self.accessible_node = accessible_node self.tree_representation = "" self.file_name = file_name self.labels = labels def _get_label_string(self, acc_object): labeler_string = "" if self.labels and acc_object.labeler: for iteration, labeler in enumerate(acc_object.labeler): labeler_string += f" (labeler_{iteration}.text='{labeler.text}') " labeler_string += f" (labeler_{iteration}.name='{labeler.name}') " if self.labels and acc_object.labelee: for iteration, labelee in enumerate(acc_object.labelee): labeler_string += f" (labelee_{iteration}.text='{labelee.name}') " labeler_string += f" (labelee_{iteration}.name='{labelee.name}') " if self.labels and (not acc_object.labeler and not acc_object.labelee): labeler_string += " (No labeler) " return labeler_string def _represent_structure_as_plain(self, acc_object, level=0): labeler_string = self._get_label_string(acc_object) tree_representation = self.spacer * level + str(acc_object) + labeler_string + "\n" for child in acc_object.children: # Not adding Action strings by choice, we can revisit in the future. tree_representation += self._represent_structure_as_plain(child, level + 1) return tree_representation def _represent_structure_as_verbose(self, acc_object, level=0): if not acc_object: return labeler_string = self._get_label_string(acc_object) verbose_str = "".join(( str(acc_object), " - ", f"(position:'{acc_object.position}', size:'{acc_object.size}', ", f"visible:{acc_object.visible}, showing:{acc_object.showing})", f"{labeler_string}" )) tree_representation = self.spacer * level + verbose_str + "\n" for child in acc_object.children: # Not adding Action strings by choice, we can revisit in the future. tree_representation += self._represent_structure_as_verbose(child, level + 1) return tree_representation def _represent_structure_as_tree(self, node, last=True, spacer=""): self._recursive_tree_structure_construction(node, last=last, spacer=spacer) return self.tree_representation def _recursive_tree_structure_construction(self, acc_object, last, spacer): if not acc_object: return prefix_spacer = " " prefix_extend = " │ " suffix_branch = " ├──" suffix_last = " └──" labeler_string = self._get_label_string(acc_object) # Attempt to shorten the line. suffix_based_on_last = suffix_last if last else suffix_branch # Add a line to the tree representation. self.tree_representation += (spacer + suffix_based_on_last + str(acc_object)) + labeler_string + "\n" for index, child in enumerate(acc_object.children): # Not adding Action strings by choice, we can revisit in the future. # Check if the index is last. is_last = index == acc_object.child_count - 1 # Make a new spacer. new_spacer = spacer + (prefix_spacer if last else prefix_extend) self._recursive_tree_structure_construction(child, last=is_last, spacer=new_spacer) def load_data_to_file(self, data): """ Insert Accessible Structure Representation to the file. :param data: Data to insert to the file. :type data: str :raises OSError: Raise OSError if the is an issue with file manipulation. """ try: with open(self.file_name, "w", encoding="utf-8") as file_: file_.write(data) except OSError as error: raise OSError(f"Issue with opening '{self.file_name}' file.") from error def __str__(self): if self.format == "plain": plain_data = self._represent_structure_as_plain(self.accessible_node) if self.file_name: self.load_data_to_file(data=plain_data) return f"[ Plain structure was inserted in file '{self.file_name}'. ]" return plain_data if self.format == "verbose": verbose_data = self._represent_structure_as_verbose(self.accessible_node) if self.file_name: self.load_data_to_file(data=verbose_data) return f"[ Verbose structure was inserted in file '{self.file_name}'. ]" return verbose_data if self.format == "tree": tree_data = self._represent_structure_as_tree(self.accessible_node) if self.file_name: self.load_data_to_file(data=tree_data) return f"[ Tree structure was inserted in file '{self.file_name}'. ]" return tree_data return f"[Unknown format selected '{self.format}']" def __repr__(self): return "" dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/errors.py000066400000000000000000000020601520163156700243270ustar00rootroot00000000000000#!/usr/bin/python3 """ Handles General exceptions, not overly module-specific. Leftover from dogtail-1.x. """ # pylint: disable=line-too-long # ruff: noqa: E501 import inspect from dogtail.logging import logging_class LOGGING = logging_class.logger __author__ = "Zack Cerza " def warn(message, caller=True): """ Generate a warning, and pass it to the debug logger. """ last_frame_info_object = inspect.stack()[-1] # frame = last_frame_info_object[0] file_name = last_frame_info_object[1] lineno = last_frame_info_object[2] #function = last_frame_info_object[3] code_context = last_frame_info_object[4] # index = last_frame_info_object[5] new_message = f"Warning: {file_name}:{lineno}: {message}" if caller and file_name != "" and file_name != "" and code_context: new_message = new_message + ":\n " + code_context[0] del last_frame_info_object LOGGING.debug(new_message) class DependencyNotFoundError(Exception): """ A dependency was not found. """ dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/logging.py000066400000000000000000000214521520163156700244470ustar00rootroot00000000000000#!/usr/bin/python3 """ The logging module for dogtail. """ # pylint: disable=broad-exception-caught # pylint: disable=protected-access # pylint: disable=import-outside-toplevel import os import sys import logging import inspect from functools import wraps # Singleton solution provided by https://stackoverflow.com/a/54209647 class Singleton(type): """ Singleton class used as metaclass by :py:class:`logger.Logging`. """ _instances = {} def __call__(cls, *args, **kwargs): if cls not in cls._instances: cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) return cls._instances[cls] class Logging(metaclass=Singleton): """ Logging class. """ logger = None default_log_file = "/tmp/dogtail_debug.log" def __init__(self) -> None: """ Initiating logger class with some basic logging setup. """ # Preventing circular import, we only need one value for logging. import configparser config_parser = configparser.ConfigParser() config_parser.read("dogtail_config.ini") self.debug_file = config_parser.get( "config", "debug_file", fallback=self.default_log_file ) # Validate the debug file value. self.debug_file = None if self.debug_file == "None" else self.debug_file self.debug_file_persistence = config_parser.get( "config", "debug_file_persistence", fallback=False ) # Validate the debug file persistence value. self.debug_file_persistence = True if self.debug_file_persistence == "True" else False # Nulling the file based persistence config value. if not self.debug_file_persistence: self.dogtail_truncate_the_logger_file() self.logger = logging.getLogger("__dogtail_logger__") self.logger.setLevel(logging.DEBUG) # Disable default handler. self.logger.propagate = False formatter = logging.Formatter( "".join(( "[%(levelname)s] %(asctime)s: ", "[%(filename)s:%(lineno)d] ", "%(func_name)s: %(message)s" )) ) # Default umask is 0o022 which turns off permissions for groups and others. # We need the logger file to be readable and modifiable by anyone. os.umask(0) # Setup of file handler. # All DEBUG and higher level logs will be going to the file. if self.debug_file: file_handler = logging.FileHandler(self.debug_file) file_handler.setLevel(logging.DEBUG) file_handler.setFormatter(formatter) file_handler.set_name("file_handler") # Add file handler only if file is defined. self.logger.addHandler(file_handler) # Setup of console handler. # All INFO and higher level logs will be going to the console. console_handler = logging.StreamHandler(sys.stdout) console_handler.setLevel(logging.INFO) console_handler.setFormatter(formatter) console_handler.set_name("console_handler") # Add console handler. self.logger.addHandler(console_handler) self.logger.addFilter(FuncFilter()) def debug_to_console(self) -> None: """ Set file handler level to DEBUG to get the output to console. """ for handler in self.logger.handlers: if handler.get_name() == "console_handler": handler.setLevel(logging.DEBUG) break def disable_debug_to_console(self) -> None: """ Set file handler level to INFO to get the output to console. """ for handler in self.logger.handlers: if handler.get_name() == "console_handler": handler.setLevel(logging.INFO) break def get_func_params_and_values(self): """ Simple way to have logging data from the place it was called in. """ try: frame = sys._getframe().f_back _local_func_parameters = frame.f_locals.keys() _local_func_parameters_value = frame.f_locals.values() # ATTENTION: Never change this from repr() to str(). # In case of .name .role_name and .description when log is called it goes # to the Node's __str__ which will want to represent the node with # name role_name and description causing it to get logged again and again # wanting to go the __str__ representation. # Causing infinite recursion. # The need to do it this way is for backwards compatibility. # Logging is new so we have to work around it. map_of_keys_and_values = [ x if "self" == x else x if "context" == x else repr(x) + "=" + repr(y) for x, y in zip(_local_func_parameters, _local_func_parameters_value) ] return f"({', '.join(map_of_keys_and_values)})" except Exception as error: self.logger.error("Error in get_func_params_and_values: %s", error) return "" def dogtail_truncate_the_logger_file(self) -> None: """ If the Logging class is used from multiple files or scripts especially when running one script from another, the logger would not be found and file is deleted, making previous logs disappear. Lets just null the file only on request. """ # Null the file only if it exists. # If it does not, no problem, do nothing. if os.path.isfile(self.debug_file): # Default umask is 0o022 which turns off permissions for groups and others. os.umask(0) # Create a specific file descriptor for our needs with proper permissions. descriptor = os.open( path=self.debug_file, flags=(os.O_CREAT | os.O_TRUNC), # Create if not existing, truncate. mode=0o666, ) # Close the file descriptor. os.close(descriptor) def generate_log_for_method(self, func): @wraps(func) def wrapped(*args, **kwargs): # 1. Get definition metadata def_line = func.__code__.co_firstlineno def_file = os.path.basename(inspect.getsourcefile(func) or "unknown") # 2. Parse arguments sig = inspect.signature(func) try: bound = sig.bind(*args, **kwargs) bound.apply_defaults() params = [ f"{key}={value!r}" if key != "self" else "self" for key, value in bound.arguments.items() ] formatted_message = ", ".join(params) output_message = f"({formatted_message})" except ValueError: output_message = "(args/kwargs binding failed)" # 3. Log with definition info # We construct the prefix manually to show exactly where the method lives self.logger.debug( f"defined at [{def_file}:{def_line}] {func.__name__}: {output_message}", stacklevel=2 ) return func(*args, **kwargs) return wrapped def log_a_class(self, cls): """ Go over a class and replace local functions with wrapped versions. """ for name, attr in vars(cls).items(): # 1. Handle Static Methods. if isinstance(attr, staticmethod): if not getattr(attr.__func__, "_do_not_log", False): wrapped_func = self.generate_log_for_method(attr.__func__) setattr(cls, name, staticmethod(wrapped_func)) # 2. Handle Properties. elif isinstance(attr, property) and attr.fget: if not getattr(attr.fget, "_do_not_log", False): new_getter = self.generate_log_for_method(attr.fget) setattr(cls, name, property(new_getter, attr.fset, attr.fdel, attr.__doc__)) # 3. Handle Standard Methods / Functions. elif (inspect.isfunction(attr) or inspect.ismethod(attr)): if not getattr(attr, "_do_not_log", False): setattr(cls, name, self.generate_log_for_method(attr)) return cls def do_not_log(self, fnc): """ Flag a method to be skipped by the class logger. """ fnc._do_not_log = True return fnc class FuncFilter(logging.Filter): """ Filter Class to get exact wanted frame. """ def filter(self, record): # Have to walk the frame a bit back. # 1. filter, 2. handle, 3. _log, 4. debug, 5. Original calling function. record.func_name = str( sys._getframe().f_back.f_back.f_back.f_back.f_back.f_code.co_name ) return True logging_class = Logging() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/ponytail_helper.py000066400000000000000000000345521520163156700262240ustar00rootroot00000000000000#!/usr/bin/python3 """ The ponytail helper module for dogtail. """ # pylint: disable=broad-exception-caught # pylint: disable=import-outside-toplevel # pylint: disable=wrong-import-position # pylint: disable=wrong-import-order # pylint: disable=import-error # pylint: disable=protected-access # pylint: disable=no-name-in-module # ruff: noqa: E402 # ruff: noqa: E501 import os from time import sleep from dogtail.config import config from dogtail.utils import do_delay from dogtail.logging import logging_class LOGGING = logging_class.logger class Singleton(type): """ Singleton class used as metaclass by :py:class:`logger.Logging`. """ _instances = {} def __call__(cls, *args, **kwargs): if cls not in cls._instances: cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) return cls._instances[cls] @logging_class.log_a_class class PonytailHelper(metaclass=Singleton): """ Simple Ponytail wrapper class to initiate ponytail and keep one instance of it. """ # Initial state, no connection. _window_id = None _ponytail_dbus_interface = None _already_initiated = False expected_packages = [ "gnome-ponytail-daemon", "python3-gnome-ponytail-daemon" ] error_message = "\n".join(( "\n", "Error in ponytail initiation might be cause by several reasons:", f"\t1) Packages '{str(expected_packages)}' are not installed.", "\t2) If installed, the gnome-ponytail-daemon process might not be running.", "\t3) You are on the system that does not have GNOME Shell Introspection.", "\n", )) # Keep session as a variable. session_type = "x11" if "XDG_SESSION_TYPE" in os.environ and "wayland" in os.environ["XDG_SESSION_TYPE"]: session_type = "wayland" def __init__(self): # Only makes sense when running under Wayland. if self.session_type != "wayland": return # Do not initiate it if already done. # But make a check to see if self._ponytail_dbus_interface is valid. if not self._already_initiated or not self._ponytail_dbus_interface: try: LOGGING.debug("Initiating ponytail.") from ponytail.ponytail import Ponytail self._ponytail_dbus_interface = Ponytail() sleep(0.1) # Prevent any race conditions. self._ponytail_dbus_interface.disconnect() sleep(0.1) # Prevent any race conditions. self._already_initiated = True except Exception as error: LOGGING.info(f"Error when initiating ponytail: '{error}'") LOGGING.info(self.error_message) def get_ponytail_interface(self): """ Get ponytail interface. """ if not self._ponytail_dbus_interface: raise RuntimeError(self.error_message) return self._ponytail_dbus_interface def ponytail_check_is_xwayland(self, window_id=None, window_list=None): """ Detect Xwayland windows in order to use globals and recordMonitor. """ ponytail_interface = self.get_ponytail_interface() # If there is not window_list, get it from ponytail. if window_list is None: window_list = ponytail_interface.window_list # If window_id is integer return client type. if isinstance(window_id, int): LOGGING.debug("Return wanted window_id.") try: _window_id = int([x["client-type"] for x in window_list if x["id"] == window_id][0]) LOGGING.debug(f"Returning: '{_window_id}'") return _window_id except Exception as error: LOGGING.debug(f"Exception in ponytail: '{error}'") return window_id # If window_id is None, get window_id of focused window. if window_id is None: LOGGING.debug("Return focused window if no window_id was provided.") for window in window_list: if bool(window["has-focus"]): LOGGING.debug(f"Returning client-type '{window['client-type']}'.") return int(window["client-type"]) # The window_id match was not successful, return window_if of gnome-shell: 0. LOGGING.debug("Return window_id 0 which is gnome-shell if no id is found.") return 0 def ponytail_check_connection(self, window_id=None, input_source="mouse"): """ Check ponytail connection. """ ponytail_interface = self.get_ponytail_interface() window_list = ponytail_interface.window_list if (not isinstance(window_id, str)) and (window_list != []) and\ window_id not in [x["id"] for x in window_list]: window_id = None LOGGING.debug("Checking if possibly connected window still exists.") if ponytail_interface.connected and isinstance(ponytail_interface.connected, int): if ponytail_interface.connected not in [x["id"] for x in window_list]: LOGGING.debug("Disconnecting no longer open window.") ponytail_interface.disconnect() sleep(1) LOGGING.debug("Done.") if input_source == "keyboard" and ponytail_interface.connected is None: LOGGING.debug("Keyboard event, connecting monitor.") # ponytail_interface.disconnect() # sleep(1) ponytail_interface.connectMonitor() elif input_source == "keyboard" and ponytail_interface.connected is not None: if window_id == "" and isinstance(ponytail_interface.connected, int): LOGGING.debug("Keyboard event, monitor request, forcing monitor.") ponytail_interface.disconnect() sleep(1) ponytail_interface.connectMonitor() #try: # ponytail_interface.connectMonitor() #except Exception: # ponytail_interface.disconnect() # sleep(1) # ponytail_interface.connectMonitor() else: LOGGING.debug("Any window/monitor already connected for keyboard event.") else: LOGGING.debug("Mouse input event.") if self.ponytail_check_is_xwayland(window_id, window_list): window_id = "" if ponytail_interface.connected is None and window_id is None: # There is a corner case when using unit tests. window_was_connected = False for window in window_list: if bool(window["has-focus"]) is True: # ponytail_interface.disconnect() # sleep(1) ponytail_interface.connectWindow(window["id"]) window_was_connected = True LOGGING.debug(f"Connected active window '{window['id']}'.") # Attempt to connect monitor only in cases when window was not found. if not window_was_connected: LOGGING.debug("No active window, connecting monitor.") ponytail_interface.disconnect() sleep(1) ponytail_interface.connectMonitor() elif ponytail_interface.connected is not None and window_id is None: for window in window_list: if bool(window["has-focus"]) is True: if ponytail_interface.connected != window["id"]: ponytail_interface.disconnect() sleep(1) ponytail_interface.connectWindow(window["id"]) LOGGING.debug("Re-connected active window.") elif ponytail_interface.connected is None: if isinstance(window_id, int): # ponytail_interface.disconnect() # sleep(1) ponytail_interface.connectWindow(window_id) LOGGING.debug("Connected window by window_id.") elif isinstance(window_id, str): # ponytail_interface.disconnect() # sleep(1) ponytail_interface.connectMonitor(window_id) LOGGING.debug("Connected monitor (Xwayland?) by window_id.") elif ponytail_interface.connected != window_id and isinstance(window_id, int): LOGGING.debug("Disconnecting window.") ponytail_interface.disconnect() sleep(1.5) LOGGING.debug(f"Connected window: '{ponytail_interface.connected}'.") LOGGING.debug(f"Reconnecting window: '{window_id}'.") ponytail_interface.connectWindow(window_id) LOGGING.debug(f"Connected window: '{ponytail_interface.connected}'.") elif ponytail_interface.connected != window_id and isinstance(window_id, str): LOGGING.debug("Disconnecting monitor.") ponytail_interface.disconnect() sleep(1) LOGGING.debug("Reconnecting monitor.") ponytail_interface.connectMonitor() LOGGING.debug("Connected monitor.") elif ponytail_interface.connected == window_id: LOGGING.debug("Window or monitor is already connected.") LOGGING.debug(f"Working with window_id: '{ponytail_interface.connected}'.") def get_window_id(self, accessible_object): """ Get window_id. """ LOGGING.debug(" ===== Verbose window_id handling ===== ") window_id_return_value = "" if accessible_object.session_type == "x11": LOGGING.debug(f"Session type: '{accessible_object.session_type}'") return None # Save window_list to work through. ponytail_interface = self.get_ponytail_interface() window_list = ponytail_interface.window_list if len(window_list) == 0: LOGGING.debug("Window list was empty, doing another attempt.") do_delay(config.action_delay) window_list = ponytail_interface.window_list if self._window_id is None: LOGGING.debug("Window id event.") for window in window_list: if "title" not in window.keys(): window["title"] = "" node = accessible_object parent_list = [node] while node.parent is not None: parent_list.append(node.parent) node = node.parent for ancestor in parent_list: # Ancestors parent is None if ancestor.parent is None: window_list_node_with_focus = [x["id"] for x in window_list if bool(x["has-focus"])] self._window_id = window_list_node_with_focus[0] if window_list_node_with_focus else None window_id_return_value = self._window_id break # Ancestors parent is an application and the application is gnome-shell. if (ancestor.parent.role_name == "application"and ancestor.parent.name == "gnome-shell"): window_id_return_value = "" break # Ancestors parent is an application and ancestor is a window. if (ancestor.parent.role_name == "application" and ancestor.role_name == "window" and ancestor.name == ""): window_list_node_with_focus = [x["id"] for x in window_list if bool(x["has-focus"])] self._window_id = window_list_node_with_focus[0] if window_list_node_with_focus else None window_id_return_value = self._window_id break # Ancestors parent is an application and ancestors name is in the list. if ancestor.parent.role_name == "application" and ancestor.name in [x["title"] for x in window_list]: window_list_node_with_title = [x["id"] for x in window_list if x["title"] == ancestor.name] self._window_id = window_list_node_with_title[0] if window_list_node_with_title else None window_id_return_value = self._window_id break # Ancestors parent is an application. if ancestor.parent.roleName == "application": window_id_return_value = self._window_id break # If user provides a window that is not focused, refocus. elif self._window_id is not None and not self.get_window_has_focus(): LOGGING.debug("Attempt to do an action on unfocused window, refocusing.") window_list_node_with_focus = [x["id"] for x in window_list if bool(x["has-focus"])] self._window_id = window_list_node_with_focus[0] if window_list_node_with_focus else None window_id_return_value = self._window_id else: LOGGING.debug("Window ID provided.") window_id_return_value = self._window_id # Final check for focus on existing window. if window_id_return_value != "" and not self.get_window_has_focus(): LOGGING.debug(f"Attempting to use window_id '{window_id_return_value}'") LOGGING.debug("Window ID provided led to unfocused window, refocusing.") window_list_node_with_focus = [x["id"] for x in window_list if bool(x["has-focus"])] self._window_id = window_list_node_with_focus[0] if window_list_node_with_focus else None window_id_return_value = self._window_id LOGGING.debug(f"Window ID to use '{window_id_return_value}'") LOGGING.debug(" ===== End of window_id handling ====== ") # Do not cache the value. self._window_id = None return window_id_return_value def get_window_has_focus(self): """ Check if window is focused. """ #Get current window_id. window_id = self._window_id # Get window_list. ponytail_interface = self.get_ponytail_interface() window_list = ponytail_interface.window_list # Iterate through the window_list to see what window has focus. for window in window_list: if window["id"] == window_id and bool(window["has-focus"]) is True: return True return False ponytail_helper = PonytailHelper() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/predicate.py000066400000000000000000000247011520163156700247610ustar00rootroot00000000000000#!/usr/bin/python3 """ The predicate module for dogtail. """ # pylint: disable=broad-exception-caught # pylint: disable=wrong-import-position # pylint: disable=wrong-import-order # pylint: disable=import-error # pylint: disable=line-too-long # pylint: disable=no-name-in-module # ruff: noqa: E402 # ruff: noqa: E501 from dogtail.logging import logging_class LOGGER = logging_class.logger class GenericPredicate: """ Simple predicate class, providing easy way to compare nodes. """ def __init__(self, name=None, role_name=None, description=None, label=None, identifier=None, **kwargs): using_role_name = role_name # === Backward compatibility and extra parameters === allowed_kwargs = {"roleName"} for key, val in kwargs.items(): if "roleName" in str(key): using_role_name = val # Handle possible leftovers and raise TypeError on unexpected value. else: unexpected_arguments = ", ".join(set(kwargs) - allowed_kwargs) function_name = GenericPredicate.__init__.__name__ raise TypeError( f"{function_name}() got an unexpected keyword argument(s): {unexpected_arguments}" ) # Arguments were handled, do not pass them to another method. for allowed_argument in allowed_kwargs: kwargs.pop(allowed_argument, False) # =================================================== self.name = name self.role_name = using_role_name self.description = description self.label = label self.identifier = identifier self.showing_only = None self.satisfied_by_node = self._get_compare_function() def _get_compare_function(self): def predicate_satisfied_by_node(node): """ Simple comparison function that can be used in case of non-lambda search. :param node: Accessible object. :type node: Atspi.Accessible :return: True or False based on given criteria. :rtype: bool """ # First lets check if user wants to check label. if self.label is not None: # If so, check if labeler labeler is present. if node.labeler: # Try to match either text or name of the labeler. label_name = self.label == node.labeler.name label_text = self.label == node.labeler.text return label_name or label_text # No labeler, match is False. return False try: # Check if user wants to check name. if self.name is not None: # Compare name of self with the given node. if self.name != node.name: return False # Check if user wants to check role_name. if self.role_name is not None: # Compare role_name of self with the given node. # If 'push button' is wanted, attempt to match also 'button' if self.role_name != node.role_name: return False # Check if user wants to check description. if self.description is not None: # Compare description of self with the given node. if self.description != node.description: return False # Check if user wants to check identifier. if self.identifier is not None: # Compare identifier with the given node's "id" or "accessible_id". if self.identifier not in (node.id, node.accessible_id): return False # Check if user wants to match showing only. if self.showing_only: # Compare description of self with the given node. if not node.showing: return False # There are no conflicting parameters, match is a success. return True except Exception: return False return predicate_satisfied_by_node def __eq__(self, other): """ Predicates are considered equal if they are of the same subclass and have the same data. """ if not isinstance(self, type(other)): return False # Since the compare function will have different hexadecimal, # lets remove it from the comparison. # predicate_satisfied_by_node at 0x7fdb99f787c0> # predicate_satisfied_by_node at 0x7fdb96683060> exclude_key = {"satisfied_by_node"} self_dict = {key: self.__dict__[key] for key in set(list(self.__dict__.keys())) - set(exclude_key)} other_dict = {key: other.__dict__[key] for key in set(list(other.__dict__.keys())) - set(exclude_key)} return self_dict == other_dict def __str__(self): """ Representation of what the Predicate is matching. """ string_to_represent_object = "" string_to_represent_object += f"name:{repr(self.name)} " string_to_represent_object += f"role_name:{repr(self.role_name)} " string_to_represent_object += f"description:{repr(self.description)} " string_to_represent_object += f"label:{repr(self.label)} " string_to_represent_object += f"id:{repr(self.identifier)} " string_to_represent_object += f"showing_only:{repr(self.showing_only)}" return string_to_represent_object class IsAnApplicationNamed(GenericPredicate): """ Predicate subclass that looks for a application by name. """ def __init__(self, name=None): self.name = name if not self.name: LOGGER.info("You did not provide any 'name' to match.") self.name = "__invalid__" super().__init__(name=self.name, role_name="application") class IsATabNamed(GenericPredicate): """ Predicate subclass that looks for a tab by name. """ def __init__(self, name=None): self.name = name if not self.name: LOGGER.info("You did not provide any 'name' to match.") self.name = "__invalid__" super().__init__(name=self.name, role_name="tab") class IsAButtonNamed(GenericPredicate): """ Predicate subclass that looks for a button by name. """ def __init__(self, name=None): self.name = name if not self.name: LOGGER.info("You did not provide any 'name' to match.") self.name = "__invalid__" super().__init__(name=self.name, role_name="button") class IsAPushButtonNamed(GenericPredicate): """ Predicate subclass that looks for a push button by name. """ def __init__(self, name=None): self.name = name if not self.name: LOGGER.info("You did not provide any 'name' to match.") self.name = "__invalid__" super().__init__(name=self.name, role_name="push button") class IsATextEntryNamed(GenericPredicate): """ Predicate subclass that looks for a text entry by name. """ def __init__(self, name=None): self.name = name if not self.name: LOGGER.info("You did not provide any 'name' to match.") self.name = "__invalid__" super().__init__(name=self.name, role_name="text") class IsAMenuItemNamed(GenericPredicate): """ Predicate subclass that looks for a menu item by name. """ def __init__(self, name=None): self.name = name if not self.name: LOGGER.info("You did not provide any 'name' to match.") self.name = "__invalid__" super().__init__(name=self.name, role_name="menu item") class IsAMenuNamed(GenericPredicate): """ Predicate subclass that looks for a menu by name. """ def __init__(self, name=None): self.name = name if not self.name: LOGGER.info("You did not provide any 'name' to match.") self.name = "__invalid__" super().__init__(name=self.name, role_name="menu") class IsLabelledBy(GenericPredicate): """ Predicate: is this node labelled by another node. """ # I do not see a reason for this. # If user wants to know that something has a labeller, who not just ask <>.labeller? # This was empty in older version of dogtail, so what did it do? class IsLabelledAs(GenericPredicate): """ Predicate: is this node labelled with the text string (i.e. by another node with that as a name). """ def __init__(self, label=None): self.label = label if not self.label: LOGGER.info("You did not provide any 'label' to match.") self.label = "__invalid__" super().__init__(label=self.label) class IsADialogNamed(GenericPredicate): """ Predicate subclass that looks for a top-level dialog by name. """ def __init__(self, name=None): self.name = name if not self.name: LOGGER.info("You did not provide any 'name' to match.") self.name = "__invalid__" super().__init__(name=self.name, role_name="dialog") class IsAWindow(GenericPredicate): """ Predicate subclass that looks for top-level windows. """ def __init__(self): super().__init__(role_name="window") class IsAWindowNamed(GenericPredicate): """ Predicate subclass that looks for a top-level window by name. """ def __init__(self, name=None): self.name = name if not self.name: LOGGER.info("You did not provide any 'name' to match.") self.name = "__invalid__" super().__init__(name=self.name, role_name="window") class IsAFrameNamed(GenericPredicate): """ Predicate subclass that looks for a top-level frame by name. """ def __init__(self, name=None): self.name = name if not self.name: LOGGER.info("You did not provide any 'name' to match.") self.name = "__invalid__" super().__init__(name=self.name, role_name="frame") class IsNamed(GenericPredicate): """ Predicate subclass that looks simply by name. """ def __init__(self, name=None): self.name = name if not self.name: LOGGER.info("You did not provide any 'name' to match.") self.name = "__invalid__" super().__init__(name=self.name) dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/rawinput.py000066400000000000000000000664341520163156700247030ustar00rootroot00000000000000#!/usr/bin/python3 """ The raw input module for dogtail. """ # pylint: disable=broad-exception-caught # pylint: disable=wrong-import-position # pylint: disable=wrong-import-order # pylint: disable=import-error # pylint: disable=no-name-in-module # ruff: noqa: E402 # ruff: noqa: E501 from __future__ import absolute_import, division, print_function, unicode_literals import os import gi gi.require_version("Atspi", "2.0") gi.require_version("Gtk", "3.0") gi.require_version("Gdk", "3.0") from gi.repository import Gdk from gi.repository import Atspi import warnings warnings.filterwarnings("ignore", "g_object_unref") from dogtail.config import config from dogtail.utils import do_delay from dogtail.ponytail_helper import ponytail_helper # To preserve API from dogtail-1.x lets provide ponytail here as well. ponytail = ponytail_helper._ponytail_dbus_interface # pylint: disable=protected-access from dogtail.logging import logging_class LOGGING = logging_class.logger SESSION_TYPE = "x11" if "XDG_SESSION_TYPE" in os.environ and "wayland" in os.environ["XDG_SESSION_TYPE"]: SESSION_TYPE = "wayland" # There is a better way to do this since I am already rewriting it. def update_coords(coords): """ Lets fo this better that this. """ # Reserved for wrong coordinates reporting. return coords # Coordinate check for a negative value. def check_coordinates(x, y): """ Simple coordination check for positive coordinates. """ message = f"Attempting to generate a mouse event at negative coordinates: ({x},{y})" # Lets not check against 0. # There are shadows in effect that can go beyond the screen. # Its not unusual for coordinate to be around -10 to -30. if x < -50 or y < -50: raise ValueError(message) def do_typing_delay(): """ Execute a typing delay. """ do_delay(float(config.typing_delay)) def click(x, y, button=1, check=True, window_id=None): """ Synthesize a mouse button click at coordinates (x, y). """ LOGGING.debug(logging_class.get_func_params_and_values()) # TODO remove and do it better. (_x, _y) = update_coords((x, y)) # Check if the coordinates are not if check: check_coordinates(_x, _y) LOGGING.debug(f"Mouse button '{button}' click at ({_x}, {_y})") if SESSION_TYPE == "x11": # TODO Newton if condition. Atspi.generate_mouse_event(_x, _y, name=f"b{button}c") else: # ponytail_helper._ponytail_dbus_interface.disconnect() ponytail_helper.ponytail_check_connection(window_id) ponytail_interface = ponytail_helper.get_ponytail_interface() window_list = ponytail_interface.window_list debug_click = [(x["id"], x["has-focus"]) for x in window_list] LOGGING.debug(f"Ponytail click: '{debug_click}'") ponytail_interface.generateButtonEvent(button, _x, _y) do_delay(config.action_delay) def point(x, y, check=True, window_id=None): """ Synthesize a button point at (x,y) """ LOGGING.debug(logging_class.get_func_params_and_values()) # TODO remove and do it better. (_x, _y) = update_coords((x, y)) # Check if the coordinates are not if check: check_coordinates(_x, _y) LOGGING.debug(f"Mouse over at ({_x}, {_y})") if SESSION_TYPE == "x11": # TODO Newton if condition. Atspi.generate_mouse_event(_x, _y, name="abs") else: # ponytail_helper._ponytail_dbus_interface.disconnect() ponytail_helper.ponytail_check_connection(window_id) ponytail_interface = ponytail_helper.get_ponytail_interface() ponytail_interface.generateMotionEvent(_x, _y) do_delay(config.action_delay) def double_click(x, y, button=1, check=True, window_id=None): """ Synthesize a mouse button double-click at (x,y) """ LOGGING.debug(logging_class.get_func_params_and_values()) # TODO remove and do it better. (_x, _y) = update_coords((x, y)) # Check if the coordinates are not if check: check_coordinates(x, y) LOGGING.debug(f"Mouse button '{button}' double-click at ({_x}, {_y})") if SESSION_TYPE == "x11": # TODO Newton if condition. Atspi.generate_mouse_event(x, y, name=f"b{button}d") else: # ponytail_helper._ponytail_dbus_interface.disconnect() ponytail_helper.ponytail_check_connection(window_id) ponytail_interface = ponytail_helper.get_ponytail_interface() ponytail_interface.generateButtonEvent(button, _x, _y) do_delay(config.double_click_delay) # Prevent executing release event on closed application ponytail_interface.generateButtonPress(button) if ponytail_interface.connected: ponytail_interface.generateButtonRelease(button) else: # not ponytail.connected: # ponytail_interface.disconnect() ponytail_interface.connectMonitor() ponytail_interface.generateButtonRelease(button) do_delay(config.action_delay) def press(x, y, button=1, check=True, window_id=None, delay=None): """ Synthesize a mouse button press at (x,y) """ LOGGING.debug(logging_class.get_func_params_and_values()) # Check if user defined their own delay. if delay: delay_to_use = delay else: delay_to_use = config.default_delay # TODO remove and do it better. (_x, _y) = update_coords((x, y)) if check: check_coordinates(_x, _y) LOGGING.debug(f"Mouse button '{button}' press at ({_x}, {_y})") if SESSION_TYPE == "x11": # TODO Newton if condition. Atspi.generate_mouse_event(_x, _y, name=f"b{button}p") else: # ponytail_helper._ponytail_dbus_interface.disconnect() ponytail_helper.ponytail_check_connection(window_id) ponytail_interface = ponytail_helper.get_ponytail_interface() ponytail_interface.generateMotionEvent(_x, _y) ponytail_interface.generateButtonPress(button) do_delay(delay_to_use) def release(x, y, button=1, check=True, window_id=None): """ Synthesize a mouse button release at (x,y) """ LOGGING.debug(logging_class.get_func_params_and_values()) # TODO remove and do it better. (_x, _y) = update_coords((x, y)) if check: check_coordinates(_x, _y) LOGGING.debug(f"Mouse button '{button}' release at ({_x}, {_y})") if SESSION_TYPE == "x11": # TODO Newton if condition. Atspi.generate_mouse_event(_x, _y, name=f"b{button}r") else: # ponytail_helper._ponytail_dbus_interface.disconnect() ponytail_helper.ponytail_check_connection(window_id) ponytail_interface = ponytail_helper.get_ponytail_interface() ponytail_interface.generateMotionEvent(_x, _y) do_delay(config.action_delay) ponytail_interface.generateButtonRelease(button) do_delay(config.action_delay) def absolute_motion(x, y, mouse_delay=None, check=True, window_id=None): """ Synthesize mouse absolute motion to (x,y) """ LOGGING.debug(logging_class.get_func_params_and_values()) # Check if user defined their own delay. if mouse_delay: delay_to_use = mouse_delay else: delay_to_use = config.action_delay # TODO, remove and do it better. (_x, _y) = update_coords((x, y)) if check: check_coordinates(_x, _y) LOGGING.debug(f"Mouse absolute motion at ({_x}, {_y})") if SESSION_TYPE == "x11": # TODO Newton if condition. Atspi.generate_mouse_event(_x, _y, name="abs") else: ponytail_helper.ponytail_check_connection(window_id) ponytail_interface = ponytail_helper.get_ponytail_interface() ponytail_interface.generateMotionEvent(_x, _y) do_delay(delay_to_use) def absolute_motion_with_trajectory( source_x, source_y, destination_x, destination_y, mouse_delay=None, check=True, window_id=None ): """ Synthesize mouse absolute motion with trajectory. The 'trajectory' means that the whole motion is divided into several atomic movements which are Synthesize separately. """ LOGGING.debug(logging_class.get_func_params_and_values()) # Check if user defined their own delay. if mouse_delay: delay_to_use = mouse_delay else: delay_to_use = config.action_delay if check: check_coordinates(source_x, source_y) check_coordinates(destination_x, destination_y) LOGGING.debug(f"Mouse absolute motion with trajectory at ({destination_x}, {destination_y})") difference_x = float(destination_x - source_x) difference_y = float(destination_y - source_y) max_length = max(abs(difference_x), abs(difference_y)) if max_length == 0: return difference_x = difference_x / max_length difference_y = difference_y / max_length LOGGING.debug(f"Moving with difference of ({difference_x}, {difference_y})") act_x = float(source_x) act_y = float(source_y) for _ in range(0, int(max_length)): act_x += difference_x act_y += difference_y LOGGING.debug(f"Absolute motion with trajectory in progress ({act_x}, {act_y})") if mouse_delay: do_delay(mouse_delay) if SESSION_TYPE == "x11": # TODO Newton if condition. Atspi.generate_mouse_event(int(act_x), int(act_y), name="abs") else: ponytail_helper.ponytail_check_connection(window_id) ponytail_interface = ponytail_helper.get_ponytail_interface() ponytail_interface.generateMotionEvent(int(act_x), int(act_y)) do_delay(delay_to_use) def relative_motion(x, y, mouse_delay=None): """ Synthesize a relative motion from actual position. Note: Does not check if the end coordinates are positive. """ LOGGING.debug(logging_class.get_func_params_and_values()) LOGGING.debug(f"Mouse relative motion of ({x}, {y})") if SESSION_TYPE == "wayland": LOGGING.debug("Relative motion unavailable under wayland not available.") return # TODO Newton if condition. Atspi.generate_mouse_event(x, y, name="rel") if mouse_delay: do_delay(mouse_delay) else: do_delay(config.action_delay) def drag(fromXY, toXY, button=1, check=True): # pylint: disable=invalid-name """ Synthesize a mouse press, drag, and release on the screen. """ LOGGING.debug(logging_class.get_func_params_and_values()) LOGGING.debug(f"Mouse button '{button}' drag from '{fromXY}' to '{toXY}'") (_x, _y) = fromXY press(x=_x, y=_y, button=button, check=check) (_x, _y) = toXY absolute_motion(x=_x, y=_y, check=check) do_delay(config.action_delay) release(x=_x, y=_y, button=button, check=check) do_delay(config.action_delay) def drag_node_to_node(source_node, destination_node, button=1, check=True): """ Drag source_node onto dest_node. These are tree.Node objects. Takes positions of these Nodes directly, so you don't have to calculate end enter them directly. """ LOGGING.debug(logging_class.get_func_params_and_values()) LOGGING.debug(f"Dragging from '{str(source_node)}' to '{str(destination_node)}'.") LOGGING.debug(f"Dragging using button '{button}' and check '{check}'") x = int(source_node.position[0] + source_node.size[0] / 2) y = int(source_node.position[1] + source_node.size[1] / 2) press(x, y, button, check, source_node.window_id) x = int(destination_node.position[0] + destination_node.size[0] / 2) y = int(destination_node.position[1] + destination_node.size[1] / 2) release(x, y, button, check, window_id=destination_node.window_id) do_delay(config.action_delay) def drag_with_trajectory( # pylint: disable=invalid-name fromXY, toXY, button=1, check=True, press_delay=0.2, mouse_delay=0.01 ): """ Synthesize a mouse press, drag (including move events), and release on the screen Please note, that on Wayland, this function works for drags only within a single window. On X this function works with global coords and equals dragWithTrajectoryGlobal """ LOGGING.debug(logging_class.get_func_params_and_values()) # Check if user defined their own delay. if press_delay: delay_to_use = press_delay else: delay_to_use = config.default_delay LOGGING.debug(f"Dragging with trajectory from '{fromXY}' to '{toXY}'.") LOGGING.debug(f"Dragging using button '{button}' and check '{check}'") LOGGING.debug(f"Dragging using press delay '{delay_to_use}' and mouse_delay '{mouse_delay}'") (from_x, from_y) = fromXY press(x=from_x, y=from_y, button=button, check=check, delay=delay_to_use) (to_x, to_y) = toXY absolute_motion_with_trajectory( source_x=from_x, source_y=from_y, destination_x=to_x, destination_y=to_y, mouse_delay=mouse_delay, check=check ) do_delay(config.action_delay) release(x=to_x, y=to_y, button=button, check=check) do_delay(config.action_delay) def drag_with_trajectory_global(fromXY, toXY, button=1): # pylint: disable=invalid-name """ Synthesize a mouse press, drag (including move events), and release on the screen For use on Wayland - as this function forces using global coords, although we get local ones from a11y. So this function is targeted to be used for inter-window drags on Wayland, where you will need to quesstimate the coords. Which is doable i.e by having source window placed on the left *half* of the screen (local coords will be very similar to globals perhaps with the top panel offset). Having 'trajectory' appears to be necessary on Wayland for any drags. Use 'drag_with_trajectory' or just 'drag' on X sessions like in pre-wayland version of dogtail. """ LOGGING.debug(logging_class.get_func_params_and_values()) if SESSION_TYPE == "wayland": (from_x, from_y) = fromXY # ponytail_helper._ponytail_dbus_interface.disconnect() ponytail_helper.ponytail_check_connection(window_id="") ponytail_interface = ponytail_helper.get_ponytail_interface() ponytail_interface.generateMotionEvent(from_x, from_y) do_delay(config.action_delay) ponytail_interface.generateButtonPress(1) # We cannot do delay here or the behavior will change on long press. # do_delay(config.action_delay) (to_x, to_y) = toXY absolute_motion_with_trajectory( from_x, from_y, to_x, to_y, mouse_delay=0.01, window_id="" ) ponytail_interface.generateMotionEvent(to_x, to_y) do_delay(config.action_delay) ponytail_interface.generateButtonRelease(1) do_delay(config.action_delay) else: drag_with_trajectory(fromXY, toXY, button) # Section for Keyboard Events. KEY_NAME_ALIASES = { "enter": "Return", "esc": "Escape", "backspace": "BackSpace", "alt": "Alt_L", "control": "Control_L", "ctrl": "Control_L", "shift": "Shift_L", "del": "Delete", "ins": "Insert", "pageup": "Page_Up", "pagedown": "Page_Down", "win": "Super_L", "meta": "Super_L", "super": "Super_L", "tab": "Tab", "print": "Print", " ": "space", "\t": "Tab", "\n": "Return", "\b": "BackSpace" } def key_name_to_key_sym(key_name): """ Use GDK to get the key symbol for a key name. :param key_name: Key Name :type key_name: str """ LOGGING.debug(logging_class.get_func_params_and_values()) # Try to get the name from its alias. actual_key_name = KEY_NAME_ALIASES.get(key_name.lower(), key_name) LOGGING.debug(f"Key name after possible alias used '{actual_key_name}'.") # Get name vale from Gdk.keyval_from_name. actual_key_sym = Gdk.keyval_from_name(actual_key_name) LOGGING.debug(f"Key symbol value after Gdk.keyval_from_name '{actual_key_sym}'.") # If an error value is detected. if actual_key_sym in (0xffffff, 0x0, None): LOGGING.debug("Error value was detected '0xffffff, 0x0, None'.") try: LOGGING.debug("Attempt to get key value from Unicode representation of the key.") # Unicode code from a given character. unicode_key_value = ord(actual_key_name) actual_key_sym = Gdk.unicode_to_keyval(unicode_key_value) LOGGING.debug(f"Key symbol value from Gdk.unicode_to_keyval '{actual_key_sym}'.") # Not valid utf-8 character. except Exception as error : LOGGING.debug(f"Not valid character '{error}'.") try: LOGGING.debug("Attempt to match key name with Gdk.KEY_.") actual_key_sym = getattr(Gdk, f"KEY_{actual_key_sym}") except AttributeError as attribute_error: # Caught Attribute Error is in reality a Key Error of user. raise KeyError(key_name) from attribute_error return actual_key_sym def key_name_to_key_code(key_name): """ Use GDK to get the keycode for a given key string. Note that the keycode returned by this function is often incorrect when the requested key string is obtained by holding down the Shift key. """ LOGGING.debug(logging_class.get_func_params_and_values()) # Get Display. display = Gdk.Display().get_default() # Get Key Map key_map = Gdk.Keymap().get_for_display(display) # Get Key value. key_symbol_value = key_name_to_key_sym(key_name) entries = key_map.get_entries_for_keyval(key_symbol_value) try: return entries[1][0].keycode except TypeError: return key_name def press_key(key_name, window_id=None): """ Presses (and releases) the key specified by key_name. The key_name is the English name of the key as seen on the keyboard. Ex: 'enter' Names are looked up in Gdk.KEY_ If they are not found there, they are looked up by Gdk.unicode_to_keyval(key_name). """ LOGGING.debug(logging_class.get_func_params_and_values()) if key_name.lower() in ("esc", "escape", "enter", "return"): # When this would quit a window, release event would be doomed. window_id = "" key_symbol_value = key_name_to_key_sym(key_name) if SESSION_TYPE == "x11": Atspi.generate_keyboard_event(key_symbol_value, None, Atspi.KeySynthType.SYM) do_delay(float(config.typing_delay)) else: ponytail_helper.ponytail_check_connection(window_id, input_source="keyboard") ponytail_interface = ponytail_helper.get_ponytail_interface() use_delay = float(config.typing_delay) ponytail_interface.generateKeysymEvent(key_symbol_value, delay=use_delay) def hold_key(key_name): """ Press and hold the key specified by key_name. """ LOGGING.debug(logging_class.get_func_params_and_values()) key_code = key_name_to_key_code(key_name) if SESSION_TYPE == "x11": Atspi.generate_keyboard_event(key_code, None, Atspi.KeySynthType.PRESS) do_delay(float(config.typing_delay)) else: ponytail_helper.ponytail_check_connection(input_source="keyboard") ponytail_interface = ponytail_helper.get_ponytail_interface() ponytail_interface.generateKeycodePress(key_code) do_delay(config.action_delay) def release_key(key_name): """ Releases the held key specified by keyName. """ LOGGING.debug(logging_class.get_func_params_and_values()) LOGGING.debug(f"Attempting to release key of name '{key_name}'.") key_code = key_name_to_key_code(key_name) if SESSION_TYPE == "x11": Atspi.generate_keyboard_event(key_code, None, Atspi.KeySynthType.RELEASE) do_delay(float(config.typing_delay)) else: ponytail_helper.ponytail_check_connection(input_source="keyboard") ponytail_interface = ponytail_helper.get_ponytail_interface() ponytail_interface.generateKeycodePress(key_code) do_delay(config.action_delay) def type_text(string): """ Types the specified string, one character at a time. Please note, you may have to set a higher typing delay, if your machine misses/switches the characters typed. Needed sometimes on slow setups/VMs typing non-ASCII utf8 chars. """ LOGGING.debug(logging_class.get_func_params_and_values()) for character in string: press_key(character) def key_combo(combo_string): """ Generates the appropriate keyboard events to simulate a user pressing the specified key combination. The combo_string is the representation of the key combo to be generated. e.g. 'p' or 'PageUp' or 'q' The combo string does not need to have a final key. It can also be used as. '

' or '' or '' which might be more readable in some situations. """ LOGGING.debug(logging_class.get_func_params_and_values()) keys_to_press = [] # Remove left side lesser than sign. for key_partial_split in combo_string.split("<"): if key_partial_split: # Remove right side bigger than sign. for key_full_split in key_partial_split.split(">"): if key_full_split: possible_alias_use = KEY_NAME_ALIASES.get( key_full_split.lower(), # key key_full_split # default ) keys_to_press.append(possible_alias_use) # Check that the keys to be pressed are defined. for key_name in keys_to_press: if not hasattr(Gdk, key_name): if not hasattr(Gdk, f"KEY_{key_name}"): raise ValueError(f"Cannot find key '{key_name}'") # Get all keys leading up to the final key. modifiers = keys_to_press[:-1] # Get the final key to press. final_key = keys_to_press[-1] if SESSION_TYPE == "x11": # First press all modifiers leading up to the final key. for modifier in modifiers: # TODO figure out how to tell if Newton is to be used. key_code = key_name_to_key_code(modifier) Atspi.generate_keyboard_event(key_code, None, Atspi.KeySynthType.PRESS) # TODO figure out how to tell if Newton is to be used. final_key_code = key_name_to_key_code(final_key) Atspi.generate_keyboard_event(final_key_code, None, Atspi.KeySynthType.PRESSRELEASE) # Than release all modifiers after the final key was pressed and released. for modifier in modifiers: # TODO figure out how to tell if Newton is to be used. key_code = key_name_to_key_code(modifier) Atspi.generate_keyboard_event(key_code, None, Atspi.KeySynthType.RELEASE) else: #Always use monitor, window will often get closed # before final release i.e. with Alt-F4, Ctrl-Q etc! ponytail_helper.ponytail_check_connection(window_id="", input_source="keyboard") ponytail_interface = ponytail_helper.get_ponytail_interface() # First press all modifiers leading up to the final key. for modifier in modifiers: key_code = key_name_to_key_code(modifier) ponytail_interface.generateKeycodePress(key_code) key_code = key_name_to_key_code(final_key) ponytail_interface.generateKeycodeEvent(key_code) # Than release all modifiers after the final key was pressed and released. for modifier in modifiers: key_code = key_name_to_key_code(modifier) ponytail_interface.generateKeycodeRelease(key_code) do_delay(config.action_delay) ######################################## # Backwards compatibility definitions. # ######################################## def doubleClick(x, y, button=1, check=True, window_id=None): # pylint: disable=invalid-name """ Backwards compatibility wrapper over double_click. """ double_click(x=x, y=y, button=button, check=check, window_id=window_id) def doTypingDelay(): # pylint: disable=invalid-name """ Backwards compatibility wrapper over do_typing_delay. """ do_typing_delay() def absoluteMotion(x, y, mouseDelay=None, check=True, window_id=None): # pylint: disable=invalid-name """ Backwards compatibility wrapper over absolute_motion. """ absolute_motion(x=x, y=y, mouse_delay=mouseDelay, check=check, window_id=window_id) def absoluteMotionWithTrajectory( # pylint: disable=invalid-name source_x, source_y, dest_x, dest_y, mouseDelay=None, check=True, window_id=None ): """ Backwards compatibility wrapper over absolute_motion_with_trajectory. """ absolute_motion_with_trajectory( source_x=source_x, source_y=source_y, destination_x=dest_x, destination_y=dest_y, mouse_delay=mouseDelay, check=check, window_id=window_id ) def relativeMotion(x, y, mouseDelay=None): # pylint: disable=invalid-name """ Backwards compatibility wrapper over relative_motion. """ relative_motion(x=x, y=y, mouse_delay=mouseDelay) def dragNodeToNode(source_node, dest_node, button=1, check=True): # pylint: disable=invalid-name """ Backwards compatibility wrapper over drag_node_to_node. """ drag_node_to_node( source_node=source_node, destination_node=dest_node, button=button, check=check ) def dragWithTrajectory( # pylint: disable=invalid-name fromXY, toXY, button=1, check=True, press_delay=0.5, mouse_delay=0.01 ): """ Backwards compatibility wrapper over drag_with_trajectory. """ drag_with_trajectory( fromXY=fromXY, toXY=toXY, button=button, check=check, press_delay=press_delay, mouse_delay=mouse_delay ) def dragWithTrajectoryGlobal(fromXY, toXY, button=1): # pylint: disable=invalid-name """ Backwards compatibility wrapper over drag_with_trajectory_global """ drag_with_trajectory_global(fromXY=fromXY, toXY=toXY, button=button) def pressKey(keyName, window_id=None): # pylint: disable=invalid-name """ Backwards compatibility wrapper over press_key. """ press_key(key_name=keyName, window_id=window_id) def typeText(string): # pylint: disable=invalid-name """ Backwards compatibility wrapper over type_text. """ type_text(string=string) def keyNameToKeyCode(keyName): # pylint: disable=invalid-name """ Backwards compatibility wrapper over key_name_to_key_code. """ return key_name_to_key_code(key_name=keyName) def holdKey(keyName): # pylint: disable=invalid-name """ Backwards compatibility wrapper over hold_key. """ hold_key(key_name=keyName) def releaseKey(keyName): # pylint: disable=invalid-name """ Backwards compatibility wrapper over release_key. """ release_key(key_name=keyName) def keyCombo(comboString): # pylint: disable=invalid-name """ Backwards compatibility wrapper over key_combo. """ key_combo(combo_string=comboString) # To keep backwards compatibility and allow users to import doDelay from here. def doDelay(delay=None): # pylint: disable=invalid-name """ Do Delay. """ do_delay(delay=delay) ############################################### # End of backwards compatibility definitions. # ############################################### dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/scripts/000077500000000000000000000000001520163156700241325ustar00rootroot00000000000000dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/scripts/__init__.py000066400000000000000000000000001520163156700262310ustar00rootroot00000000000000dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/scripts/dogtail_create_config.py000066400000000000000000000020601520163156700307750ustar00rootroot00000000000000#!/usr/bin/env python3 """ Create configuration in your current directory. """ # pylint: disable=invalid-name import os import sys from dogtail.config import CONFIG_SCHEMA as schema def main(): directory = os.path.realpath(os.getcwd()) configuration = "" for section in schema.keys(): configuration += f"[{section}]\n" for key, scheme_value in schema[section].items(): _, raw_value = scheme_value configuration += f"{key} = {raw_value}\n" try: if os.path.isfile("dogtail_config.ini"): print("Configuration file already exists.") sys.exit(0) with open(directory + "/dogtail_config.ini", "w", encoding="utf-8") as _file: _file.write(configuration) print(" ".join(( "File was successfully created.", os.path.abspath(os.path.join(directory, "dogtail_config.ini")), ))) except IOError as error: raise IOError("File creation of 'dogtail_config.ini' failed.") from error if __name__ == "__main__": main() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/scripts/dogtail_get_config.py000066400000000000000000000023731520163156700303200ustar00rootroot00000000000000#!/usr/bin/env python3 """ Print default configuration. """ from dogtail.config import config, CONFIG_SCHEMA def main(): # Print the retrieved values print("Configuration.") expected_types = {} header_options = "Config Options" header_value = "Value" header_type = "Acceptable Types" max_string_options_length = len(header_options) max_string_value_length = len(header_value) for section, fields in CONFIG_SCHEMA.items(): for key, (expected_type, default_value) in fields.items(): expected_types[key] = (expected_type, default_value) for key, value in config.validated_config.items(): max_string_options_length = max(max_string_options_length, len(key)) max_string_value_length = max(max_string_value_length, len(str(value))) print(f"{header_options:<{max_string_options_length+2}} {header_value:<{max_string_value_length+1}} {header_type}") for key, value in config.validated_config.items(): expected_type = "object" if key in expected_types.keys(): expected_type = str(expected_types[key][0]) print(f"{key:<{max_string_options_length + 1}}: {str(value):<{max_string_value_length + 1}} '{expected_type}'") if __name__ == "__main__": main() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/scripts/dogtail_headless.py000077500000000000000000002071051520163156700300070ustar00rootroot00000000000000#!/usr/bin/env python3 """ A dogtail "headless" script that takes care of starting the session in wanted configuration. """ # ruff: noqa: E501 # pylint: disable=broad-exception-caught # pylint: disable=import-outside-toplevel # pylint: disable=line-too-long import os import errno import signal import subprocess import traceback from subprocess import check_output, STDOUT, CalledProcessError import time import argparse import configparser import shutil import sys import re ENVIRONMENT_VARIABLES_TO_PRESERVE = [ "PYTHONPATH", "TEST", "TERM", ] def print_info(string_to_print) -> None: """ Print string. :param string_to_print: String to print coloured based on setup. :type string_to_print: str """ print("headless: " + str(string_to_print)) def run(command) -> str: """ Utility function to execute given command and return its output. """ try: output = check_output(command, shell=True, env=os.environ, stderr=STDOUT, encoding="utf-8") return output.strip("\n") except CalledProcessError as error: return error.output def run_verbose(command) -> tuple: """ Utility function to execute given command and return its output. """ try: output = check_output(command, shell=True, env=os.environ, stderr=STDOUT, encoding="utf-8") return (output.strip("\n"), 0, None) except CalledProcessError as error: return (error.output, error.returncode, error) def get_initial_environment_dictionary(binary="gnome-session-binary", term=None) -> dict: """ Lets assume initial state of the machine is with already running display manager. We need to get environment variables from the session to check correct configuration. Print no errors since this will fail when session is not running. Returns environment as dictionary. """ return get_environment_dictionary(binary=binary, verbose=False, term=term) def get_environment_dictionary(binary="gnome-session-binary", verbose=True, term=None) -> dict: """ Targeting the gnome-session-binary process to get all environment variables. Returns environment as dictionary. """ environment_dictionary = {} user_id = str(os.geteuid()) gnome_session_binary_pid = None try: gnome_session_binary_pid = run(f"pgrep -fAu {user_id} {binary}").split("\n")[0] except CalledProcessError: if verbose: print_info(f"Failed to retrieve {binary} pid from pgrep:") print_info(f"\n{traceback.format_exc()}") # Use a given binary pid to get its environment variables. if gnome_session_binary_pid: environment_process_path = f"/proc/{gnome_session_binary_pid}/environ" # Verify that the environ file can be opened and load environment variables to dictionary. try: with open(environment_process_path, "r", encoding="utf-8") as environ_file: for item in environ_file.read().split("\x00"): if "=" in item: key, value = item.split("=", 1) environment_dictionary[key] = value except IOError as error: if verbose: print_info(f"Environment file manipulation failed on: '{error}'") # Preserving wanted environment variables. for environment_variable in ENVIRONMENT_VARIABLES_TO_PRESERVE: if environment_variable in os.environ: environment_dictionary[environment_variable] = os.environ[environment_variable] # Check if the TERM variable is set. try: current_term_variable = os.environ["TERM"] except KeyError: current_term_variable = None # If the user sets the TERM variable, overwrite it, no matter what was there. if term is not None: environment_dictionary["TERM"] = term print_info(f"Setting environment variable TERM as '{term}'") # If the variable is not set (should not happen, legacy issue for us) set it. elif not current_term_variable: environment_dictionary["TERM"] = "xterm-256color" print_info("Setting environment variable TERM as 'xterm-256color'") # Keep current variable in all other cases. else: environment_dictionary["TERM"] = current_term_variable if "XAUTHORITY" not in environment_dictionary: xwaylandauth = "" run_user_dir = os.path.abspath(f"/run/user/{user_id}/") # Attempt to prevent race conditions, give it at most 5 seconds to appear. for _ in range(5): # Iterate over the files. for _file in os.scandir(run_user_dir): # Check for the file we need. if _file.is_file() and "Xwaylandauth" in _file.path or "xauth" in _file.path: # Set the file to the variable. xwaylandauth = _file.path break # We only care about xauthority variable in non-initial call. if xwaylandauth == "" and verbose: print_info("Environment variable XAUTHORITY is unavailable, retrying.") time.sleep(1) if verbose: print_info(f"Setting environment variable XAUTHORITY as '{xwaylandauth}'") environment_dictionary["XAUTHORITY"] = xwaylandauth return environment_dictionary if verbose: print_info("Environment file not found, most likely caused by display manager not running.") return environment_dictionary def is_binary_existing_and_executable(path) -> bool: """ Test if given binary file exists. """ if ( path.startswith(os.path.sep) or path.startswith(os.path.join(".", "")) or path.startswith(os.path.join("..", "")) ): if not os.path.exists(path): raise IOError(errno.ENOENT, "No such file", path) if not os.access(path, os.X_OK): raise IOError(errno.ENOEXEC, "Permission denied", path) return True def verify_file_ownership() -> None: """ Verify ownership of the dconf file. """ user_id = os.geteuid() file_owner_command = f"sudo stat -c '%U %G' /run/user/{user_id}/dconf/user" if not os.path.isfile(f"/run/user/{user_id}/dconf/user"): return file_owner_command_result = run_verbose(file_owner_command) if file_owner_command_result[1] == 0 and "root" in file_owner_command_result[0]: print_info("Attempting to restore dconf file ownership.") run(f"sudo rm -rf /run/user/{user_id}/dconf/user") elif file_owner_command_result[1] != 0: print_info("Issue was detected and might need attention") print_info(f"\n{file_owner_command_result}") ### def parse(): """ Parser for arguments given to the script. :return: Namespace object with attributes parsed out of the command line. :rtype: Namespace """ parser = argparse.ArgumentParser( prog="$ dogtail-headless", description="Adjusted headless script.", formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( "script", nargs="?", default="bash", help="Script to be executed, if not provided 'bash' will be used.", ) parser.add_argument( "--session", required=False, help="\n".join(( "What session to use (e.g. 'gnome', 'kde-plasma', 'mate').", "Comes from /usr/share/xsessions/ or /usr/share/wayland-sessions/." )) ) parser.add_argument("--session-binary", required=False, help="Full path to an in-session binary (e.g. '/usr/bin/gnome-shell')." ) parser.add_argument("--dm", required=False, help="\n".join(( "What display manager to use for spawning session.", "Supported: 'gdm' (default), 'sddm', or 'lightdm'." )) ) parser.add_argument( "--session-type", required=False, choices=("xorg", "wayland"), help="Choose which session type will be used.", ) parser.add_argument( "--session-desktop", required=False, help="Choose which session desktop will be used.", ) parser.add_argument( "--display", required=False, help="Number of the DISPLAY to connect to - default value is ':0'", ) parser.add_argument( "--dont-start", required=False, action="store_true", help="Use the system as is. Does not have to be under display manager", ) parser.add_argument( "--dont-kill", required=False, action="store_true", help="Do not kill the session when script exits.", ) parser.add_argument( "--restart", required=False, action="store_true", help="Restart previously running display manager session before script execution.", ) parser.add_argument( "--keep", required=False, help="Number of tests to keep display manager running." ) parser.add_argument( "--keep-max", required=False, action="store_true", help="Keep display manager running 'maximum number of tests' times. This equals --dont-kill parameter.", ) parser.add_argument( "--disable-a11y", required=False, action="store_true", help="Disable accessibility technologies on script (not session) exit.", ) parser.add_argument( "--force", required=False, action="store_true", help="Will check if the configuration was setup correctly. Exit upon fail.", ) parser.add_argument( "--debug", required=False, action="store_true", help="Will enable debugging of dogtail.", ) parser.add_argument( "--no-color", required=False, action="store_true", help="Do not use colors.", ) parser.add_argument( "--allow-duplication", required=False, action="store_true", help="Allow nested/duplicated headless script.", ) parser.add_argument( "--no-autologin", required=False, action="store_true", help=argparse.SUPPRESS, ) parser.add_argument( "--virtual-monitors", choices=("1", "2"), required=False, help="EXPERIMENTAL: Choose how many virtual monitors to use.", ) parser.add_argument( "--term", required=False, help="Set TERM variable.", ) return parser.parse_args() class DisplayManager: """ Display Manager class. """ # Display manager presets. gdm_options = { "config": "/etc/gdm/custom.conf", "section": "daemon", "enable": "AutomaticLoginEnable", "user": "AutomaticLogin" } sddm_options = { "config": "/etc/sddm.conf", "section": "Autologin", "relogin": "Relogin", "session": "Session", "user": "User" } lightdm_options = { "config": "/etc/lightdm/lightdm.conf", "section": "Seat:*", "user": "autologin-user", "timeout": "autologin-user-timeout", "session": "autologin-session" } def __init__( self, session="gnome", session_binary="/usr/bin/gnome-shell", display_manager="gdm", session_type=None, session_desktop=None, enable_start=True, enable_stop=True, display_manager_restart=False, no_automatic_login=False, virtual_monitors=0, ) -> None: self.enable_start = enable_start self.enable_stop = enable_stop self.display_manager_restart = display_manager_restart self.no_automatic_login = no_automatic_login self.virtual_monitors=virtual_monitors self.session = session self.session_binary = session_binary self.display_manager = display_manager # Session type is xorg, wayland, None -> respect setting of the system. self.session_type = session_type # Session desktop is gnome gnome-classic, None -> respect setting of the system. self.session_desktop = session_desktop if self.display_manager == "gdm": self.options = self.gdm_options elif self.display_manager == "sddm": self.options = self.sddm_options elif self.display_manager == "lightdm": self.options = self.lightdm_options else: raise ValueError(f"Unsupported display manager: {self.display_manager}") self.session_started_indicator = self.session_binary self.user = run("whoami") self.user_id = run(f"id -u {self.user}") self.config_file = self.options["config"] # e.g. /etc/gdm/custom.conf self.temporary_config_file = f"/tmp/{os.path.basename(self.config_file)}" # Debugging a11y randomly turning off. self.debug_a11y_file = "/usr/lib/systemd/user/org.gnome.SettingsDaemon.A11ySettings.service" self.temporary_debug_a11y_file = f"/tmp/{os.path.basename(self.debug_a11y_file)}" # GNOME Shell unsafe mode. self.unsafe_mode_config_file_shell = "/usr/lib/systemd/user/org.gnome.Shell@wayland.service" self.unsafe_mode_config_file_shell_etc = "/etc/systemd/user/org.gnome.Shell@wayland.service" self.temporary_unsafe_mode_config_file_shell = f"/tmp/{os.path.basename(self.unsafe_mode_config_file_shell)}" # GNOME Kiosk unsafe mode. self.unsafe_mode_config_file_kiosk = "/usr/lib/systemd/user/org.gnome.Kiosk@wayland.service" self.unsafe_mode_config_file_kiosk_etc = "/etc/systemd/user/org.gnome.Kiosk@wayland.service" self.temporary_unsafe_mode_config_file_kiosk = f"/tmp/{os.path.basename(self.unsafe_mode_config_file_kiosk)}" self.virtual_monitor_config_file = self.unsafe_mode_config_file_shell self.temporary_virtual_monitor_config_file = f"/tmp/{os.path.basename(self.virtual_monitor_config_file)}" self.session_restart_required = False def restore_config(self) -> None: """ Restore configuration file. Not used, but implemented if needed. """ shutil.copy(self.config_file, self.temporary_config_file) config_parser = configparser.ConfigParser() config_parser.optionxform = str config_parser.read(self.temporary_config_file) config_parser.remove_option(self.options["section"], self.options["enable"]) config_parser.remove_option(self.options["section"], self.options["user"]) config_parser.remove_option(self.options["section"], self.options["timeout"]) config_parser.remove_option(self.options["section"], self.options["session"]) config_parser.remove_option(self.options["section"], self.options["relogin"]) config_parser.remove_option(self.options["section"], "WaylandEnable") with open(self.temporary_config_file, "w", encoding="utf-8") as _file: config_parser.write(_file) def handling_config_setup(self) -> None: """ Handling config setup """ run(f"cp -f {self.config_file} {self.temporary_config_file}") config_parser = configparser.ConfigParser() # Default option returns lower-case, setting this will make it case sensitive. config_parser.optionxform = str try: config_parser.read(self.temporary_config_file) except configparser.Error: # Adding this to its own variable as f-strings do not handle nested quotes well. options_config = self.options["config"] print_info(f"Unable to parse '{options_config}'.") print(str(traceback.format_exc())) print_info(f"Content of '{options_config}' file:") print(run(f"cat {self.temporary_config_file}") + "\n") print_info("Exiting headless.") sys.exit(1) # Section does not exist. if not config_parser.has_section(self.options["section"]): config_parser.add_section(self.options["section"]) if self.no_automatic_login: print_info("AutomaticLogin and AutomaticLoginEnable disabled.") print_info("This is for gnome-initial-setup automation only.") print_info("Not setting this in any other case will cause issues.") config_parser.remove_option(self.options["section"], self.options["enable"]) config_parser.remove_option(self.options["section"], self.options["user"]) time.sleep(1) else: if self.display_manager == "gdm": config_parser.set(self.options["section"], self.options["enable"], "true") config_parser.set(self.options["section"], self.options["user"], self.user) elif self.display_manager == "sddm": config_parser.set(self.options["section"], self.options["relogin"], "false") config_parser.set(self.options["section"], self.options["session"], "plasma") config_parser.set(self.options["section"], self.options["user"], self.user) else: # lightdm config_parser.set(self.options["section"], self.options["user"], self.user) config_parser.set(self.options["section"], self.options["timeout"], "0") if self.session: config_parser.set(self.options["section"], self.options["session"], self.session) if self.session_type == "xorg": config_parser.set(self.options["section"], "WaylandEnable", "false") elif self.session_type == "wayland": config_parser.set(self.options["section"], "WaylandEnable", "true") # Respecting system setting, get the session that is to be started. elif self.session_type is None: if "WaylandEnable" in config_parser.options(self.options["section"]): # Set xorg only if there is WaylandEnable=false. # Wayland should be default everywhere. if config_parser.get(self.options["section"], "WaylandEnable") == "false": self.session_type = "xorg" else: self.session_type = "wayland" # "WaylandEnable" not in config_parser.options("daemon"). else: self.session_type = "wayland" config_parser.set(self.options["section"], "WaylandEnable", "true") else: print_info("This is not acceptable session type. Fallback to the 'xorg' session type") print_info("Acceptable names for --session-type: ['xorg', 'wayland']") self.session_type = "xorg" config_parser.set(self.options["section"], "WaylandEnable", "false") with open(self.temporary_config_file, "w", encoding="utf-8") as _file: config_parser.write(_file) if not os.path.isfile(self.temporary_config_file): print_info("Temporary config file was not found, waiting a bit...") time.sleep(1) run(f"sudo mv -f {self.temporary_config_file} {self.config_file}") run(f"sudo rm -f {self.temporary_config_file}") # If there is a session running. # Verify that the expected configuration matches. # If wayland is expected from custom.conf and xorg is detected, restart the session. currently_running_type = None try: if os.environ["XDG_SESSION_TYPE"] == "x11": currently_running_type = "xorg" else: currently_running_type = os.environ["XDG_SESSION_TYPE"] except KeyError: currently_running_type = None # Save bool value signifying running session. session_is_currently_running = currently_running_type in ("xorg", "wayland") # Check only if session is currently running. # None signifies that the session is not running. if session_is_currently_running and (currently_running_type != self.session_type): self.session_restart_required = True def handling_account_setup(self) -> None: """ Handling account setup """ # Get all defined desktop file name for xorg and wayland. acceptable_x_desktop_names = run("ls /usr/share/xsessions").split("\n") acceptable_wayland_desktop_names = run("ls /usr/share/wayland-sessions").split("\n") acceptable_desktop_file_names = None # Get acceptable desktop file names for xorg. if self.session_type == "xorg": acceptable_desktop_file_names = [ x.replace(".desktop", "") for x in acceptable_x_desktop_names if x ] # Get acceptable desktop file names for wayland. elif self.session_type == "wayland": acceptable_desktop_file_names = [ x.replace(".desktop", "") for x in acceptable_wayland_desktop_names if x ] # Get initial values that we work with. interface = "".join(( "org.freedesktop.Accounts ", f"/org/freedesktop/Accounts/User{self.user_id} ", "org.freedesktop.Accounts.User", )) saved_session_desktop = run(f"busctl get-property {interface} Session") saved_x_session_desktop = run(f"busctl get-property {interface} XSession") # Handling result from get-property. Making sure they are equal. saved_session_desktop = saved_session_desktop[3:-1] saved_x_session_desktop = saved_x_session_desktop[3:-1] if saved_session_desktop == "": run(f"busctl call {interface} SetSession 's' '{saved_x_session_desktop}'") saved_session_desktop = saved_x_session_desktop elif saved_x_session_desktop == "": run(f"busctl call {interface} SetXSession 's' '{saved_session_desktop}'") saved_x_session_desktop = saved_session_desktop # Chosen desktop differs from current one. if ( self.session_desktop not in (saved_session_desktop, None) and self.session_desktop in acceptable_desktop_file_names ): print_info(f"Changing desktop '{saved_session_desktop}' -> '{self.session_desktop}'") run(f"busctl call {interface} SetSession 's' '{self.session_desktop}'") run(f"busctl call {interface} SetXSession 's' '{self.session_desktop}'") self.session_restart_required = True # Choosing desktop not found in acceptable desktop file names. elif ( self.session_desktop is not None and self.session_desktop not in acceptable_desktop_file_names ): print_info("This is not acceptable session desktop name. Fallback to the 'gnome' session desktop") print_info(f"Acceptable names for '{self.session_type}': {acceptable_desktop_file_names}") run(f"busctl call {interface} SetSession 's' 'gnome'") run(f"busctl call {interface} SetXSession 's' 'gnome'") self.session_restart_required = True def handling_debug_accessibility_setup(self) -> None: """ Handling debug accessibility setup. """ # If there is no debug file do not attempt to set it. if not os.path.isfile(self.debug_a11y_file): return run(f"cp -f {self.debug_a11y_file} {self.temporary_debug_a11y_file}") config_parser = configparser.ConfigParser() # Default option returns lower-case, setting this will make it case sensitive. config_parser.optionxform = str config_parser.read(self.temporary_debug_a11y_file) if not config_parser.has_section("Service"): config_parser.add_section("Service") config_parser.set("Service", "Environment", '"G_MESSAGES_DEBUG=a11y-settings-plugin"') # Write the data to temporary file. with open(self.temporary_debug_a11y_file, "w", encoding="utf-8") as _file: config_parser.write(_file) if not os.path.isfile(self.temporary_debug_a11y_file): print_info("Temporary config file was not found, waiting a bit...") time.sleep(1) # Moving the file to its destination and removing the temporary one. run(f"sudo mv -f {self.temporary_debug_a11y_file} {self.debug_a11y_file}") run(f"sudo rm -f {self.temporary_debug_a11y_file}") print_info("Enabling G_MESSAGES_DEBUG for accessibility.") def handling_unsafe_mode_setup(self, use_unsafe_mode=True, desktop="gnome-shell") -> None: """ Handling unsafe mode setup. :param use_unsafe_mode: Using unsafe mode, defaults to True. :type use_unsafe_mode: bool, optional """ print_info("Handling --unsafe-mode.") if desktop not in ("gnome-shell", "gnome-kiosk"): print_info("Unknown desktop, unable to set unsafe mode.") return unsafe_config = None unsafe_config_etc = None unsafe_temporary_config = None if desktop == "gnome-shell": unsafe_config = self.unsafe_mode_config_file_shell unsafe_config_etc = self.unsafe_mode_config_file_shell_etc unsafe_temporary_config = self.temporary_unsafe_mode_config_file_shell if desktop == "gnome-kiosk": unsafe_config = self.unsafe_mode_config_file_kiosk unsafe_config_etc = self.unsafe_mode_config_file_kiosk_etc unsafe_temporary_config = self.temporary_unsafe_mode_config_file_kiosk # No point of checking unsafe mode of gnome-shell if there is no gnome-shell. #if self.session != "gnome": # print_info(f"GNOME not detected at this point: session is '{self.session}'") # return # If the gnome-shell or kiosk has no unsafe mode, do not use it. unsafe_mode_present = run_verbose(f"/usr/bin/{desktop} --unsafe-mode --help") unsafe_mode_present_output = unsafe_mode_present[0].strip("\n") unsafe_mode_present_return_code = unsafe_mode_present[1] if unsafe_mode_present_return_code != 0: print_info(f"Unsafe mode available?: '{unsafe_mode_present_output}'") return # Handling unsafe mode setup. if not os.path.isfile(unsafe_config): print_info("Defined unsafe mode config file not detected.") # File is not present on rhel-8 so it will not be executed. return if not unsafe_config or not unsafe_temporary_config: print_info(f"Nothing to copy: '{unsafe_config}', '{unsafe_temporary_config}'") return run(f"cp -f {unsafe_config} {unsafe_temporary_config}") config_parser = configparser.ConfigParser() config_parser.optionxform = str config_parser.read(unsafe_temporary_config) if ( use_unsafe_mode and f"/usr/bin/{desktop} --unsafe-mode" not in config_parser.get("Service", "ExecStart") ): print_info(f"Using {desktop} --unsafe-mode under Wayland.") config_parser.set("Service", "ExecStart", f"/usr/bin/{desktop} --unsafe-mode") self.session_restart_required = True elif ( not use_unsafe_mode and config_parser.get("Service", "ExecStart") != f"/usr/bin/{desktop}" ): config_parser.set("Service", "ExecStart", f"/usr/bin/{desktop}") # No change required. else: run(f"sudo rm -f {unsafe_temporary_config}") return with open(unsafe_temporary_config, "w", encoding="utf-8") as _file: config_parser.write(_file) move_service = run_verbose(f"sudo mv -f {unsafe_temporary_config} {unsafe_config}") # Check the return code. if move_service[1] != 0: print_info(move_service[2]) print_info("Most likely running in Image Mode.") print_info("Using /etc/systemd/user/ directory to set --unsafe-mode.") move_service = run_verbose(f"sudo mv -f {unsafe_temporary_config} {unsafe_config_etc}") run_verbose(f"sudo rm -f {unsafe_temporary_config}") def handling_virtual_monitors(self) -> None: """ Handling virtual monitors. """ if not os.path.isfile(self.virtual_monitor_config_file): # End quietly in systems it is not usable. return if not os.path.isfile(self.virtual_monitor_config_file) and self.virtual_monitors: # End with info message in systems it is not usable and used. print_info("Virtual Monitors feature unavailable.") return run(f"cp -f {self.virtual_monitor_config_file} {self.temporary_virtual_monitor_config_file}") config_parser = configparser.ConfigParser() config_parser.optionxform = str config_parser.read(self.temporary_virtual_monitor_config_file) # Add a virtual monitor with default size, configurable in session. virtual_monitor_string = " --virtual-monitor 1024x768" service_exec = config_parser.get("Service", "ExecStart") lm_occurrences = service_exec.count(virtual_monitor_string) # If there are occurrences found and user wants zero. Remove all virtual monitors. if lm_occurrences > 0 and self.virtual_monitors == 0: print_info("Removing virtual monitors.") config_parser.set( "Service", "ExecStart", service_exec.replace(virtual_monitor_string, "") ) self.session_restart_required = True elif lm_occurrences != self.virtual_monitors: print_info(f"Adding {self.virtual_monitors} virtual monitor/s with resolution 1024x768.") config_parser.set( "Service", "ExecStart", service_exec.replace(virtual_monitor_string, "") + virtual_monitor_string * self.virtual_monitors ) self.session_restart_required = True # No change required. else: run(f"sudo rm -f {self.temporary_virtual_monitor_config_file}") return with open(self.temporary_virtual_monitor_config_file, "w", encoding="utf-8") as _file: config_parser.write(_file) run(f"sudo mv -f {self.temporary_virtual_monitor_config_file} {self.virtual_monitor_config_file}") run(f"sudo rm -f {self.temporary_virtual_monitor_config_file}") def start_display_manager(self) -> None: """ Starting the display manager. """ # Stop display manager only if requested by user or required by configuration change. # Continue using the running session otherwise. if self.display_manager_restart or self.session_restart_required: print_info("Restart required.") # Stopping display manager to reload session type. self.stop_display_manager() # A little delay to prevent races. time.sleep(1) list_of_systemd_processes = ( run(f"pgrep -u {os.geteuid()} -f '/usr/lib/systemd/systemd --user'") .strip("\n") .split("\n") ) number_of_systemd_processes = len(list_of_systemd_processes) if number_of_systemd_processes > 1: print_info("Multiple instances of '/usr/lib/systemd/systemd --user' detected - trying to recover...") # Stop the running session first - important for 'keep' option. self.stop_display_manager() for pid in list_of_systemd_processes: run(f"sudo kill -9 {pid}") # Prevent any race when cleaning systemd processes and starting display manager. time.sleep(1) # Example loginctl show-user 1000 --property=Sessions with run_verbose. # ("Sessions=X Y", , ) loginctl_command = f"loginctl show-user {self.user} --property=Sessions" loginctl_show_user_sessions_verbose = run_verbose(loginctl_command) # If the command return code is 0 we can assume it has the data we need. if loginctl_show_user_sessions_verbose[1] == 0: # Get the actual command output. loginctl_show_user_sessions_output = loginctl_show_user_sessions_verbose[0] # "Sessions=X Y" -> ["Sessions", "X Y"] sessions_list = loginctl_show_user_sessions_output.split("=", 1) # Initiating the session number list. session_numbers_list = [] # Get the data only if it is in expected format. if len(sessions_list) == 2: # ["Sessions", "X Y"] -> "X Y" session_numbers_string = sessions_list[1] # "X Y" -> ["X", "Y"] session_numbers_list = session_numbers_string.split(" ") for session in session_numbers_list: if not session.isdigit(): # No session detected, skipping. break # In case of unforeseen issues, initiate the values. session_type = "" session_service = "" # Sessions are numbers X, Y, .. session_type_command = f"loginctl show-session {session} --property=Type" session_type_verbose = run_verbose(session_type_command) if session_type_verbose[1] == 0: # Expected type output is Type=x11 or Type=wayland session_type = session_type_verbose[0].split("=", 1)[1] # Sessions are numbers X, Y, .. session_service_command = f"loginctl show-session {session} --property=Service" session_service_verbose = run_verbose(session_service_command) if session_service_verbose[1] == 0: # Expected service output is Service=gdm-autologin session_service = session_service_verbose[0].split("=", 1)[1] # For easy readability, adding bool function which is not needed. expected_type = bool(session_type in ("x11", "wayland")) expected_service = bool("autologin" in session_service) # Have a variable to check if a session was detected in closing state. session_in_closing_state = False # If both conditions apply check the State of the session. if expected_type and expected_service: # If something goes wrong the system might hang up to 90 seconds # waiting for the service to die. for _ in range(100): # Reload the variable. loginctl_command = f"loginctl show-session {session} --property=State" session_state = run(loginctl_command).strip("\n") # Make the check for the state and act properly. if session_state == "State=closing": session_in_closing_state = True time.sleep(1) elif session_state == "State=active": break # If closing state was detected and it got to this point. # The session is still closing, kill it. if session_in_closing_state: print_info("".join(( "Session detected in closing state ", "which did not end after 100 seconds, sending SIGTERM." ))) time.sleep(1) # Extra log straight to journal if for some reason wrong session is killed. log_message_to_journal( priority="warning", identifier="dogtail-headless", invoke=f"echo '=== loginctl kill-session --signal=15 {session} ==='" ) run(f"sudo loginctl kill-session --signal=15 {session}") time.sleep(1) # Start display manager if display manager is not active already. is_display_manager_active = run(f"systemctl is-active {self.display_manager}").strip("\n") if is_display_manager_active != "active": run(f"sudo systemctl start {self.display_manager}") print_info("Starting Display Manager.") time.sleep(4) # But the session must be running. if not self.wait_until_process_is_running(self.session_started_indicator): print_info(" ".join(( f"Running session indicator '{self.session_started_indicator}'", "was not detected - restart required." ))) print_info(f"Attempt to restore headless - stopping '{self.display_manager}'") self.stop_display_manager() print_info(f"Attempt to restore headless - starting '{self.display_manager}'") run(f"sudo systemctl start {self.display_manager}") time.sleep(4) def stop_display_manager(self) -> None: """ Stopping the display manager. """ run(f"sudo systemctl stop {self.display_manager}") print_info("Stopping Display Manager.") # Dumb sleep to prevent any kind of races. time.sleep(3) self.wait_until_process_is_not_running(self.session_started_indicator) # Failsafe, the session should end, but if not # send SIGTERM and give it time to close. loginctl_command = f"sudo loginctl | grep {self.user} | grep seat0" still_open_session = run_verbose(loginctl_command) # Check return code. if still_open_session[1] == 0: os.system("sudo loginctl") still_open_session_number = ( run(f"sudo loginctl | grep {self.user} | grep seat0") .strip(" ") .split(" ")[0] ) print_info(f"Session did not end after 'systemctl stop {self.display_manager}'.") print_info(f"Sending SIGTERM to '{still_open_session_number}'.") time.sleep(1) # Extra log straight to journal if for some reason wrong session is killed. log_message_to_journal( priority="warning", identifier="dogtail-headless", invoke=f"echo '=== loginctl kill-session --signal=15 {still_open_session_number} ==='" ) run(f"sudo loginctl kill-session --signal=15 {still_open_session_number}") time.sleep(1) # If something goes wrong the system might hang up to 90 seconds waiting # for the service to die. for counter in range(100): loginctl_command = f"loginctl show-user {self.user} --property=State" user_still_logged_in = run_verbose(loginctl_command) user_still_logged_in_stripped = user_still_logged_in[0].strip("\n") if user_still_logged_in_stripped == "State=closing": print_info(f"State of '{loginctl_command}' is '{user_still_logged_in_stripped}'.") time.sleep(1) elif user_still_logged_in_stripped == "State=active": print_info("Session is still active after sending SIGTERM.") break # Failed to get user: User ID 1000 is not logged in or lingering. elif user_still_logged_in[1] == 1: print_info(f"Session ended after SIGTERM in '{counter}' seconds.") break # Leftover display manager process that seems to mess everything up. still_open_dm_session = run_verbose(f"sudo loginctl | grep {self.display_manager}") # Check return code. if still_open_dm_session[1] == 0: os.system("sudo loginctl") still_open_dm_session_number = run(f"sudo loginctl | grep {self.display_manager}").strip(" ").split(" ")[0] print_info(f"Session still open, sending SIGTERM to '{self.display_manager}' login '{still_open_dm_session_number}'.") time.sleep(1) # Extra log straight to journal if for some reason wrong session is killed. log_message_to_journal( priority="warning", identifier="dogtail-headless", invoke=f"echo '=== loginctl kill-session --signal=15 {still_open_dm_session_number} ==='" ) run(f"sudo loginctl kill-session --signal=15 {still_open_dm_session_number}") time.sleep(1) @staticmethod def is_process_running(process_to_find) -> bool: """ Is process running helper function. :param process_to_find: Process to find. :type process_to_find: str :return: Process is running. :rtype: bool """ active_processes = run("ps axw").split("\n") for active_process in active_processes: if re.search(process_to_find, str(active_process).lower()): return True return False def wait_until_process_is_running(self, process_to_find) -> bool: """ Waiting until the process is running function. :param process_to_find: Process to find. :type process_to_find: str :return: The process was found before the 30 second timeout. :rtype: bool """ for _ in range(60): if not self.is_process_running(process_to_find): time.sleep(0.5) else: return True return False def wait_until_process_is_not_running(self, process_to_find) -> None: """ Waiting until the process is NOT running function. :param process_to_find: Process to NOT find. :type process_to_find: str :return: The process was NOT found in the 30 second timeout. :rtype: bool """ for _ in range(60): if self.is_process_running(process_to_find): time.sleep(0.5) else: break class Headless: """ Headless class. """ def __init__(self) -> None: # Saving journal cursor for debugging. initial_cursor_output = run("sudo journalctl --lines=0 --show-cursor").strip() cursor_target = initial_cursor_output.split("cursor: ", 1)[-1] self.logging_cursor = f'"--after-cursor={cursor_target}"' self.display_manager_control = None self.environment_control = None self.script_control = None self.arguments = None self.script_as_list_of_arguments = "" self.session = None self.session_binary = None self.display_manager = None self.enable_start = True self.enable_stop = True self.display_manager_restart = False self.disable_accessibility_on_script_exit = None self.force = None self.session_type = None self.session_desktop = None self.user_script_process = None self.user_script_exit_code = None self.no_automatic_login = False self.display_number = ":0" self.virtual_monitors = 0 self.allow_duplication = False self.term = None # Upon configuration change restart the session if it is running. self.restart_required = False @staticmethod def set_accessibility_to(enable_accessibility) -> None: """ Using simple gsettings command to enable or disable toolkit-accessibility. """ set_accessibility_value = "true" if enable_accessibility else "false" gsetting_get_command = " ".join( ( "dbus-run-session gsettings get", "org.gnome.desktop.interface", "toolkit-accessibility", ) ) gsetting_set_command = " ".join( ( "dbus-run-session gsettings set", "org.gnome.desktop.interface", f"toolkit-accessibility {set_accessibility_value}", ) ) accessibility_value = run(gsetting_get_command) if accessibility_value != set_accessibility_value: print_info(f"Changing a11y value from '{accessibility_value}' to '{set_accessibility_value}'") run(gsetting_set_command) @staticmethod def adjust_gsettings_values() -> None: """ Using simple gsettings command to adjust values of delay, repeat and repeat-interval. """ user = run("whoami") scheme = "org.gnome.desktop.peripherals.keyboard" gsetting_get_delay_command = f"sudo -Hu {user} gsettings get {scheme} delay" gsetting_set_delay_command = f"sudo -Hu {user} gsettings set {scheme} delay 'uint32 500'" gsetting_get_repeat_command = f"sudo -Hu {user} gsettings get {scheme} repeat" gsetting_set_repeat_command = f"sudo -Hu {user} gsettings set {scheme} repeat true" gsetting_get_repeat_interval_command = f"sudo -Hu {user} gsettings get {scheme} repeat-interval" gsetting_set_repeat_interval_command = f"sudo -Hu {user} gsettings set {scheme} repeat-interval 'uint32 30'" delay_result = run(gsetting_get_delay_command) repeat_result = run(gsetting_get_repeat_command) repeat_interval_result = run(gsetting_get_repeat_interval_command) if delay_result != "uint32 500": run(gsetting_set_delay_command) print_info(f"Value of gsettings delay was '{delay_result}' - changing to 'uint32 500'.") if repeat_result != "true": run(gsetting_set_repeat_command) print_info(f"Value of gsettings repeat was '{repeat_result}' - changing to 'true'.") if repeat_interval_result != "uint32 30": run(gsetting_set_repeat_interval_command) print_info(f"Value of gsettings repeat-interval was '{repeat_interval_result}' - changing to 'uint32 30'.") def troubleshoot(self) -> None: """ Test parts of the system for correct configuration. """ # Troubleshooting gnome-session-binary upon failure. user_id = str(os.geteuid()) gnome_session_binary_pid = run(f"pgrep -fAu {user_id} {self.session_binary}").split("\n")[0] ps_command = f"ps ax -o uid,gid,pid,tname,command | grep {self.session_binary}" ps_command_result = run(ps_command) print_info(f"Troubleshooting '{self.session_binary}':") print(f"Target file for environment is '{self.session_binary}'") print(f"Search was done with 'pgrep -fAu {user_id} {self.session_binary}'.") print(f"With result '{str(gnome_session_binary_pid)}'\n") print_info("Actual running processes:") print(f"$ {ps_command}") print(f"{str(ps_command_result)}\n") # Troubleshooting dbus upon failure. dbus_processes_command = "ps ax -o uid,gid,pid,tname,command | grep dbus" dbus_processes_result = run(dbus_processes_command) print_info("Troubleshooting dbus:") print(f"$ {dbus_processes_command}") print(f"{dbus_processes_result}\n") # Troubleshooting at-spi processes that are required. print_info("Troubleshooting AT-SPI processes:") at_spi_command = "ps ax -o uid,gid,pid,tname,command | grep at-spi" at_spi_result = run(at_spi_command) print_info("Expecting running processes to be:") print("'/usr/libexec/at-spi-bus-launcher'") print("'/usr/libexec/at-spi2-registryd'\n") print_info("Actual running processes:") print(f"$ {at_spi_command}") print(f"{at_spi_result}\n") # Troubleshooting important environment variables. print_info("Troubleshooting Environment variables:") display = os.getenv("DISPLAY") xauthority = os.getenv("XAUTHORITY") dbus_session_bus_address = os.getenv("DBUS_SESSION_BUS_ADDRESS") print(f"DISPLAY = '{display}'") print(f"XAUTHORITY = '{xauthority}'") print(f"DBUS_SESSION_BUS_ADDRESS = '{dbus_session_bus_address}'\n") session_result = "ps ax | grep -E 'gdm|gnome-shell|gnome-session'" session_run_result = run(session_result) print(f"SESSION DEBUG:\n'{session_run_result}'") journal_run = run_verbose(" ".join(( "sudo journalctl --all", f"--output=short-precise {self.logging_cursor}", f"| grep -E '{self.display_manager}|gnome-shell|gnome-session'", ))) if journal_run[1] != 0: print_info(f"journalctl data gathering failed: '{journal_run}'") else: print_info("journalctl data:") print(f"{journal_run[0]}\n") def handle_keep_logic(self, keep_value) -> None: """ Handling keep logic. :param keep_value: String form of an integer expressing how many test will be run in a single session before restart. :type keep_value: str """ keep_file = "/tmp/dogtail_keep" keep_from_argument = int(keep_value) try: with open(keep_file, "r", encoding="utf-8") as _file: keep_from_file = int(_file.read()) except OSError: keep_from_file = 1 self.enable_stop = False if keep_from_file == 1: self.display_manager_restart = True if keep_from_file >= keep_from_argument: self.enable_stop = True keep_from_file = 0 with open(keep_file, "w", encoding="utf-8") as _file: _file.write(f"{keep_from_file + 1}") def handle_arguments(self) -> None: """ Makes all necessary steps for arguments passed along the headless script. """ # Workaround for firefox with bugged accessibility which will sometimes turn off. os.environ["GNOME_ACCESSIBILITY"] = "1" # Parse arguments of headless. self.arguments = parse() # Parse arguments of given script. self.script_as_list_of_arguments = self.arguments.script.split() # Handle session. if self.arguments.session: self.session = self.arguments.session # Handle session binary. if self.arguments.session_binary: self.session_binary = self.arguments.session_binary # Handle display manager. if self.arguments.dm: self.display_manager = self.arguments.dm # Default session / binary logic. if self.display_manager == "lightdm": self.session = self.session or "mate" self.session_binary = self.session_binary or "/usr/bin/mate-session" elif self.display_manager == "sddm": self.session = self.session or "kde" self.session_binary = self.session_binary or "/usr/bin/plasmashell" elif self.display_manager in (None, "gdm"): self.display_manager = "gdm" self.session_binary = self.session_binary or "/usr/bin/gnome-session" else: print(f"Unknown Display Manager '{self.display_manager}'!") sys.exit(-1) # If there is no default, user needs to specify. if not self.session_binary: print_info(f"Need to specify --session-binary for '{self.session}'.") sys.exit(-1) # Handle headless duplication. if self.arguments.allow_duplication: self.allow_duplication = True # Handle headless debug variable. if self.arguments.debug: os.environ["DOGTAIL_DEBUG"] = "true" # Handle keep argument, check value of /tmp/dogtail_keep. if self.arguments.keep: self.handle_keep_logic(self.arguments.keep) # Handle display number. if self.arguments.display: self.display_number = self.arguments.display # Handle headless don't start variable. if self.arguments.dont_start: self.enable_start = False # Handle headless don't kill variable. # Handle headless keep max variable, which is just do not kill variable. if self.arguments.dont_kill or self.arguments.keep_max: self.enable_stop = False # Handle headless restart variable. if self.arguments.restart: self.display_manager_restart = True # Handle headless disable a11y variable. if self.arguments.disable_a11y: self.disable_accessibility_on_script_exit = True # Handle headless force variable. if self.arguments.force: self.force = True # Handle session type variable. if self.arguments.session_type: self.session_type = self.arguments.session_type # Handle session desktop variable. if self.arguments.session_desktop: self.session_desktop = self.arguments.session_desktop # Handle no automatic login variable. if self.arguments.no_autologin: self.no_automatic_login = self.arguments.no_autologin # Handle TERM variable. if self.arguments.term: self.term = self.arguments.term def set_display_number(self) -> None: """ Retrieve information about running process and prints it before user script start. """ os.environ["DISPLAY"] = self.display_number print_info(f"Setting DISPLAY variable to '{self.display_number}'.") def check_what_desktop_and_type_is_running(self) -> None: """ Retrieve information about running process and prints it before user script start. """ if not self.enable_start: return error_list = [] try: if os.environ["XDG_SESSION_TYPE"] == "x11": current_type = "xorg" else: current_type = os.environ["XDG_SESSION_TYPE"] except KeyError as error: error_list.append(error) current_type = "__unavailable__" try: current_desktop = os.environ["XDG_SESSION_DESKTOP"] except KeyError as error: error_list.append(error) current_desktop = "__unavailable__" print_info(f"Running '{current_type}' with desktop '{current_desktop}'") if error_list: print_info(f"Error detected when loading environment variables: '{error_list}'") def verify_that_correct_session_was_started(self) -> None: """ Verifies that correct session type as started, terminate on mismatch. """ if not self.enable_start: return # Using 'xorg' naming for x11 session. current_type = "" if os.environ["XDG_SESSION_TYPE"] == "x11": current_type = "xorg" else: current_type = os.environ["XDG_SESSION_TYPE"] if ( self.display_manager_control.session_type and self.display_manager_control.session_type != current_type ): print_info(f"Script requires session of type: '{self.display_manager_control.session_type}'") print_info(f"Script was started under session of type: '{current_type}'\n") # Check if the session failed because of drivers. # Actual use case is to know if wayland failed because of qxl driver. for command in ["lspci", "sudo lspci"]: # Execute the lspci command and check if it was a success. lspci_result = run_verbose(command) if lspci_result[1] == 0: # On success print the result and skip to the headless exit. print_info(f"'{command}' command output:\n{lspci_result[0]}") break # On fail print the error and run the 'sudo lspci' command. print_info(f"'{command}' command did not succeed: '{lspci_result}'.") print_info("Exiting the headless script.") sys.exit(1) current_desktop = os.environ["XDG_SESSION_DESKTOP"] if ( self.display_manager_control.session_desktop and self.display_manager_control.session_desktop != current_desktop ): print_info(f"Script requires session with desktop: '{self.display_manager_control.session_desktop}'") print_info(f"Script was started under session with desktop: '{current_desktop}'\n") print_info("Exiting the headless script.") sys.exit(1) def handle_coloring_of_headless_messages(self, use_color=True) -> None: """ Handle coloring of headless images. """ # Process id of current (headless) process - os.getpid() # Process group id of current (headless) process - os.getpgid(0) # If they differ, the process (headless) was started from another script. # Another option is to check if real terminal is used and enable nested starts # also. Serves as an indicator if we can use colors or not. global COLORS_ENABLED # User cmd line arguments has top priority. if not use_color: COLORS_ENABLED = False print("headless: Colors disabled by user.") # Next priority is isatty or if the script was started directly. elif sys.stdout.isatty() or os.getpid() == os.getpgid(0): COLORS_ENABLED = True print_info("Colors enabled.") # In other cases just disable the colors. else: print("headless: Colors disabled.") def handle_nested_dogtail_process(self): """ Handle nested dogtail headless process. """ # First lets make sure no orphaned processes are present. cmd = ["pgrep -f .*dogtail-headless*"] process = subprocess.Popen( cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8" ) all_dogtail_processes, _ = process.communicate() all_dogtail_processes = all_dogtail_processes.splitlines() try: # Import psutil here in case it is missing on the system. import psutil # Iterate over processes. for process_id in all_dogtail_processes: # Get parent_process_id from process_id. try: parent_process_id = psutil.Process(int(process_id)).ppid() except Exception: pass # Orphaned dogtail-headless check, terminate it. if int(parent_process_id) == 1: print_info(f"Terminating orphaned '{process_id}' dogtail-headless process.") run(f"sudo kill -15 {process_id}") time.sleep(1) except ImportError: print_info("Module 'psutil' unavailable. Any orphaned process is not terminated.") except TypeError as error: print_info(f"Attempted to use PID that is not valid: '{error}'.") print_info(f"Process IDs used: '{all_dogtail_processes}'.") # Failsafe if user wants to start another nested script. cmd = ["pgrep -f .*python.*dogtail-headless*"] process = subprocess.Popen( cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8" ) all_dogtail_processes, _ = process.communicate() number_of_running_python_headless_scripts = len(all_dogtail_processes.splitlines()) if number_of_running_python_headless_scripts > 1: print_info("Attempting to start another instance of headless script.") print_info("Exiting the duplicate of headless script.") sys.exit(1) def execute(self) -> None: """ Makes all necessary preparations for the system to start display manager and execute user script. """ # Arguments handling. self.handle_arguments() try: from importlib.metadata import version dogtail_version = version("dogtail") print_info(f"Version of dogtail installed: '{dogtail_version}'.") except Exception as error: print_info(f"Version of dogtail was not retrieved for: '{error}'.") # Starting the timer to help debug issues with timeouts. starting_point = time.time() print_info("Starting the timer for the headless script.") # Headless is not supposed to be run under user root. if os.geteuid() == 0: print_info("Script is not meant to be run under the root user.") print_info("Exiting the headless script.") sys.exit(1) # There is a bug in which the root takes ownership of dconf file. So far we did # not discover the cause. We can easily fix this by deleting the file, upon # session start new file with correct owner is created. verify_file_ownership() # Nested script handling. if not self.allow_duplication: self.handle_nested_dogtail_process() # Before the Display Manager handling, check if the session is running already. environment_dictionary = get_initial_environment_dictionary(binary=self.session_binary) if environment_dictionary: os.environ = environment_dictionary # Display manager setup and handling. print_info(f"Using Display Manager: '{self.display_manager}'") self.display_manager_control = DisplayManager( session=self.session, session_binary=self.session_binary, display_manager=self.display_manager, session_type=self.session_type, session_desktop=self.session_desktop, enable_start=self.enable_start, enable_stop=self.enable_stop, display_manager_restart=self.display_manager_restart, no_automatic_login=self.no_automatic_login, virtual_monitors=self.virtual_monitors ) self.display_manager_control.handling_config_setup() self.display_manager_control.handling_account_setup() # Debugging a11y randomly turning off. self.display_manager_control.handling_debug_accessibility_setup() # GNOME Shell is default - when session_desktop is not provided we assume GNOME Shell is desired. if not self.display_manager_control.session_desktop: self.display_manager_control.handling_unsafe_mode_setup( use_unsafe_mode=(self.display_manager_control.session_type == "wayland"), desktop="gnome-shell" ) # Set unsafe mode only for wayland and GNOME Kiosk. elif "kiosk" in self.display_manager_control.session_desktop: self.display_manager_control.handling_unsafe_mode_setup( use_unsafe_mode=("wayland" in self.display_manager_control.session_type), desktop="gnome-kiosk" ) # Set unsafe mode only for wayland and GNOME Shell. elif "gnome" in self.display_manager_control.session_desktop: self.display_manager_control.handling_unsafe_mode_setup( use_unsafe_mode=(self.display_manager_control.session_type == "wayland"), desktop="gnome-shell" ) else: print_info(f"Unexpected session desktop: '{self.display_manager_control.session_desktop}'") print_info("Failed to establish desktop environment, unsafe mode not set!") # Set virtual monitors if user requires. self.display_manager_control.handling_virtual_monitors() # Required setup that needs to go through in its entirety or it will fail. if self.enable_start: self.display_manager_control.start_display_manager() # Environment handling. environment_dictionary = get_environment_dictionary(binary=self.session_binary, term=self.term) if environment_dictionary: os.environ = environment_dictionary else: print_info("Display Manager Status is:") print(f"{run(f'systemctl status {self.display_manager}')}") self.troubleshoot() print_info("Attempt to retrieve environmental variables failed.") self.display_manager_control.stop_display_manager() print_info("Exiting the headless script.") sys.exit(1) # Force xorg/wayland setting - terminate upon error. if self.force: self.verify_that_correct_session_was_started() # Check xorg/wayland setting - print what the test will run under. self.check_what_desktop_and_type_is_running() # Set DISPLAY number. self.set_display_number() # Accessibility - has to be started after display manager / dbus is running. self.set_accessibility_to(True) # Make a gsettings that sometimes get set too high and is uncomfortable to work with. self.adjust_gsettings_values() # User script handling. if is_binary_existing_and_executable(self.script_as_list_of_arguments[0]): self.user_script_process = subprocess.Popen( self.script_as_list_of_arguments, env=os.environ ) print_info(f"Started the script with PID {self.user_script_process.pid}.") self.user_script_exit_code = self.user_script_process.wait() return_code_string = f"The user script finished with return code {self.user_script_exit_code}." if int(self.user_script_exit_code) == 0: print_info(return_code_string) else: print_info(return_code_string) # Disable accessibility upon script exit. if self.disable_accessibility_on_script_exit: self.set_accessibility_to(False) # Stop display manager unless user specifies otherwise. if self.enable_stop or self.user_script_exit_code != 0: self.display_manager_control.stop_display_manager() ending_point = time.time() print_info(f"The headless script ran for {(ending_point - starting_point):.2f} seconds.") def log_message_to_journal(priority, identifier, invoke): """ Invoke a program to be logged in journal. :param priority: Priority of the message. :type priority: str :param identifier: Identifier we want in the journal. :type identifier: str :param invoke: Invoke a program to execute. :type invoke: str """ # journal entry -p priority -t identifier [invoke a program] run( " ".join( ( "systemd-cat", f"-p {priority}", f"-t {identifier}", invoke, ) ) ) # Graceful exit in the event of an interrupt from user SIGINT or system SIGTERM. def graceful_exit(signum, frame): # pylint: disable=unused-argument """ Attempting graceful exit on signal interruption. """ print_info(f"The headless script was interrupted by signal '{signum}'.") # Attempt to debug issues upon timeout if used together with qecore library. if int(signum) == 15 and os.path.exists("/tmp/qecore_logger.log"): run("echo '===== Timeout - debugging =====' >> /tmp/automation_debug.log") run("cat '/tmp/qecore_logger.log' >> /tmp/automation_debug.log") run("echo '===============================' >> /tmp/automation_debug.log") log_message_to_journal( priority="info", identifier="dogtail-headless", invoke="echo '=== Graceful exit of dogtail-headless upon SIGINT or SIGTERM ==='" ) sys.exit(signum) def main(): """ Main function. """ log_message_to_journal( priority="warning", identifier="dogtail-headless", invoke="echo '=== Starting dogtail-headless ==='" ) headless = Headless() # Register signal handlers. signal.signal(signal.SIGINT, graceful_exit) signal.signal(signal.SIGTERM, graceful_exit) headless.execute() log_message_to_journal( priority="warning", identifier="dogtail-headless", invoke=f"echo '=== Exiting dogtail-headless with {headless.user_script_exit_code} ==='" ) sys.exit(headless.user_script_exit_code) if __name__ == "__main__": main() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/tree.py000066400000000000000000001723521520163156700237660ustar00rootroot00000000000000#!/usr/bin/python3 """ The tree module for dogtail. """ # pylint: disable=broad-exception-caught # pylint: disable=no-name-in-module # pylint: disable=wrong-import-position # pylint: disable=wrong-import-order # pylint: disable=import-error # pylint: disable=import-outside-toplevel # pylint: disable=protected-access # ruff: noqa: E402 # ruff: noqa: E501 import os import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi import warnings warnings.filterwarnings("ignore", "g_object_unref") from .logging import logging_class LOGGING = logging_class.logger from .accessibles.accessible_actions import AccessibleActions from .accessibles.accessible_component import AccessibleComponent from .accessibles.accessible_editable_text import AccessibleEditableText from .accessibles.accessible_hypertext import AccessibleHypertext from .accessibles.accessible_image import AccessibleImage from .accessibles.accessible_object import AccessibleObject, SearchError from .accessibles.accessible_root import AccessibleRoot from .accessibles.accessible_selection import AccessibleSelection from .accessibles.accessible_state import AccessibleState from .accessibles.accessible_table import AccessibleTable from .accessibles.accessible_table_cell import AccessibleTableCell from .accessibles.accessible_text import AccessibleText from .accessibles.accessible_utilities import AccessibleUtilities from .accessibles.accessible_value import AccessibleValue from dogtail.predicate import ( GenericPredicate, IsAMenuNamed, IsAMenuItemNamed, IsATextEntryNamed, IsLabelledAs, IsNamed, IsATabNamed, IsAWindowNamed, IsADialogNamed, ) from dogtail.ponytail_helper import ponytail_helper from dogtail.rawinput import click, point, double_click from dogtail.utils import do_delay @logging_class.log_a_class class Node(object): """ Main dogtail API definition. """ LOGGING.debug("Initiating dogtail api.") event_listeners = {} # Keep session as a variable. session_type = "x11" if "XDG_SESSION_TYPE" in os.environ and "wayland" in os.environ["XDG_SESSION_TYPE"]: session_type = "wayland" # I understand this a little better now. def register_event_listener(self, client, *names): """ Register an event listener. In the future we might need to register all event listeners to get updates. """ try: listener = self.event_listeners[client] except Exception: self.event_listeners[client] = Atspi.EventListener.new(self.dummy_callback) listener = self.event_listeners[client] for name in names: # If Newton do X. # Newton.event_listener.register... # else: Do Atspi Atspi.EventListener.register(listener, name) # I understand this a little better now. def deregister_event_listener(self, client, *names): """ Register an event listener. """ try: listener = self.event_listeners[client] except Exception: return for name in names: # If Newton do X. # Newton.event_listener.deregister... # else: Do Atspi Atspi.EventListener.deregister(listener, name) # I understand this a little better now. def dummy_callback(self, event): """ Define what to do when a registered event is fired. In this case it will print what event was fired and what source was the cause. """ # The Atspi object. source = event.source if isinstance(source, Atspi.Accessible): try: name = Atspi.Accessible.get_name(source) role_name = Atspi.Accessible.get_role_name(source) source_description = f"source: [ {role_name} | {name} ]" except Exception as e: source_description = f"[DEAD] - '{e}'" else: source_description = "" # Not printing any unnamed objects, which will flood the log. if "| ]" not in source_description: print(f"Event: '{event.type}' '{source_description}'") def do_action(self, index): """ Invoke the action indicated by index on the object implementing Atspi.Action. """ return AccessibleActions.do_action(self, index) def get_action_description(self, index): """ Get the description of the index-th Action on the object implementing Atspi.Action. """ return AccessibleActions.get_action_description(self, index) def get_action_name(self, index): """ Get the name of the index-th Action on the object implementing Atspi.Action. """ return AccessibleActions.get_action_description(self, index) def get_key_binding(self, index): """ Get the keybindings of the index-th Action on the object implementing Atspi.Action. """ return AccessibleActions.get_key_binding(self, index) def get_localized_name(self, index): """ Get the localized name of the index-th Action on the object implementing Atspi.Action. """ return AccessibleActions.get_localized_name(self, index) @property def actions(self): """ Get dictionary of actions. """ return AccessibleActions.get_actions(self) def do_action_named(self, named_action): """ Invoke the action indicated by its name on the object implementing Atspi.Action. """ return AccessibleActions.do_action_named(self, named_action) def contains(self, x_coordinate, y_coordinate, coordinate_type=None): """ Queries whether a given Atspi.Component contains a particular point. """ return AccessibleComponent.contains(self, x_coordinate, y_coordinate, coordinate_type) def get_accessible_at_point(self, x_coordinate, y_coordinate, coordinate_type=None): """ Gets the accessible child at a given coordinate within an Atspi.Component. """ return AccessibleComponent.get_accessible_at_point(self, x_coordinate, y_coordinate, coordinate_type) def get_alpha(self): """ Gets the opacity/alpha value of a component, if alpha blending is in use. """ return AccessibleComponent.get_alpha(self) def get_extents(self): """ Gets the bounding box of the specified Atspi.Component. The returned values are meaningful only if the Component has both STATE_VISIBLE and STATE_SHOWING. """ return AccessibleComponent.get_extents(self) @property def extents(self): """ Gets the bounding box of the specified Atspi.Component. The returned values are meaningful only if the Component has both STATE_VISIBLE and STATE_SHOWING. """ return self.get_extents() def get_layer(self): """ Queries which layer the component is painted into, to help determine its visibility in terms of stacking order. """ return AccessibleComponent.get_layer(self) def get_mdi_z_order(self): """ Queries the z stacking order of a component which is in the MDI or window layer. (Bigger z-order numbers mean nearer the top) """ return AccessibleComponent.get_mdi_z_order(self) def get_position(self): """ A tuple containing the position of the Accessible: (x, y). """ return AccessibleComponent.get_position(self) @property def position(self): """ A tuple containing the position of the Accessible: (x, y). """ return self.get_position() def get_size(self): """ A tuple containing the size of the Accessible: (w, h) """ return AccessibleComponent.get_size(self) @property def size(self): """ A tuple containing the size of the Accessible: (w, h). """ return self.get_size() def grab_focus(self): """ Grab focus. """ return AccessibleComponent.grab_focus(self) def scroll_to(self, scroll_type): """ Scrolls whatever container of the Atspi.Component object so it becomes visible on the screen. Atspi.ScrollType.TOP_LEFT = 0 Atspi.ScrollType.BOTTOM_RIGHT = 1 Atspi.ScrollType.TOP_EDGE = 2 Atspi.ScrollType.BOTTOM_EDGE = 3 Atspi.ScrollType.LEFT_EDGE = 4 Atspi.ScrollType.RIGHT_EDGE = 5 Atspi.ScrollType.ANYWHERE = 6 """ return AccessibleComponent.scroll_to(self, scroll_type) def scroll_to_point(self, coordinate_type, x_coordinate, y_coordinate): """ Scrolls whatever container of the Atspi.Component object so it becomes visible on the screen. Atspi.CoordType.SCREEN = 0 Atspi.CoordType.WINDOW = 1 Atspi.CoordType.PARENT = 2 """ return AccessibleComponent.scroll_to_point( self, coordinate_type, x_coordinate, y_coordinate ) def set_extents(self, x_coordinate, y_coordinate, width, height, coordinate_type): """ Moves and resizes the specified component. Atspi.CoordType.SCREEN = 0 Atspi.CoordType.WINDOW = 1 Atspi.CoordType.PARENT = 2 """ return AccessibleComponent.set_extents( self, x_coordinate, y_coordinate, width, height, coordinate_type ) def set_position(self, x_coordinate, y_coordinate, coordinate_type): """ Moves the component to the specified position. Atspi.CoordType.SCREEN = 0 Atspi.CoordType.WINDOW = 1 Atspi.CoordType.PARENT = 2 """ return AccessibleComponent.set_position( self, x_coordinate, y_coordinate, coordinate_type ) def set_size(self,width, height): """ Resizes the specified component to the given pixel dimensions. """ return AccessibleComponent.set_size(self, width, height) def get_center(self): """ A tuple containing the center of the Accessible: (x, y). """ return AccessibleComponent.get_center(self) @property def center(self): """ A tuple containing the center of the Accessible: (x, y). """ return self.get_center() def copy_text(self, start_position=None, end_position=None): """ Copies text from an Atspi.EditableText object into the system clipboard. """ return AccessibleEditableText.copy_text(self, start_position, end_position) def cut_text(self, start_position=None, end_position=None): """ Deletes text from an Atspi.EditableText object, copying the excised portion into the system clipboard. """ return AccessibleEditableText.cut_text(self, start_position, end_position) def delete_text(self, start_position=None, end_position=None): """ Deletes text from an Atspi.EditableText object, without copying the excised portion into the system clipboard. """ return AccessibleEditableText.delete_text(self, start_position, end_position) def insert_text(self, text, position=0, length=0): """ Inserts text into an Atspi.EditableText object. As with all character offsets, the specified position may not be the same as the resulting byte offset, since the text is in a variable-width encoding. Deliberately moving parameters around so that the text is first. """ return AccessibleEditableText.insert_text(self, text, position, length) def paste_text(self, position): """ Inserts text from the system clipboard into an Atspi.EditableText object. As with all character offsets, the specified position may not be the same as the resulting byte offset, since the text is in a variable-width encoding. """ return AccessibleEditableText.paste_text(self, position) def set_text_contents(self, new_contents): """ Replace the entire text contents of an Atspi.EditableText object. """ return AccessibleEditableText.set_text_contents(self, new_contents) def get_link(self, link_index): """ Gets the Atspi.Hyperlink object at a specified index. """ return AccessibleHypertext.get_link(self, link_index) def get_link_index(self, character_offset): """ Gets the index of the Atspi.Hyperlink object at a specified character offset. """ return AccessibleHypertext.get_link_index(self, character_offset) def get_n_links(self): """ Gets the total number of Atspi.Hyperlink objects that an Atspi.Hypertext implementor has. """ return AccessibleHypertext.get_n_links(self) def get_image_description(self): """ Gets the description of the image displayed in an Atspi.Image object. """ return AccessibleImage.get_image_description(self) def get_image_extents(self, coordinate_type=None): """ Gets the bounding box of the image displayed in a specified Atspi.Image implementor. The returned values are meaningful only if the Image has both STATE_VISIBLE and STATE_SHOWING. """ return AccessibleImage.get_image_extents(self, coordinate_type) def get_image_locale(self): """ Gets the locale associated with an image and its textual representation. """ return AccessibleImage.get_image_locale(self) def get_image_position(self, coordinate_type=None): """ Gets the minimum x and y coordinates of the image displayed in a specified Atspi.Image implementor. The returned values are meaningful only if the Image has both STATE_VISIBLE and STATE_SHOWING. """ return AccessibleImage.get_image_position(self, coordinate_type) def get_image_size(self): """ Gets the size of the image displayed in a specified Atspi.Image object. The returned values are meaningful only if the Image has both STATE_VISIBLE and STATE_SHOWING. """ return AccessibleImage.get_image_size(self) def clear_selection(self): """ Clears the current selection, removing all selected children from the specified Atspi.Selection implementor's selection list. """ return AccessibleSelection.clear_selection(self) def deselect_all(self): """ Deselects all children. """ return self.clear_selection() def deselect_child(self, child_index): """ Deselects a specific child of an Atspi.Selection. Note that child_index is the index of the child in the parent container. """ return AccessibleSelection.deselect_child(self, child_index) def deselect_selected_child(self, child_index): """ Removes a child from the selected children list of an Atspi.Selection. Note that selected_child_index is the index in the selected-children list, not the index in the parent container. selected_child_index in this method, and child_index in Atspi.Selection.select_child are asymmetric. """ return AccessibleSelection.deselect_selected_child(self, child_index) def get_n_selected_children(self): """ Gets the number of children of an Atspi.Selection implementor which are currently selected. """ return AccessibleSelection.get_n_selected_children(self) def get_selected_child(self, selected_child_index): """ Gets the i-th selected Atspi.Accessible child of an Atspi.Selection. Note that selected_child_index refers to the index in the list of 'selected' children and generally differs from that used in Atspi.Accessible.get_child_at_index or returned by Atspi.Accessible.get_index_in_parent. selected_child_index must lie between 0 and Atspi.Selection.get_n_selected_children - 1, inclusive. """ return AccessibleSelection.get_selected_child(self, selected_child_index) def is_child_selected(self, child_index): """ Determines whether a particular child of an Atspi.Selection implementor is currently selected. Note that child_index is the index into the standard Atspi.Accessible container's list of children. """ return AccessibleSelection.is_child_selected(self, child_index) def select_all(self): """ Attempts to select all of the children of an Atspi.Selection implementor. Not all Atspi.Selection implementors support this operation. """ return AccessibleSelection.select_all(self) def select_child(self, child_index): """ Adds a child to the selected children list of an Atspi.Selection. For Atspi.Selection implementors that only allow single selections, this may replace the (single) current selection. """ return AccessibleSelection.select_child(self, child_index) def select(self): """ Selects a child. """ return AccessibleSelection.select(self) def deselect(self): """ Deselects a child. """ return AccessibleSelection.deselect(self) @property def active(self): """ Indicates this object can accept keyboard focus, which means all events resulting from typing on the keyboard will normally be passed to it when it has focus. """ return AccessibleState.get_active(self) @property def focusable(self): """ Indicates this object can accept keyboard focus, which means all events resulting from typing on the keyboard will normally be passed to it when it has focus. """ return AccessibleState.get_focusable(self) @property def focused(self): """ Indicates this object currently has the keyboard focus. """ return AccessibleState.get_focused(self) @property def pressed(self): """ Indicates this object is currently pressed. """ return AccessibleState.get_pressed(self) @property def resizable(self): """ Indicates the size of this object's size is not fixed. """ return AccessibleState.get_resizable(self) @property def selected(self): """ Indicates this object is selected.. """ return AccessibleState.get_selected(self) @property def selectable(self): """ Indicates this object is selected.. """ return AccessibleState.get_selectable(self) @property def sensitive(self): """ Indicates this object is sensitive, e.g. to user interaction. """ return AccessibleState.get_sensitive(self) @property def showing(self): """ Indicates this object, the object's parent, the object's parent's parent, and so on, are all 'shown' to the end-user, i.e. subject to “exposure” if blocking or obscuring objects do not interpose between this object and the top of the window stack. """ return AccessibleState.get_showing(self) @property def visible(self): """ Indicates this object is visible, e.g. has been explicitly marked for exposure to the user. """ return AccessibleState.get_visible(self) @property def checked(self): """ Indicates this object is currently checked. """ return AccessibleState.get_checked(self) @property def checkable(self): """ Indicates this object has the potential to be checked, such as a checkbox or toggle-able table cell. """ return AccessibleState.get_checkable(self) @property def read_only(self): """ Indicates that an object which is ENABLED and SENSITIVE has a value which can be read, but not modified, by the user. """ return AccessibleState.get_read_only(self) @property def collapsed(self): """ Indicates this object is collapsed. """ return AccessibleState.get_collapsed(self) @property def editable(self): """ Indicates the user can change the contents of this object. """ return AccessibleState.get_editable(self) @property def armed(self): """ Indicates that the object is armed. """ return AccessibleState.get_armed(self) @property def invalid(self): """ Indicates that the object is invalid. """ return AccessibleState.get_invalid(self) @property def state_set(self): """ Get states. """ return AccessibleState.get_accessible_states(self) def add_column_selection(self, column): """ Selects the specified column, adding it to the current column selection. Not all tables support column selection. """ return AccessibleTable.add_column_selection(self, column) def add_row_selection(self, row): """ Selects the specified row, adding it to the current row selection. Not all tables support row selection. """ return AccessibleTable.add_row_selection(self, row) def get_accessible_at(self, row, column): """ Gets the table cell at the specified row and column indices. To get the accessible object at a particular (x, y) screen coordinate, use Atspi.Component.get_accessible_at_point. """ return AccessibleTable.get_accessible_at(self, row, column) def get_caption(self): """ Gets an accessible representation of the caption for an Atspi.Table. """ return AccessibleTable.get_caption(self) def get_column_at_index(self, index): """ Gets the table column index occupied by the child at a particular 1-D child index. See Atspi.Table.get_index_at, Atspi.Table.get_row_at_index. """ return AccessibleTable.get_column_at_index(self, index) def get_column_description(self, column): """ Gets a text description of a particular table column. This differs from Atspi.Table.get_column_header, which returns an #Accessible. """ return AccessibleTable.get_column_description(self, column) def get_column_extent_at(self, row, column): """ Gets the number of columns spanned by the table cell at the specific row and column (some tables can have cells which span multiple rows and/or columns). The returned values are meaningful only if the Table has both STATE_VISIBLE and STATE_SHOWING. """ return AccessibleTable.get_column_extent_at(self, row, column) def get_column_header(self, column): """ Gets the header associated with a table column, if available. This differs from Atspi.Table.get_column_description, which returns a string. """ return AccessibleTable.get_column_header(self, column) def get_index_at(self, row, column): """ Gets the 1-D child index corresponding to the specified 2-D row and column indices. To get the accessible object at a particular (x, y) screen coordinate, use Atspi.Component.get_accessible_at_point. See Atspi.Table.get_row_at_index, Atspi.Table.get_column_at_index """ return AccessibleTable.get_index_at(self, row, column) def get_n_columns(self): """ Gets the number of columns in an Atspi.Table, exclusive of any columns that are programmatically hidden, but inclusive of columns that may be outside of the current scrolling window or viewport. """ return AccessibleTable.get_n_columns(self) def get_n_rows(self): """ Gets the number of rows in an Atspi.Table, exclusive of any rows that are programmatically hidden, but inclusive of rows that may be outside of the current scrolling window or viewport. """ return AccessibleTable.get_n_rows(self) def get_n_selected_columns(self): """ Queries a table to find out how many columns are currently selected. Not all tables support column selection. """ return AccessibleTable.get_n_selected_columns(self) def get_n_selected_rows(self): """ Query a table to find out how many rows are currently selected. Not all tables support row selection. """ return AccessibleTable.get_n_selected_rows(self) def get_row_at_index(self, index): """ Gets the table row index occupied by the child at a particular 1-D child index. See Atspi.Table.get_index_at, Atspi.Table.get_column_at_index. """ return AccessibleTable.get_row_at_index(self, index) def get_row_column_extents_at_index(self, index): """ Given a child index, determines the row and column indices and extents, and whether the cell is currently selected. If the child at index is not a cell (for instance, if it is a summary, caption, etc.), False is returned. The returned values are meaningful only if the Table has both STATE_VISIBLE and STATE_SHOWING. Return type: (bool, row: int, col: int, row_extents: int, col_extents: int, is_selected: bool) Example: If the Atspi.Table child at index '6' extends across columns 5 and 6 of row 2 of an Atspi.Table instance, and is currently selected, then retval = Atspi.Table.get_row_column_extents_at_index (table, 6, row, col, row_extents, col_extents, is_selected); will return True, and after the call row, col, row_extents, col_extents, and is_selected will contain 2, 5, 1, 2, and True, respectively. """ return AccessibleTable.get_row_column_extents_at_index(self, index) def get_row_description(self, row): """ Gets a text description of a particular table row. This differs from Atspi.Table.get_row_header, which returns an Atspi.Accessible. """ return AccessibleTable.get_row_description(self, row) def get_row_extent_at(self, row, column): """ Gets the number of rows spanned by the table cell at the specific row and column. (some tables can have cells which span multiple rows and/or columns). The returned values are meaningful only if the Table has both STATE_VISIBLE and STATE_SHOWING. """ return AccessibleTable.get_row_extent_at(self, row, column) def get_row_header(self, row): """ Gets the header associated with a table row, if available. This differs from Atspi.Table.get_row_description, which returns a string. """ return AccessibleTable.get_row_header(self, row) def get_selected_columns(self): """ Queries a table for a list of indices of columns which are currently selected. """ return AccessibleTable.get_selected_columns(self) def get_selected_rows(self): """ Queries a table for a list of indices of rows which are currently selected. """ return AccessibleTable.get_selected_rows(self) def get_summary(self): """ Gets an accessible object which summarizes the contents of an Atspi.Table. """ return AccessibleTable.get_summary(self) def is_column_selected(self, column): """ Determines whether specified table column is selected. Not all tables support column selection. """ return AccessibleTable.is_column_selected(self, column) def is_row_selected(self, row): """ Determines whether a table row is selected. Not all tables support row selection. """ return AccessibleTable.is_row_selected(self, row) def is_selected(self, row, column): """ Determines whether the cell at a specific row and column is selected. """ return AccessibleTable.is_selected(self, row, column) def remove_column_selection(self, column): """ De-selects the specified column, removing it from the current column selection. Not all tables support column selection. """ return AccessibleTable.remove_column_selection(self, column) def remove_row_selection(self, row): """ De-selects the specified row, removing it from the current row selection. Not all tables support row selection. """ return AccessibleTable.remove_row_selection(self, row) def get_column_header_cells(self): """ Returns the column headers as an array of cell accessibles. """ return AccessibleTableCell.get_column_header_cells(self) def get_column_index(self): """ Returns the column index. """ return AccessibleTableCell.get_column_index(self) def get_column_span(self): """ Returns the number of columns occupied by this cell accessible. The returned values are meaningful only if the table cell has both STATE_VISIBLE and STATE_SHOWING. """ return AccessibleTableCell.get_column_span(self) def get_cell_position(self): """ Retrieves the tabular position of this cell. """ return AccessibleTableCell.get_position(self) def get_row_column_span(self): """ Gets the row and column indexes and extents of this cell accessible. The returned values are meaningful only if the table cell has both STATE_VISIBLE and STATE_SHOWING. """ return AccessibleTableCell.get_row_column_span(self) def get_row_header_cells(self): """ Returns the row headers as an array of cell accessibles. """ return AccessibleTableCell.get_row_header_cells(self) def get_row_span(self): """ Returns the number of rows occupied by this cell accessible. The returned values are meaningful only if the table cell has both STATE_VISIBLE and STATE_SHOWING. """ return AccessibleTableCell.get_row_span(self) def get_table(self): """ Returns a reference to the accessible of the containing table. """ return AccessibleTableCell.get_table(self) def add_selection(self, start_offset, end_offset): """ Selects some text (adds a text selection) in an Atspi.Text object. """ return AccessibleText.add_selection(self, start_offset, end_offset) def get_attribute_run(self, offset, include_defaults): """ Gets a set of attributes applied to a range of text from an Atspi.Text object, optionally including its 'default' attributes. """ return AccessibleText.get_attribute_run(self, offset, include_defaults) def get_bounded_ranges(self, x, y, width, height, coord_type, clip_type_x, clip_type_y): """ Gets the ranges of text from an Atspi.Text object which lie within the bounds defined by (x, y) and (x+'width', y+'height'). """ return AccessibleText.get_bounded_ranges( self, x, y, width, height, coord_type, clip_type_x, clip_type_y ) def get_caret_offset(self): """ Gets the current offset of the text caret in an Atspi.Text object. """ return AccessibleText.get_caret_offset(self) def get_character_at_offset(self, offset): """ Gets the character at a given offset for an Atspi.Text object. """ return AccessibleText.get_character_at_offset(self, offset) def get_character_count(self): """ Gets the character count of an #AccessibleText object. """ return AccessibleText.get_character_count(self) def get_character_extents(self, offset, coord_type): """ Gets a bounding box containing the glyph representing the character at a particular text offset. The returned values are meaningful only if the Text has both STATE_VISIBLE and STATE_SHOWING. """ return AccessibleText.get_character_extents(self, offset, coord_type) def get_default_attributes(self): """ Gets the default attributes applied to an Atspi.Text object. The text attributes correspond to CSS attributes where possible. The combination of this attribute set and the attributes reported by Atspi.Text.get_text_attributes describes the entire set of text attributes over a range. """ return AccessibleText.get_default_attributes(self) def get_n_selections(self): """ Gets the number of active non-contiguous selections for an Atspi.Text object. """ return AccessibleText.get_n_selections(self) def get_offset_at_point(self, x, y, coord_type): """ Gets the character offset into the text at a given point. """ return AccessibleText.get_offset_at_point(self, x, y, coord_type) def get_range_extents(self, start_offset, end_offset, coord_type): """ Gets the bounding box for text within a range in an Atspi.Text object. The returned values are meaningful only if the Text has both STATE_VISIBLE and STATE_SHOWING. """ return AccessibleText.get_range_extents(self, start_offset, end_offset, coord_type) def get_selection(self, selection_num): """ Gets the bounds of the selection_num-th active text selection for an Atspi.Text object. """ return AccessibleText.get_selection(self, selection_num) def get_string_at_offset(self, offset, granularity): """ Gets a portion of the text exposed through an Atspi.Text according to a given offset and a specific granularity, along with the start and end offsets defining the boundaries of such a portion of text. New in version Atspi 2.9.90. """ return AccessibleText.get_string_at_offset(self, offset, granularity) def get_text(self, start_offset, end_offset): """ Gets a range of text from an Atspi.Text object. The number of bytes in the returned string may exceed either end_offset or start_offset, since UTF-8 is a variable-width encoding. """ return AccessibleText.get_text(self, start_offset, end_offset) def get_text_after_offset(self, offset, coord_type): """ Gets delimited text from an Atspi.Text object which follows a given text offset. """ return AccessibleText.get_text_after_offset(self, offset, coord_type) def get_text_at_offset(self, offset, coord_type): """ Gets delimited text from an Atspi.Text object which includes a given text offset. Deprecated since version 2.10. Use Atspi.Text.get_string_at_offset. """ return AccessibleText.get_text_at_offset(self, offset, coord_type) def get_text_attribute_value(self, offset, attribute_name): """ Gets the value of a named attribute at a given offset. Deprecated since version 2.10: Use Atspi.Text.get_text_attribute_value instead. Note: The deprecation note what to use seems wrong. """ return AccessibleText.get_text_attribute_value(self, offset, attribute_name) def get_text_attributes(self, offset): """ Gets the attributes applied to a range of text from an Atspi.Text object. The text attributes correspond to CSS attributes where possible. Deprecated since version 2.10: Use Atspi.Text.get_text_attributes instead. Note: The deprecation note what to use seems wrong. """ return AccessibleText.get_text_attributes(self, offset) def get_text_before_offset(self, offset, coord_type): """ Gets delimited text from an Atspi.Text object which precedes a given text offset. """ return AccessibleText.get_text_before_offset(self, offset, coord_type) def remove_selection(self, selection_num): """ De-selects a text selection. """ return AccessibleText.remove_selection(self, selection_num) def scroll_substring_to(self, start_offset, end_offset, coord_type): """ Scrolls whatever container of the Atspi.Text text range so it becomes visible on the screen. """ return AccessibleText.scroll_substring_to(self, start_offset, end_offset, coord_type) def scroll_substring_to_point(self, start_offset, end_offset, coord_type, x, y): """ Scrolls whatever container of the Atspi.Text text range so it becomes visible on the screen at a given position. """ return AccessibleText.scroll_substring_to_point( self, start_offset, end_offset, coord_type, x, y ) def set_caret_offset(self, new_offset): """ Moves the text caret to a given position. """ return AccessibleText.set_caret_offset(self, new_offset) def set_selection(self, selection_num, start_offset, end_offset): """ Changes the bounds of an existing Atspi.Text text selection. """ return AccessibleText.set_selection(self, selection_num, start_offset, end_offset) @property @logging_class.do_not_log def text(self): """ Define a 'text' property for Atspi object. """ return AccessibleText.get_all_text(self) @text.setter def text(self, content): """ Define a 'text' setter for Atspi.Text object. """ return AccessibleEditableText.set_text_contents(self, content) def get_current_value(self): """ Gets the current value for an Atspi.Value. """ return AccessibleValue.get_current_value(self) @property def value(self): """ Gets the current value for an Atspi.Value. """ return self.get_current_value() @value.setter def value(self, new_value): """ Gets the current value for an Atspi.Value. """ return AccessibleValue.set_current_value(self, new_value) def get_maximum_value(self): """ Gets the maximum allowed value for an Atspi.Value. """ return AccessibleValue.get_maximum_value(self) @property def max_value(self): """ Gets the maximum allowed value for an Atspi.Value. """ return self.get_maximum_value() def get_minimum_increment(self): """ Gets the minimum increment by which an Atspi.Value can be adjusted. """ return AccessibleValue.get_minimum_increment(self) @property def min_value_increment(self): """ Gets the minimum increment by which an Atspi.Value can be adjusted. """ return self.get_minimum_increment() def get_minimum_value(self): """ Gets the minimum allowed value for an Atspi.Value. """ return AccessibleValue.get_minimum_value(self) @property def min_value(self): """ Gets the minimum allowed value for an Atspi.Value. """ return self.get_minimum_value() @property @logging_class.do_not_log def toolkit(self): """ Return string description of the toolkit we are using. """ return AccessibleUtilities.get_toolkit(self) @property def window_id(self): """ Return window_id of a node. """ return ponytail_helper.get_window_id(self) @property def window_has_focus(self): """ Check if window is focused. """ if self.session_type != "wayland": LOGGING.info("Ponytail is not used in non Wayland session.") return False return ponytail_helper.get_window_has_focus() @property def id(self): """ Define a 'id' property for Atspi object. """ return AccessibleObject.get_id(self) @property def accessible_id(self): """ Define a 'accessible_id' property for Atspi object. """ return AccessibleObject.get_accessible_id(self) @property @logging_class.do_not_log def name(self): """ Define a 'name' property for Atspi object. """ return AccessibleObject.get_name(self) @property @logging_class.do_not_log def role(self): """ Define a 'role property for Atspi object. """ return AccessibleObject.get_role(self) @property @logging_class.do_not_log def role_name(self): """ Define a 'role_name' property for Atspi object. """ return AccessibleObject.get_role_name(self) @property @logging_class.do_not_log def description(self): """ Define a 'description' property for Atspi object. """ return AccessibleObject.get_description(self) @property @logging_class.do_not_log def parent(self): """ Define a 'parent' property for Atspi object. """ return AccessibleObject.get_parent(self) @property @logging_class.do_not_log def index_in_parent(self): """ Define a 'index_in_parent' property for Atspi object. """ return AccessibleObject.get_index_in_parent(self) @property def attributes(self): """ Define a 'attributes' property for Atspi object. """ return AccessibleObject.get_attributes(self) @property def attributes_as_array(self): """ Define a 'attributes_as_array' property for Atspi object. """ return AccessibleObject.get_attributes_as_array(self) @property @logging_class.do_not_log def children(self): """ Creating children easy navigation when required. """ return AccessibleObject.get_children(self) @property def dead(self): """ Is the node dead (defunct)? """ return AccessibleObject.is_dead(self) def child(self, name=None, role_name=None, description=None, label=None, **kwargs): """ Find a single child satisfying the name role_name or description. """ using_identifier = None using_role_name = role_name # === Backward compatibility and extra parameters === allowed_kwargs = {"roleName", "identifier"} for key, val in kwargs.items(): if "roleName" in str(key): using_role_name = val if "identifier" in str(key): using_identifier = val # Arguments were handled, do not pass them to another method. for allowed_argument in allowed_kwargs: kwargs.pop(allowed_argument, False) # =================================================== predicate = GenericPredicate( name=name, role_name=using_role_name, description=description, label=label, identifier=using_identifier ) return AccessibleObject.find_descendant(self, predicate, **kwargs) def is_child(self, name=None, role_name=None, description=None, label=None, **kwargs): """ Determines whether a child satisfying the given criteria exists. """ using_identifier = None using_role_name = role_name # === Backward compatibility and extra parameters === allowed_kwargs = {"roleName", "identifier"} for key, val in kwargs.items(): if "roleName" in str(key): using_role_name = val if "identifier" in str(key): using_identifier = val # Arguments were handled, do not pass them to another method. for allowed_argument in allowed_kwargs: kwargs.pop(allowed_argument, False) # =================================================== predicate = GenericPredicate( name=name, role_name=using_role_name, description=description, label=label, identifier=using_identifier ) # Execute the search. try: # We do not care about a result here, just existence. AccessibleObject.find_descendant(self, predicate, **kwargs) return True except SearchError: return False def find_ancestor(self, predicate, **kwargs): """ Find a single ancestor satisfying the predicate. """ return AccessibleObject.find_ancestor(self, predicate, **kwargs) def find_child(self, predicate, **kwargs): """ Find a single child satisfying the predicate. """ return AccessibleObject.find_descendant(self, predicate, **kwargs) def find_children(self, predicate, **kwargs): """ Find a all children satisfying the predicate. """ return AccessibleObject.find_all_descendants(self, predicate, **kwargs) @property def last_child(self): """ Is this Accessible Node the last child in parent node? """ return AccessibleObject.is_last_child(self) @property def child_count(self): """ Get how many children Accessible Node has. """ return AccessibleObject.get_child_count(self) @property def labeler(self): """ 'labeller' (read-only list of Node instances): The node(s) that is/are a label for this node. Generated from 'relations'. """ return AccessibleObject.get_labeler(self) @property def labelee(self): """ 'labelee' (read-only list of Node instances): The node(s) that this node is a label for. Generated from 'relations'. """ return AccessibleObject.get_labelee(self) def click(self, button=1, offset_x=0, offset_y=0): """ Generates a raw mouse click event, using the specified button. - 1 is left, - 2 is middle, - 3 is right. """ click_x = int(self.position[0] + self.size[0] / 2) + offset_x click_y = int(self.position[1] + self.size[1] / 2) + offset_y # Starting with no workarounds for any role names or objects. # If something will start crashing we can fix it easily. click(click_x, click_y, button, window_id=self.window_id) def double_click(self, button=1, offset_x=0, offset_y=0): """ Generates a raw mouse double-click event, using the specified button. """ click_x = int(self.position[0] + self.size[0] / 2) + offset_x click_y = int(self.position[1] + self.size[1] / 2) + offset_y # Starting with no workarounds for any role names or objects. # If something will start crashing we can fix it easily. double_click(click_x, click_y, button, window_id=self.window_id) def point(self, offset_x=0, offset_y=0): """ Move mouse cursor to the center of the widget. """ point_x = int(self.position[0] + self.size[0] / 2) + offset_x point_y = int(self.position[1] + self.size[1] / 2) + offset_y # Starting with no workarounds for any role names or objects. # If something will start crashing we can fix it easily. point(point_x, point_y, window_id=self.window_id) def dump(self, output_type="plain", file_name=None, labels=False): """ Dumping a structure representation. """ from dogtail.dump import AccessibleStructureRepresentation if output_type == "plain": print(AccessibleStructureRepresentation(self, "plain", file_name=file_name, labels=labels)) elif output_type == "verbose": print(AccessibleStructureRepresentation(self, "verbose", file_name=file_name, labels=labels)) elif output_type == "tree": # Print done by tree method. self.tree(file_name=file_name, labels=labels) else: LOGGING.info(f"Unknown output type: '{output_type}'") def tree(self, file_name=None, labels=False): """ Dumping a tree structure of the node in tree like representation. """ from dogtail.dump import AccessibleStructureRepresentation print(AccessibleStructureRepresentation(self, "tree", file_name=file_name, labels=labels)) def blink(self): """ Blink will highlight the current node on the screen. Using Blinker from utility. """ if not self.extents: return False #from dogtail.utils import Blinker #(x, y, w, h) = self.extents #Blinker(x, y, w, h) return True @staticmethod def application(application_name): """ Gets an application by name, returning an Application instance or raising an exception. """ return AccessibleRoot.application(application_name) @staticmethod def applications(): """ Get all applications. """ return AccessibleRoot.applications() ######################################## # Backwards compatibility definitions. # ######################################## def __setupUserData(self): # pylint: disable=invalid-name """ Setup user data dictionary. """ try: len(self.user_data) except (AttributeError, TypeError): self.user_data = {} # pylint: disable=attribute-defined-outside-init @property def debugName(self): # pylint: disable=invalid-name """ Debug name assigned during search operations. """ self.__setupUserData() return self.user_data.get("debugName", None) @debugName.setter def debugName(self, debugName): # pylint: disable=invalid-name """ Debug name setter. """ self.__setupUserData() self.user_data["debugName"] = debugName @property def combovalue(self): """ The value (as a string) currently selected in the combo box. """ return self.name @combovalue.setter def combovalue(self, value): """ Set the value (as a string) in the combo box. """ self.childNamed(childName=value).doActionNamed("click") do_delay() @property def isChecked(self): # pylint: disable=invalid-name """ Is the Accessible a checked checkbox? Compatibility property, same as Node.checked. """ return self.checked @property def isSelected(self): # pylint: disable=invalid-name """ Is the Accessible selected? """ return self.selected @property def selectedChildren(self): # pylint: disable=invalid-name """ Returns a list of children that are selected. """ selected_children = [] for index in range(AccessibleSelection.get_n_selected_children(self)): selected_children.append(AccessibleSelection.get_selected_child(self, index)) return selected_children @property def roleName(self): # pylint: disable=invalid-name """ For backwards compatibility reasons. Wrapper for self.role_name. """ return self.role_name @property def minValue(self): # pylint: disable=invalid-name """ For backwards compatibility reasons. Wrapper for self.min_value. """ return self.min_value @property def maxValue(self): # pylint: disable=invalid-name """ For backwards compatibility reasons. Wrapper for self.max_value. """ return self.max_value @property def minValueIncrement(self): # pylint: disable=invalid-name """ For backwards compatibility reasons. Wrapper for self.min_value_increment. """ return self.min_value_increment def doActionNamed(self, named_action): # pylint: disable=invalid-name """ For backwards compatibility reasons. Wrapper for self.do_action_named. """ return self.do_action_named(named_action) def grabFocus(self): # pylint: disable=invalid-name """ For backwards compatibility reasons. Wrapper for self.grab_focus. """ return self.grab_focus() @property def indexInParent(self): # pylint: disable=invalid-name """ For backwards compatibility reasons. Wrapper for self.index_in_parent. """ return self.index_in_parent def isChild(self, name=None, roleName=None, description=None, label=None, **kwargs): # pylint: disable=invalid-name """ For backwards compatibility reasons. Wrapper for self.is_child. """ return self.is_child( name=name, role_name=roleName, description=description, label=label, **kwargs ) def menu(self, menuName, recursive=True, showingOnly=None): # pylint: disable=invalid-name """ Search below this node for a menu with the given name. """ return self.find_child( IsAMenuNamed(name=menuName), recursive=recursive, showing_only=showingOnly ) def menuItem(self, menuItemName, recursive=True, showingOnly=None): # pylint: disable=invalid-name """ Search below this node for a menu item with the given name. """ return self.find_child( IsAMenuItemNamed(name=menuItemName), recursive=recursive, showing_only=showingOnly ) def textentry(self, textEntryName, recursive=True, showingOnly=None): # pylint: disable=invalid-name """ Search below this node for a text entry with the given name. """ return self.find_child( IsATextEntryNamed(name=textEntryName), recursive=recursive, showing_only=showingOnly ) def button(self, buttonName, recursive=True, showingOnly=None): # pylint: disable=invalid-name """ Search below this node for a button with the given name. The 'button' and 'push button' are represented by the same integer, no need to separate them. """ return self.find_child( lambda x: x.name == buttonName and x.roleName in ("button", "push button"), recursive=recursive, showing_only=showingOnly ) def childLabelled(self, labelText, recursive=True, showingOnly=None): # pylint: disable=invalid-name """ Search below this node for a child labelled with the given text. """ return self.find_child( IsLabelledAs(label=labelText), recursive=recursive, showing_only=showingOnly ) def childNamed(self, childName, recursive=True, showingOnly=None): # pylint: disable=invalid-name """ Search below this node for a child with the given name. """ return self.find_child( IsNamed(name=childName), recursive=recursive, showing_only=showingOnly ) def tab(self, childName, recursive=True, showingOnly=None): # pylint: disable=invalid-name """ Search below this node for a tab with the given name. """ return self.find_child( IsATabNamed(name=childName), recursive=recursive, showing_only=showingOnly ) def dialog(self, dialogName, recursive=True, showingOnly=None): # pylint: disable=invalid-name """ Search below this node for a tab with the given name. """ return self.find_child( IsADialogNamed(name=dialogName), recursive=recursive, showing_only=showingOnly ) def window(self, windowName, recursive=True, showingOnly=None): # pylint: disable=invalid-name """ Search below this node for a tab with the given name. """ return self.find_child( IsAWindowNamed(name=windowName), recursive=recursive, showing_only=showingOnly ) def findAncestor(self, predicate, retry=True): # pylint: disable=invalid-name """ For backwards compatibility reasons. Wrapper for self.find_ancestor. """ return self.find_ancestor(predicate, retry=retry) def findChild(self, predicate, retry=True): # pylint: disable=invalid-name """ For backwards compatibility reasons. Wrapper for self.find_child. """ return self.find_child(predicate, retry=retry) def findChildren(self, predicate): # pylint: disable=invalid-name """ For backwards compatibility reasons. Wrapper for self.find_children. """ return self.find_children(predicate) def selectAll(self): # pylint: disable=invalid-name """ Selects all children. """ return self.select_all() def deselectAll(self): # pylint: disable=invalid-name """ Deselects all children. """ return self.deselect_all() def doubleClick(self, button=1): # pylint: disable=invalid-name """ Make a double-click. """ return self.double_click(button=button) @property def link(self): # pylint: disable=invalid-name """ Get Link from HyperText object. """ try: if AccessibleObject.is_newton(self): # TODO return None for link_index in Atspi.HyperText.get_n_links(self): return Atspi.HyperText.get_link(self, link_index) except (NotImplementedError, AttributeError) as error: raise RuntimeError from error @property def URI(self): # pylint: disable=invalid-name """ Get Link Anchor from HyperLink object. """ try: if AccessibleObject.is_newton(self): # TODO return None for link_index in Atspi.HyperText.get_n_links(self): link = Atspi.HyperText.get_link(self, link_index) for anchor_index in link.get_n_anchors(link_index): return link.get_uri(self, anchor_index) except (NotImplementedError, AttributeError) as error: raise RuntimeError from error def satisfies(self, pred): """ Does this node satisfy the given predicate? """ assert isinstance(pred, GenericPredicate) return pred.satisfied_by_node(self) @property def labellee(self): # pylint: disable=invalid-name """ 'labelee' (read-only list of Node instances): The node(s) that this node is a label for. Generated from 'relations'. """ return self.labelee() ############################################### # End of backwards compatibility definitions. # ############################################### @logging_class.do_not_log def __len__(self): return AccessibleObject.get_child_count(self) @logging_class.do_not_log def __getitem__(self, index): length = len(self) if index < 0: index = index + length if index < 0 or index >= length: raise IndexError return AccessibleObject.get_child_at_index(self, index) @logging_class.do_not_log def __bool__(self): return self is not None @logging_class.do_not_log def __nonzero__(self): return self is not None @logging_class.do_not_log def __str__(self): try: return f"[ '{self.name}' | '{self.role_name}' | '{self.description}' ]" except Exception as error: return f"[Exception encountered]: {error}" Atspi.Accessible.__bases__ = ( Node, ) + Atspi.Accessible.__bases__ # Load desktop to the root variable. try: root = AccessibleUtilities.get_desktop() except Exception: LOGGING.info("Error: Accessible desktop is not visible. Is accessibility enabled?") # The 'children' field on desktop is inherited Field from GObject.Object # We are recoding this logic in DogtailAPI so that the Object # in 'children' is Accessible instead of default [object]. try: del Atspi.Accessible.children except AttributeError: # Might be already deleted. pass # To keep the same keyword we need to remove the Atspi.Accessible. # So that the API is forced to use DogtailAPI. and we can adjust it for Newton try: del Atspi.Accessible.parent except AttributeError: # Might be already deleted. pass try: del Atspi.Accessible.name except AttributeError: # Might be already deleted. pass try: del Atspi.Accessible.description except AttributeError: # Might be already deleted. pass try: del Atspi.Accessible.attributes except AttributeError: # Might be already deleted. pass dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/utils.py000066400000000000000000000635361520163156700241720ustar00rootroot00000000000000#!/usr/bin/python3 """ The utility module for dogtail. """ # pylint: disable=broad-exception-caught # pylint: disable=wrong-import-position # pylint: disable=wrong-import-order # pylint: disable=import-error # pylint: disable=import-outside-toplevel # pylint: disable=no-name-in-module # ruff: noqa: E402 # ruff: noqa: E501 from time import sleep from pathlib import Path import os import sys import errno import shlex import subprocess import functools import gi gi.require_version("Gtk", "3.0") gi.require_version("Gdk", "3.0") from gi.repository import Gtk from dogtail.logging import logging_class LOGGING = logging_class.logger from dogtail.config import config def screenshot(file="" , time_stamp=True, datetime_string = "%Y%m%d-%H:%M:%S"): """ Screenshot using the DBus. :param file: File to save the screenshot as, defaults to "screenshot.png" :type file: str, optional :param time_stamp: Option to include time stamp or not, defaults to True :type time_stamp: bool, optional :param datetime_string: _description_, defaults to "%Y%m%d-%H:%M:%S" :type datetime_string: str, optional :raises TypeError: File must be string or Path :raises TypeError: Time Stamp must be True or False :raises TypeError: Currently supporting only PNG. :return: Set of True or False based on success or failure and file location string. :rtype: Tuple(bool, str) """ LOGGING.debug("Capturing Screenshot via DBus.") save_file_as = None if not isinstance(file, str) and not isinstance(file, Path): raise TypeError("file name must be string or Path*") if not isinstance(time_stamp, bool): raise TypeError("time_stamp must be True or False") try: from dasbus.connection import SessionMessageBus import datetime if file: save_file_as = str(file) else: save_file_as = "screenshot.png" if ".png" not in save_file_as: raise TypeError("Currently supporting only PNG format.") if time_stamp: time_stamp_string = datetime.datetime.now().strftime(datetime_string) base_name, extension = save_file_as.rsplit(".", 1) save_file_as = base_name + "-" + time_stamp_string + "." + extension user_id = str(os.geteuid()) os.environ["DBUS_SESSION_BUS_ADDRESS"] = f"unix:path=/run/user/{user_id}/bus" bus = SessionMessageBus() proxy = bus.get_proxy( "org.gnome.Shell.Screenshot", "/org/gnome/Shell/Screenshot" ) screenshot_call = proxy.Screenshot( True, False, save_file_as ) return screenshot_call except Exception as error: LOGGING.info(f"Capturing Screenshot via DBus failed with error: '{error}'.") def do_delay(delay=None): """ Utility function to insert a delay (with logging and a configurable default delay). """ LOGGING.debug(logging_class.get_func_params_and_values()) if delay is None: delay = config.default_delay LOGGING.debug(f"Default Delay: Sleeping for '{delay}'") if config.debug_sleep: LOGGING.debug(f"Debug Sleep: Sleeping for '{delay}'") sleep(float(delay)) def run(string, timeout=None, interval=None, desktop=None, dumb=False, app_name="", **kwargs,): """ Runs an application. [For simple command execution such as 'rm *', use os.popen() or os.system()] If dumb is omitted or is False, polls at interval seconds until the application is finished starting, or until timeout is reached. If dumb is True, returns when timeout is reached. """ LOGGING.debug(logging_class.get_func_params_and_values()) # Users can change timeout and interval via config.ini or by parameter in function. validated_timeout = config.run_timeout if timeout is None else timeout validated_interval = config.run_interval if interval is None else interval # Use root if not desktop was given. Why desktop though? This is weird. if desktop is None: from dogtail.tree import root else: root = desktop args = shlex.split(string) os.environ["GTK_MODULES"] = "gail:atk-bridge" process = subprocess.Popen(args, env=os.environ) validated_application_name = app_name # === Backward compatibility for 'appName' usage === allowed_kwargs = {"appName"} for key, val in kwargs.items(): if "appName" in str(key): validated_application_name = val # Handle possible leftovers and raise TypeError on unexpected value. else: unexpected_arguments = ", ".join(set(kwargs) - allowed_kwargs) function_name = run.__name__ raise TypeError( f"{function_name}() got an unexpected keyword argument(s): {unexpected_arguments}" ) # Arguments were handled, do not pass them to another method. for allowed_argument in allowed_kwargs: kwargs.pop(allowed_argument, False) # ================================================== if not validated_application_name: validated_application_name = args[0] if dumb: LOGGING.debug("Disable startup detection.") do_delay(validated_timeout) else: LOGGING.debug("Startup detection code.") time = 0 while time < validated_timeout: time = time + validated_interval try: application = root.application(validated_application_name) assert application.child(role_name="frame") do_delay(validated_interval) return process except Exception: pass do_delay(validated_interval) return process class Lock: """ A known system-wide mutex implementation that uses atomicity of the mkdir operation in UNIX-like systems. This should be used mainly to provide mutual exclusion in handling possible collisions among multiple script instances. You can choose to make randomized single-script wise locks or a more general locks if you do not choose to randomize the lock dir name. Set unLockOnExit to True to enable automatic unlock when script process exits to avoid having to unlock manually. """ def __init__( self, location="/tmp", lockname="dogtail_lockdir_", randomize=True, unlock_on_exit=False, **kwargs, ): """ You can change the default lockdir location or name. Setting randomize to False will result in no random string being appended to the lockdir name. """ using_unlock_on_exit = unlock_on_exit # === Backward compatibility and extra parameters === allowed_kwargs = {"unlockOnExit"} for key, val in kwargs.items(): if "unlockOnExit" in str(key): using_unlock_on_exit = val # Handle possible leftovers and raise TypeError on unexpected value. else: unexpected_arguments = ", ".join(set(kwargs) - allowed_kwargs) function_name = Lock.__init__.__name__ raise TypeError( f"{function_name}() got an unexpected keyword argument(s): {unexpected_arguments}" ) # Arguments were handled, do not pass them to another method. for allowed_argument in allowed_kwargs: kwargs.pop(allowed_argument, False) # =================================================== self.lockdir = os.path.join(os.path.normpath(location), lockname) if randomize: self.lockdir = "".join((self.lockdir, self.__getPostfix())) self.unlock_on_exit = using_unlock_on_exit def __exit_unlock(self): """ Removes the lock upon exiting headless. """ #debug_log("Remove the lock. Raising the exception if the lock is not present.") if os.path.exists(self.lockdir): try: os.rmdir(self.lockdir) except OSError: pass # already deleted (by .unlock()), we're exiting, it's fine def lock(self): """ Creates a lockdir based on the settings on Lock() instance creation. Raises OSError exception of the lock is already present. Should be atomic on POSIX compliant systems. """ LOGGING.debug(" ".join(( "Create a lock directory." "Raising the exception if the lock is already present." )) ) locked_msg = "Dogtail lock: Already locked with the same lock." if not os.path.exists(self.lockdir): try: os.mkdir(self.lockdir) except OSError as error: if error.errno == errno.EEXIST and os.path.isdir(self.lockdir): raise OSError(locked_msg) from error if os.path.exists(self.lockdir): if self.unlockOnExit: import atexit atexit.register(self.__exit_unlock) return self.lockdir else: raise OSError(locked_msg) def unlock(self): """ Removes a lock. Will raise OSError exception if the lock was not present. Should be atomic on POSIX compliant systems. """ LOGGING.debug("Remove the lock. Raising the exception if the lock is not present.") if self.unlock_on_exit: raise RuntimeError("Cannot unlock with unlock_on_exit set to True!") if os.path.exists(self.lockdir): try: os.rmdir(self.lockdir) except OSError as error: if error.errno == errno.EEXIST: raise OSError("Dogtail unlock: lockdir removed elsewhere!") from error else: raise OSError("Dogtail unlock: not locked") def locked(self): """ Check if locked directory exists. """ LOGGING.debug("Checking if locked directory exists.") return os.path.exists(self.lockdir) # More commonly used word as opposed to postfix. def __get_suffix(self): """ Generate a length of 5 random string that serves as file name suffix. """ import random import string LOGGING.debug("Get random file suffix of length 5.") return "".join( random.choice(string.ascii_letters + string.digits) for x in range(5) ) ######################################## # Backwards compatibility definitions. # ######################################## def __getPostfix(self): # pylint: disable=invalid-name """ Wrapper over __get_suffix. """ return self.__get_suffix() @property def unlockOnExit(self): # pylint: disable=invalid-name """ Making a property in case users would like to access it and change it. """ return self.unlock_on_exit @unlockOnExit.setter def unlockOnExit(self, value_to_set): # pylint: disable=invalid-name """ Setter for unlockOnExit. """ assert isinstance(value_to_set, bool), "unlockOnExit has to be of type 'bool'." self.unlock_on_exit = value_to_set ############################################### # End of backwards compatibility definitions. # ############################################### def bail_because_accessibility_is_disabled(): """ Accessibility is detected as enabled. End the execution if there are no exceptions. """ LOGGING.debug(logging_class.get_func_params_and_values()) if sys.argv[0].endswith("pydoc"): LOGGING.info("Execution was not ended. Script name with 'pydoc' exception.") return try: with open(f"/proc/{os.getpid()}/cmdline", "r", encoding="utf-8") as f: content = f.read() if content.find("epydoc") != -1: LOGGING.info("Execution was not ended. Process content 'epydoc' exception.") return if content.find("sphinx") != -1: LOGGING.info("Execution was not ended. Process content 'sphinx' exception.") return except Exception as error: LOGGING.info(f"Exception caught, but the execution is ending anyway: '{error}'") LOGGING.info("".join(( "Dogtail requires that Assistive Technology support to be enabled.", "You can enable accessibility with sniff or by running:", "'gsettings set org.gnome.desktop.interface toolkit-accessibility true'", "Aborting...", ))) sys.exit(1) def check_for_accessibility(): """ Checks if accessibility is enabled, and halts execution if it is not. """ LOGGING.debug(logging_class.get_func_params_and_values()) if not is_accessibility_enabled(): bail_because_accessibility_is_disabled() def enable_accessibility(enable=True): """ Enable Accessibility toolkit via dconf. """ LOGGING.debug(logging_class.get_func_params_and_values()) from gi.repository.Gio import Settings a11y_dconf_key = "org.gnome.desktop.interface" try: interface_settings = Settings(schema_id=a11y_dconf_key) except TypeError: interface_settings = Settings(schema=a11y_dconf_key) interface_settings.set_boolean("toolkit-accessibility", enable) def is_accessibility_enabled(): """ Checks if accessibility is enabled via DConf. """ LOGGING.debug(logging_class.get_func_params_and_values()) from gi.repository.Gio import Settings a11y_dconf_key = "org.gnome.desktop.interface" try: interface_settings = Settings(schema_id=a11y_dconf_key) except TypeError: interface_settings = Settings(schema=a11y_dconf_key) dconf_enabled = interface_settings.get_boolean("toolkit-accessibility") if os.environ.get("GTK_MODULES", "").find("gail:atk-bridge") == -1: env_enabled = False else: env_enabled = True return dconf_enabled or env_enabled def check_for_accessibility_interactively(): """ Checks if accessibility is enabled, and presents a dialog prompting the user if it should be enabled if it is not already, then halts execution. """ LOGGING.debug(logging_class.get_func_params_and_values()) if is_a11y_enabled(): LOGGING.debug("Accessibility already enabled.") return # Inspiration found in Gtk examples, lets not reinvent the wheel. class InteractiveDialog(Gtk.Dialog): """ Interactive Dialog that user can confirm or cancel. """ def __init__(self, parent): super().__init__(title="Are you sure?", transient_for=parent, flags=0) self.add_buttons( Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK ) self.set_default_size(300, 100) self.set_resizable(False) grid = Gtk.Grid(column_spacing=10, row_spacing=10) # TODO is sniff still enough to just enable the toolkit? # Nowadays we need ponytail, xauthority to properly function on wayland. # using qecore-headless might be the only way to to properly enable everything. # In which case this function might be useless. question = """ Dogtail requires Assistive Technology Support to be enabled for it to function. Would you like to enable Assistive Technology support now? Note that you might have to logout for the change to fully take effect. """ label = Gtk.Label(label=question) grid.add(label) grid.set_margin_end(20) box = self.get_content_area() #box.add(label) box.add(grid) self.show_all() class InteractiveWindow(Gtk.Window): """ Interactive window that will spawn question dialog. """ def __init__(self): Gtk.Window.__init__(self, title="Interactive Check for Accessibility toolkit.") self.set_border_width(6) grid = Gtk.Grid(column_spacing=10, row_spacing=10) grid.set_margin_bottom(20) grid.set_margin_top(20) grid.set_margin_start(20) grid.set_margin_end(20) grid.set_column_homogeneous(True) grid.set_row_homogeneous(False) button = Gtk.Button(label="Enable toolkit-accessibility") button.connect("clicked", self.on_button_clicked) grid.add(button) self.add(grid) def on_button_clicked(self, widget): # pylint: disable=unused-argument """ On click function. :param widget: Unused Gtk Button. :type widget: Gtk Button. """ dialog = InteractiveDialog(self) response = dialog.run() if response == Gtk.ResponseType.OK: LOGGING.info("Enabling Accessibility.") enable_accessibility() elif response == Gtk.ResponseType.CANCEL: LOGGING.info("Execution halted, Accessibility Enabling was Cancelled.") bail_because_accessibility_is_disabled() # Destroy the dialog. dialog.destroy() # Destroy the main window. self.destroy() win = InteractiveWindow() win.connect("destroy", Gtk.main_quit) win.show_all() Gtk.main() def wait_for_window(name, timeout=30): """ Wait for window to appear. Currently wayland only. name can be a window 'title' as reported via Node.name or an app id (from .desktop file. i.e. "org.gnome.Calculator.desktop") Returns true on success, false on x11. """ LOGGING.debug(logging_class.get_func_params_and_values()) from dogtail.rawinput import SESSION_TYPE from dogtail.ponytail_helper import ponytail_helper if SESSION_TYPE == "wayland": ponytail_interface = ponytail_helper.get_ponytail_interface() ponytail_interface.waitFor(name, timeout=timeout) return True return False def get_current_x_window_position(): """ This is a helper to get window position (top left corner) solely by means of Xlib (direct X calls) - without a11y. This is targeted to be used with !GTK4! apps only - which don't support giving GLOBAL coords of their nodes under Xorg (as well as on wayland). By getting the win location and adding that up together with local coords, we can make actions with GTK4 apps under Xorg as well. Not to be mixed with what we do with local coords and local functions on W. Imports are used locally here not to bring unnecessary deps in most cases - because this is for rather a corner case... GTK4 apps will mostly be run in wayland and with Xorg most likely much much less. """ LOGGING.debug(logging_class.get_func_params_and_values()) try: from Xlib import X, display from Xlib.error import XError except ModuleNotFoundError as error: raise ImportError("".join( ( "python-xlib is required for this script to run.", "Please install it i.e. using 'pip3 install python-xlib'.", ) ) ) from error _display = display.Display() root = _display.screen().root prop = root.get_full_property( _display.intern_atom("_NET_ACTIVE_WINDOW"), X.AnyPropertyType ) if prop is None: return 0, 0 window_id = prop.value[0] window = _display.create_resource_object("window", window_id) try: geom = window.get_geometry() return geom.x, geom.y except XError as error: LOGGING.info(f"Error getting current window position: {error}") return (0, 0) # Before cache #'0.0014488697052001953' seconds - first call #'0.0018050670623779297' seconds - all following calls # After cache #'0.0014488697052001953' seconds - first call #'2.384185791015625e-06' seconds - all following calls # Since 3.2 # Since 3.9 we could use @functools.cache @functools.lru_cache def get_screen_resolution(): """ Get resolution. :return: Resolution. :rtype: list """ LOGGING.debug(logging_class.get_func_params_and_values()) try: import dbus bus = dbus.SessionBus() obj = bus.get_object( "org.gnome.Mutter.DisplayConfig", "/org/gnome/Mutter/DisplayConfig" ) interface = dbus.Interface(obj, "org.gnome.Mutter.DisplayConfig") call_method = interface.get_dbus_method("GetCurrentState") method_call_output = call_method() # Unwrapping the values to deal with usual types and not dbus types. def unwrap(value): if isinstance(value, dbus.ByteArray): return "".join([str(x) for x in value]) if isinstance(value, (dbus.Array, list, tuple, dbus.Struct)): return [unwrap(x) for x in value] if isinstance(value, (dbus.Dictionary, dict)): return dict([(unwrap(x), unwrap(y)) for x, y in value.items()]) if isinstance(value, (dbus.Signature, dbus.String)): return str(value) if isinstance(value, dbus.Boolean): return bool(value) if isinstance( value, ( dbus.Int16, dbus.UInt16, dbus.Int32, dbus.UInt32, dbus.Int64, dbus.UInt64, ), ): return int(value) if isinstance(value, (dbus.Double)): return float(value) if isinstance(value, dbus.Byte): return bytes([int(value)]) return value # Recursive helper function to walk the data structure. # Data structure is searched for current resolution. def search_dbus_structure(dbus_object, previous_object=None): # Check if the structure is dictionary. if isinstance(dbus_object, dict): for key, value in dbus_object.items(): # Check for the wanted condition of a dictionary. # We need current display. if key == "is-current" and value is True: # If current object value is True # return previous object's values. return previous_object[1], previous_object[2] # Check the result from the recursive function. result_found = search_dbus_structure(value, dbus_object) # End if there was result present. Return the value. if result_found: return result_found # Check if the structure is iterable but not a string. elif hasattr(dbus_object, "__iter__") and not isinstance( dbus_object, str ): # Check all the items present in the object. for item in dbus_object: # Check the result from the recursive function. result_found = search_dbus_structure(item, dbus_object) # End if there was result present. Return the value. if result_found: return result_found resolution = search_dbus_structure(unwrap(method_call_output)) LOGGING.debug(f"Resolution detected: '{resolution}'") return resolution except Exception as error: resolution = f"The resolution retrieval failed for: {error}" LOGGING.info(f"resolution error: '{resolution}'") return (0,0) ######################################## # Backwards compatibility definitions. # ######################################## def doDelay(delay=None): # pylint: disable=invalid-name """ Do Delay. """ do_delay(delay=delay) def waitForWindow(name, timeout=30): # pylint: disable=invalid-name """ Wrapper over wait_for_window. """ return wait_for_window(name=name, timeout=timeout) def check_for_a11y_interactively(): """ Wrapper over check_for_accessibility_interactively. """ return check_for_accessibility_interactively() def checkForA11yInteractively(): # pylint: disable=invalid-name """ Wrapper over check_for_a11y_interactively. """ return check_for_accessibility_interactively() def enable_a11y(enable=True): """ Wrapper over enable_accessibility. """ return enable_accessibility(enable=enable) def enableA11y(enable=True): # pylint: disable=invalid-name """ Wrapper over enable_accessibility. """ return enable_accessibility(enable=enable) def check_for_a11y(): """ Wrapper over check_for_accessibility. """ return check_for_accessibility() def checkForA11y(): # pylint: disable=invalid-name """ Wrapper over check_for_accessibility. """ return check_for_accessibility() def is_a11y_enabled(): """ Wrapper over is_accessibility_enabled. """ return is_accessibility_enabled() def isA11yEnabled(): # pylint: disable=invalid-name """ Wrapper over is_accessibility_enabled. """ return is_accessibility_enabled() def bail_because_a11y_is_disabled(): """ Wrapper over bail_because_accessibility_is_disabled. """ return bail_because_accessibility_is_disabled() def bailBecauseA11yIsDisabled(): # pylint: disable=invalid-name """ Wrapper over bail_because_accessibility_is_disabled. """ return bail_because_accessibility_is_disabled() # TODO delete once all changes are applied. # Keeping for now to not break something along the way. class Highlight(Gtk.Window): # pylint: disable=missing-class-docstring def __init__(self): LOGGING.info("Deprecated on Wayland with release 2.0.") class Blinker: # pylint: disable=missing-class-docstring def __init__(self): LOGGING.info("Deprecated on Wayland with release 2.0.") ############################################### # End of backwards compatibility definitions. # ############################################### dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/dogtail/version.py000066400000000000000000000057751520163156700245200ustar00rootroot00000000000000#!/usr/bin/python3 """ Handles versioning of software packages. """ # pylint: disable=line-too-long # ruff: noqa: E501 __author__ = "Dave Malcolm " from packaging import version from dogtail.logging import logging_class LOGGING = logging_class.logger class Version: """ Class representing a version of a software package. Stored internally as a list of subversions, from major to minor. Overloaded comparison operators ought to work sanely. """ def __init__(self, version_list = None, versionList = None): # pylint: disable=invalid-name LOGGING.debug(f"Version class constructor: '{type(versionList)}':'{str(versionList)}'") self.validate_version_to_use = None # Validate inputs, keeping the old variable. if version_list is None and versionList is None: LOGGING.debug("No version provided. Using empty string.") self.api_version = "" elif version_list and versionList: LOGGING.debug(f"Two versions provided. Using the '{str(version_list)}'.") self.validate_version_to_use = version_list elif version_list: self.validate_version_to_use = version_list elif versionList: self.validate_version_to_use = versionList # Validate type of input. if isinstance(self.validate_version_to_use, list): self.api_version = ".".join(str(x) for x in self.validate_version_to_use) elif isinstance(self.validate_version_to_use, str): self.api_version = self.validate_version_to_use @classmethod def from_string(cls, version_string): """ Get Version Instance from string. :param version_string: String representation of version. :type version_string: str :return: Instance of Version. :rtype: Version """ LOGGING.debug("Getting version from Version.from_string deprecated, use constructor with string.") instance = Version(version_string) return instance @classmethod def fromString(cls, versionString): # pylint: disable=invalid-name """ Get Version Instance from string. Wrapper around from_string for compatibility. """ return Version.from_string(version_string=versionString) def __str__(self): return self.api_version def __lt__(self, other): return version.parse(self.api_version) < version.parse(other.api_version) def __le__(self, other): return version.parse(self.api_version) <= version.parse(other.api_version) def __eq__(self, other): return version.parse(self.api_version) == version.parse(other.api_version) def __ne__(self, other): return version.parse(self.api_version) != version.parse(other.api_version) def __gt__(self, other): return version.parse(self.api_version) > version.parse(other.api_version) def __ge__(self, other): return version.parse(self.api_version) >= version.parse(other.api_version) dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/epydoc.conf000066400000000000000000000002071520163156700231510ustar00rootroot00000000000000[epydoc] modules: dogtail name: dogtail url: https://gitlab.com/dogtail/dogtail target: api_docs css: default #frames: no private: yes dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/examples/000077500000000000000000000000001520163156700226365ustar00rootroot00000000000000dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/examples/atspi_events_listener_script.py000077500000000000000000000114621520163156700312140ustar00rootroot00000000000000#!/usr/bin/python3 """ Event listener demo. """ # pylint: disable=import-outside-toplevel # pylint: disable=wrong-import-position # pylint: disable=wrong-import-order # pylint: disable=broad-exception-caught # ruff: noqa: E402 # ruff: noqa: E501 import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi from gi.repository import GLib from time import sleep # Explicit list of selected events: # https://gitlab.gnome.org/GNOME/at-spi2-core/-/blob/HEAD/atspi/atspi-event-listener.c#L479 # Under Wayland the mouse events are not captured, possibly by design. EVENT_NAMES = [ "focus:", "mouse:rel", "mouse:button", "mouse:abs", "keyboard:modifiers", # The property-change event will trigger together with the sub-event, leaving it out for now. #"object:property-change", "object:property-change:accessible-name", "object:property-change:accessible-description", "object:property-change:accessible-parent", "object:state-changed", "object:state-changed:focused", "object:selection-changed", "object:children-changed", "object:active-descendant-changed", "object:visible-data-changed", "object:text-selection-changed", "object:text-caret-moved", "object:text-changed", "object:column-inserted", "object:row-inserted", "object:column-reordered", "object:row-reordered", "object:column-deleted", "object:row-deleted", "object:model-changed", "object:link-selected", # Avoid swamping the log. Does not seem to do so on Wayland. #"object:bounds-changed", "window:minimize", "window:maximize", "window:restore", "window:activate", "window:create", "window:deactivate", "window:close", "window:lower", "window:raise", "window:resize", "window:shade", "window:unshade", "object:property-change:accessible-table-summary", "object:property-change:accessible-table-row-header", "object:property-change:accessible-table-column-header", "object:property-change:accessible-table-summary", "object:property-change:accessible-table-row-description", "object:property-change:accessible-table-column-description", "object:test" ] def callback(event): """ Event listener callback to be called when event is fired. :param event: Atspi Event. :type event: Atspi.Event """ # Event given to callback is of type Atspi.Event. # http://lazka.github.io/pgi-docs/#Atspi-2.0/classes/Event.html # The Atspi.Accessible. source = event.source if isinstance(source, Atspi.Accessible): try: name = Atspi.Accessible.get_name(source) role_name = Atspi.Accessible.get_role_name(source) source_description = f"source: [ {role_name} | {name} ]" except Exception as e: source_description = f"[DEAD] - '{e}'" else: source_description = "" # Not printing any unnamed objects, which will flood the log. if "| ]" not in source_description: print(f"Event: '{event.type}' '{source_description}'") EVENT_LISTENERS = {} for event_name in EVENT_NAMES: try: listener = EVENT_LISTENERS[event_name] except Exception: print(f"Creating new event listener: '{event_name}' and registering it.") EVENT_LISTENERS[event_name] = Atspi.EventListener.new(callback) listener = EVENT_LISTENERS[event_name] Atspi.EventListener.register(listener, event_name) try: # Global Interpreter Lock Release. def release_global_interpreter_lock(): """ Releasing the GIL or Global Interpreter Lock so we can catch an interrupt. """ try: sleep(1e-2) except KeyboardInterrupt as e: # Store the exception for later. release_global_interpreter_lock.keyboard_exception = e Atspi.event_quit() return True # Make room for an exception if one occurs during the execution. release_global_interpreter_lock.keyboard_exception = None event_id = GLib.idle_add(release_global_interpreter_lock) # Enter the main loop to start receiving and dispatching events. Atspi.event_main() GLib.source_remove(event_id) if release_global_interpreter_lock.keyboard_exception is not None: # Raise an keyboard exception we may have gotten earlier. raise KeyboardInterrupt(release_global_interpreter_lock.keyboard_exception) except KeyboardInterrupt as error: # Deregister the event. for event_name in EVENT_NAMES: try: listener = EVENT_LISTENERS[event_name] except Exception: pass print(f"Deregister event listener for event: '{event_name}'") Atspi.EventListener.deregister(listener, event_name) import sys print(f"Keyboard interrupt caught. Exiting script.\n{error}") sys.exit(0) dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/examples/data/000077500000000000000000000000001520163156700235475ustar00rootroot00000000000000dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/examples/data/UTF-8-demo.txt000066400000000000000000000321261520163156700260410ustar00rootroot00000000000000UTF-8 encoded sample plain-text file ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ Markus Kuhn [ˈmaʳkʊs kuːn] — 1999-08-20 The ASCII compatible UTF-8 encoding of ISO 10646 and Unicode plain-text files is defined in RFC 2279 and in ISO 10646-1 Annex R. Using Unicode/UTF-8, you can write in emails and source code things such as Mathematics and Sciences: ∮ E⋅da = Q, n → ∞, ∑ f(i) = ∏ g(i), ∀x∈ℝ: ⌈x⌉ = −⌊−x⌋, α ∧ ¬β = ¬(¬α ∨ β), ℕ ⊆ ℕ₀ ⊂ ℤ ⊂ ℚ ⊂ ℝ ⊂ ℂ, ⊥ < a ≠ b ≡ c ≤ d ≪ ⊤ ⇒ (A ⇔ B), 2H₂ + O₂ ⇌ 2H₂O, R = 4.7 kΩ, ⌀ 200 mm Linguistics and dictionaries: ði ıntəˈnæʃənəl fəˈnɛtık əsoʊsiˈeıʃn Y [ˈʏpsilɔn], Yen [jɛn], Yoga [ˈjoːgɑ] APL: ((V⍳V)=⍳⍴V)/V←,V ⌷←⍳→⍴∆∇⊃‾⍎⍕⌈ Nicer typography in plain text files: ╔══════════════════════════════════════════╗ ║ ║ ║ • ‘single’ and “double” quotes ║ ║ ║ ║ • Curly apostrophes: “We’ve been here” ║ ║ ║ ║ • Latin-1 apostrophe and accents: '´` ║ ║ ║ ║ • ‚deutsche‘ „Anführungszeichen“ ║ ║ ║ ║ • †, ‡, ‰, •, 3–4, —, −5/+5, ™, … ║ ║ ║ ║ • ASCII safety test: 1lI|, 0OD, 8B ║ ║ ╭─────────╮ ║ ║ • the euro symbol: │ 14.95 € │ ║ ║ ╰─────────╯ ║ ╚══════════════════════════════════════════╝ Greek (in Polytonic): The Greek anthem: Σὲ γνωρίζω ἀπὸ τὴν κόψη τοῦ σπαθιοῦ τὴν τρομερή, σὲ γνωρίζω ἀπὸ τὴν ὄψη ποὺ μὲ βία μετράει τὴ γῆ. ᾿Απ᾿ τὰ κόκκαλα βγαλμένη τῶν ῾Ελλήνων τὰ ἱερά καὶ σὰν πρῶτα ἀνδρειωμένη χαῖρε, ὦ χαῖρε, ᾿Ελευθεριά! From a speech of Demosthenes in the 4th century BC: Οὐχὶ ταὐτὰ παρίσταταί μοι γιγνώσκειν, ὦ ἄνδρες ᾿Αθηναῖοι, ὅταν τ᾿ εἰς τὰ πράγματα ἀποβλέψω καὶ ὅταν πρὸς τοὺς λόγους οὓς ἀκούω· τοὺς μὲν γὰρ λόγους περὶ τοῦ τιμωρήσασθαι Φίλιππον ὁρῶ γιγνομένους, τὰ δὲ πράγματ᾿ εἰς τοῦτο προήκοντα, ὥσθ᾿ ὅπως μὴ πεισόμεθ᾿ αὐτοὶ πρότερον κακῶς σκέψασθαι δέον. οὐδέν οὖν ἄλλο μοι δοκοῦσιν οἱ τὰ τοιαῦτα λέγοντες ἢ τὴν ὑπόθεσιν, περὶ ἧς βουλεύεσθαι, οὐχὶ τὴν οὖσαν παριστάντες ὑμῖν ἁμαρτάνειν. ἐγὼ δέ, ὅτι μέν ποτ᾿ ἐξῆν τῇ πόλει καὶ τὰ αὑτῆς ἔχειν ἀσφαλῶς καὶ Φίλιππον τιμωρήσασθαι, καὶ μάλ᾿ ἀκριβῶς οἶδα· ἐπ᾿ ἐμοῦ γάρ, οὐ πάλαι γέγονεν ταῦτ᾿ ἀμφότερα· νῦν μέντοι πέπεισμαι τοῦθ᾿ ἱκανὸν προλαβεῖν ἡμῖν εἶναι τὴν πρώτην, ὅπως τοὺς συμμάχους σώσομεν. ἐὰν γὰρ τοῦτο βεβαίως ὑπάρξῃ, τότε καὶ περὶ τοῦ τίνα τιμωρήσεταί τις καὶ ὃν τρόπον ἐξέσται σκοπεῖν· πρὶν δὲ τὴν ἀρχὴν ὀρθῶς ὑποθέσθαι, μάταιον ἡγοῦμαι περὶ τῆς τελευτῆς ὁντινοῦν ποιεῖσθαι λόγον. Δημοσθένους, Γ´ ᾿Ολυνθιακὸς Georgian: From a Unicode conference invitation: გთხოვთ ახლავე გაიაროთ რეგისტრაცია Unicode-ის მეათე საერთაშორისო კონფერენციაზე დასასწრებად, რომელიც გაიმართება 10-12 მარტს, ქ. მაინცში, გერმანიაში. კონფერენცია შეჰკრებს ერთად მსოფლიოს ექსპერტებს ისეთ დარგებში როგორიცაა ინტერნეტი და Unicode-ი, ინტერნაციონალიზაცია და ლოკალიზაცია, Unicode-ის გამოყენება ოპერაციულ სისტემებსა, და გამოყენებით პროგრამებში, შრიფტებში, ტექსტების დამუშავებასა და მრავალენოვან კომპიუტერულ სისტემებში. Russian: From a Unicode conference invitation: Зарегистрируйтесь сейчас на Десятую Международную Конференцию по Unicode, которая состоится 10-12 марта 1997 года в Майнце в Германии. Конференция соберет широкий круг экспертов по вопросам глобального Интернета и Unicode, локализации и интернационализации, воплощению и применению Unicode в различных операционных системах и программных приложениях, шрифтах, верстке и многоязычных компьютерных системах. Thai (UCS Level 2): Excerpt from a poetry on The Romance of The Three Kingdoms (a Chinese classic 'San Gua'): [----------------------------|------------------------] ๏ แผ่นดินฮั่นเสื่อมโทรมแสนสังเวช พระปกเกศกองบู๊กู้ขึ้นใหม่ สิบสองกษัตริย์ก่อนหน้าแลถัดไป สององค์ไซร้โง่เขลาเบาปัญญา ทรงนับถือขันทีเป็นที่พึ่ง บ้านเมืองจึงวิปริตเป็นนักหนา โฮจิ๋นเรียกทัพทั่วหัวเมืองมา หมายจะฆ่ามดชั่วตัวสำคัญ เหมือนขับไสไล่เสือจากเคหา รับหมาป่าเข้ามาเลยอาสัญ ฝ่ายอ้องอุ้นยุแยกให้แตกกัน ใช้สาวนั้นเป็นชนวนชื่นชวนใจ พลันลิฉุยกุยกีกลับก่อเหตุ ช่างอาเพศจริงหนาฟ้าร้องไห้ ต้องรบราฆ่าฟันจนบรรลัย ฤๅหาใครค้ำชูกู้บรรลังก์ ฯ (The above is a two-column text. If combining characters are handled correctly, the lines of the second column should be aligned with the | character above.) Ethiopian: Proverbs in the Amharic language: ሰማይ አይታረስ ንጉሥ አይከሰስ። ብላ ካለኝ እንደአባቴ በቆመጠኝ። ጌጥ ያለቤቱ ቁምጥና ነው። ደሀ በሕልሙ ቅቤ ባይጠጣ ንጣት በገደለው። የአፍ ወለምታ በቅቤ አይታሽም። አይጥ በበላ ዳዋ ተመታ። ሲተረጉሙ ይደረግሙ። ቀስ በቀስ፥ ዕንቁላል በእግሩ ይሄዳል። ድር ቢያብር አንበሳ ያስር። ሰው እንደቤቱ እንጅ እንደ ጉረቤቱ አይተዳደርም። እግዜር የከፈተውን ጉሮሮ ሳይዘጋው አይድርም። የጎረቤት ሌባ፥ ቢያዩት ይስቅ ባያዩት ያጠልቅ። ሥራ ከመፍታት ልጄን ላፋታት። ዓባይ ማደሪያ የለው፥ ግንድ ይዞ ይዞራል። የእስላም አገሩ መካ የአሞራ አገሩ ዋርካ። ተንጋሎ ቢተፉ ተመልሶ ባፉ። ወዳጅህ ማር ቢሆን ጨርስህ አትላሰው። እግርህን በፍራሽህ ልክ ዘርጋ። Runes: ᚻᛖ ᚳᚹᚫᚦ ᚦᚫᛏ ᚻᛖ ᛒᚢᛞᛖ ᚩᚾ ᚦᚫᛗ ᛚᚪᚾᛞᛖ ᚾᚩᚱᚦᚹᛖᚪᚱᛞᚢᛗ ᚹᛁᚦ ᚦᚪ ᚹᛖᛥᚫ (Old English, which transcribed into Latin reads 'He cwaeth that he bude thaem lande northweardum with tha Westsae.' and means 'He said that he lived in the northern land near the Western Sea.') Braille: ⡌⠁⠧⠑ ⠼⠁⠒ ⡍⠜⠇⠑⠹⠰⠎ ⡣⠕⠌ ⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠙⠑⠁⠙⠒ ⠞⠕ ⠃⠑⠛⠔ ⠺⠊⠹⠲ ⡹⠻⠑ ⠊⠎ ⠝⠕ ⠙⠳⠃⠞ ⠱⠁⠞⠑⠧⠻ ⠁⠃⠳⠞ ⠹⠁⠞⠲ ⡹⠑ ⠗⠑⠛⠊⠌⠻ ⠕⠋ ⠙⠊⠎ ⠃⠥⠗⠊⠁⠇ ⠺⠁⠎ ⠎⠊⠛⠝⠫ ⠃⠹ ⠹⠑ ⠊⠇⠻⠛⠹⠍⠁⠝⠂ ⠹⠑ ⠊⠇⠻⠅⠂ ⠹⠑ ⠥⠝⠙⠻⠞⠁⠅⠻⠂ ⠁⠝⠙ ⠹⠑ ⠡⠊⠑⠋ ⠍⠳⠗⠝⠻⠲ ⡎⠊⠗⠕⠕⠛⠑ ⠎⠊⠛⠝⠫ ⠊⠞⠲ ⡁⠝⠙ ⡎⠊⠗⠕⠕⠛⠑⠰⠎ ⠝⠁⠍⠑ ⠺⠁⠎ ⠛⠕⠕⠙ ⠥⠏⠕⠝ ⠰⡡⠁⠝⠛⠑⠂ ⠋⠕⠗ ⠁⠝⠹⠹⠔⠛ ⠙⠑ ⠡⠕⠎⠑ ⠞⠕ ⠏⠥⠞ ⠙⠊⠎ ⠙⠁⠝⠙ ⠞⠕⠲ ⡕⠇⠙ ⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠁⠎ ⠙⠑⠁⠙ ⠁⠎ ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲ ⡍⠔⠙⠖ ⡊ ⠙⠕⠝⠰⠞ ⠍⠑⠁⠝ ⠞⠕ ⠎⠁⠹ ⠹⠁⠞ ⡊ ⠅⠝⠪⠂ ⠕⠋ ⠍⠹ ⠪⠝ ⠅⠝⠪⠇⠫⠛⠑⠂ ⠱⠁⠞ ⠹⠻⠑ ⠊⠎ ⠏⠜⠞⠊⠊⠥⠇⠜⠇⠹ ⠙⠑⠁⠙ ⠁⠃⠳⠞ ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲ ⡊ ⠍⠊⠣⠞ ⠙⠁⠧⠑ ⠃⠑⠲ ⠔⠊⠇⠔⠫⠂ ⠍⠹⠎⠑⠇⠋⠂ ⠞⠕ ⠗⠑⠛⠜⠙ ⠁ ⠊⠕⠋⠋⠔⠤⠝⠁⠊⠇ ⠁⠎ ⠹⠑ ⠙⠑⠁⠙⠑⠌ ⠏⠊⠑⠊⠑ ⠕⠋ ⠊⠗⠕⠝⠍⠕⠝⠛⠻⠹ ⠔ ⠹⠑ ⠞⠗⠁⠙⠑⠲ ⡃⠥⠞ ⠹⠑ ⠺⠊⠎⠙⠕⠍ ⠕⠋ ⠳⠗ ⠁⠝⠊⠑⠌⠕⠗⠎ ⠊⠎ ⠔ ⠹⠑ ⠎⠊⠍⠊⠇⠑⠆ ⠁⠝⠙ ⠍⠹ ⠥⠝⠙⠁⠇⠇⠪⠫ ⠙⠁⠝⠙⠎ ⠩⠁⠇⠇ ⠝⠕⠞ ⠙⠊⠌⠥⠗⠃ ⠊⠞⠂ ⠕⠗ ⠹⠑ ⡊⠳⠝⠞⠗⠹⠰⠎ ⠙⠕⠝⠑ ⠋⠕⠗⠲ ⡹⠳ ⠺⠊⠇⠇ ⠹⠻⠑⠋⠕⠗⠑ ⠏⠻⠍⠊⠞ ⠍⠑ ⠞⠕ ⠗⠑⠏⠑⠁⠞⠂ ⠑⠍⠏⠙⠁⠞⠊⠊⠁⠇⠇⠹⠂ ⠹⠁⠞ ⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠁⠎ ⠙⠑⠁⠙ ⠁⠎ ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲ (The first couple of paragraphs of "A Christmas Carol" by Dickens) Compact font selection example text: ABCDEFGHIJKLMNOPQRSTUVWXYZ /0123456789 abcdefghijklmnopqrstuvwxyz £©µÀÆÖÞßéöÿ –—‘“”„†•…‰™œŠŸž€ ΑΒΓΔΩαβγδω АБВГДабвгд ∀∂∈ℝ∧∪≡∞ ↑↗↨↻⇣ ┐┼╔╘░►☺♀ fi�⑀₂ἠḂӥẄɐː⍎אԱა Greetings in various languages: Hello world, Καλημέρα κόσμε, コンニチハ Box drawing alignment tests: █ ▉ ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳ ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳ ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳ ╠╡ ╳ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳ ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎ ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏ ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█ dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/examples/gnome_shell_stress_test_tree_api.py000077500000000000000000000017621520163156700320270ustar00rootroot00000000000000#!/usr/bin/python3 """ Example of working with GNOME Shell, running clicks in loop. """ from dogtail.tree import root def gnome_shell_stress_test_tree_api(): """ Run click to GNOME Shell in loop. """ shell = root.application("gnome-shell") # Search the tree only once and use the object afterwards for clicks. activities_toggle_button = shell.child("Activities", "toggle button") system_menu = shell.child("System", "menu") for iteration in range(100): print(f"Iteration '{iteration}' - click on Activities toggle button.") # Depending on the initial state, first click will open and second will close. # Or wise versa. activities_toggle_button.click() activities_toggle_button.click() print(f"Iteration '{iteration}' - click on System menu.") # First click will open the menu. system_menu.click() # Second click will close the menu. system_menu.click() gnome_shell_stress_test_tree_api() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/examples/text_editor_utf8_tree_api.py000077500000000000000000000070071520163156700303670ustar00rootroot00000000000000#!/usr/bin/python3 """ Example of opening an example UTF-8 file in Text Editor. """ # ruff: noqa: E501 import os from time import sleep from dogtail.tree import root from dogtail.utils import run def gnome_text_editor_text_tree_api_demo(): """ Example how to work with GNOME Text Editor. """ os.environ["LANG"] = "en_US.UTF-8" # Remove the output file, if it's still there from a previous run. if os.path.isfile(os.path.join("/tmp", "UTF8demo.txt")): os.remove(os.path.join("/tmp", "UTF8demo.txt")) # Preventing the undesired result if the script is started multiple times in a row. print("EXAMPLE: Close any leftover application from previous run.") os.system("killall gnome-text-editor") sleep(1) # Start gnome-text-editor. print("EXAMPLE: Start GNOME Text Editor.") run("gnome-text-editor") sleep(1) # Get a root object to gnome-text-editor's application object. editor = root.application("gnome-text-editor") # Get a handle to gnome-text-editor's text object. text_buffer = editor.find_children(lambda x: x.role_name == "text")[-1] # First check that the file exists. file_location = os.path.abspath(".") + "/data/UTF-8-demo.txt" if not os.path.isfile(file_location): print(f"EXAMPLE: File was not found in expected location '{file_location}'") # Load the UTF-8 demo file. with open(file_location, "r", encoding="utf-8") as open_file: # Set the attribute .text to the given text object. # This does not always work and user has to type the text with type_text("..."). text_buffer.text = open_file.read() # Get a handle to gnome-text-editor's Main menu toggle button and click on it. print("EXAMPLE: Left click on Main menu in GNOME Text Editor.") main_menu = editor.child("Main menu", "toggle button") main_menu.click() sleep(1) # Get a handle to gnome-text-editor's Save menu item and click on it. # This needs to be a little bit more complicated to get to it, as it can be hidden. # save_button = editor.child("Save", "menu item") print("EXAMPLE: Left click on Save menu item in GNOME Text Editor.") save_button = editor.find_child( lambda x: "Save" in (x.name, x.text, x.labeler.name, x.labeler.text) and x.role_name == "menu item" and x.showing ) # This is a nice example how coordinates are wrong. # To fix this we need to click on the labeler and adjust the coordinates a little. # save_button.labeler.click() save_button.labeler.click(offset_y=20) sleep(1) # We want to save to the file name 'UTF8demo.txt'. print("EXAMPLE: Set location for file to save in GNOME Text Editor.") save_as_dialog = editor.findChild( lambda x: "Save As" in x.name and x.role_name in ("file chooser", "dialog") ) save_as_dialog.child("Name", "text").text = "/tmp/UTF8demo.txt" sleep(1) # Get a handle to gnome-text-editor's Save push button and click. print("EXAMPLE: Left click on Save push button in Save As dialog GNOME Text Editor.") save_as_button = editor.find_children( lambda x: x.name == "Save" and x.role_name in ("button", "push button") )[-1] save_as_button.click() sleep(1) # Let's quit now. print("EXAMPLE: Left click on Close push button to close GNOME Text Editor.") close_push_button = editor.find_child( lambda x: x.name == "Close" and x.role_name in ("button", "push button") ) close_push_button.click() gnome_text_editor_text_tree_api_demo() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/icons/000077500000000000000000000000001520163156700221335ustar00rootroot00000000000000dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/icons/dogtail-head-48.png000066400000000000000000000053071520163156700254210ustar00rootroot00000000000000PNG  IHDR00WbKGD pHYs  tIME !D TIDAThZklfc{mjc5O0 $`PTQhHԊJ$R@SERѦ J*h*4A X&FNl2fg;h+ =ѽ{ν9 bg> ?l?A:PL0y[#(w`aM|}I mǂ',DO $o+9p,, 7j1a8xxWkz⇑2`oRbZ_{ۅj b S47UU:߇Eu , ,//ܩro5mڴ*Mӄx8ޓ(˲+W\Q]]]]{e?###~.\h9}/UU3gaڵk\WW;)4555tvv* ,ihhhPXXdhhJrϸ!Dp\TUv( xd2{II[4/Sg@G}ʒ%K' U!nUU+r$ /^#MTU'S BȴYf|e=N/bYEE'Lĉ͗.]:`^BE$=  !eX~?af(E*kTSpB8a[??$If̘QVZZj/**2ɤ駟%yCCVZA&M4MPJY'hV(RYnm݁l}СC-j###ܹs' X+Q(D)==6^pEEEQOOo@3gD?JJJiiⷝNr (dY0FQ(p8P(tC BvZbtt R)0[(`Y/W~A^{mݻ]y<`Y}}}FHӈ㨮ƶmq F$l6.]˗cƌu---8eY#/^| @󝶘Z/flI]DBݳgS^6M4Bct D4H$+W4>igYyG٬oid2wwwO=Ԁ=`ݾ\1cx~QWW繫>eYHOW_}mmm6 5YVX!rv=z4yH0r\dY7jNAOz /6MӜ$p9e˖Xyyl0AI^߷o_Du&c>$I0m۪.;n=@wwwRe-h]]]ʁ͛7G:x`9VE8[xl5Q%i9˲8 )dM nWIENDB`dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/icons/dogtail-head.svg000066400000000000000000001541541520163156700252100ustar00rootroot00000000000000 image/svg+xml Máirín Duffy Red Hat, Inc. dogtail head dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/icons/dogtail-tail-48.png000066400000000000000000000046361520163156700254550ustar00rootroot00000000000000PNG  IHDR00WbKGD pHYs  tIME  mƘ +IDAThՙmll/`C!& 4 S[RPHJDJQ|i+CCHi`JhZw{{7Y[!؏4՞4>f zE_ H`Of4SFL 6϶*m8͍p!ϊ{̸Otr\t|lƤ cLi23AdaPRNuf91`3hNN?Yp~r)ƛ(/Pd ޅRF0EX2IDpzf?Tq%(;P(#IXSbPB>5ۅLDDwz(W(/y#A"v+j>r}Rf˃i?0a' p+ 9zPU5iߑurۊeY&''uد?(Pb\pB:nw.Hd|w$=_]]]#hZ", =r$=kZe1|8=ϑ׸\.(0|~> LG")mLЭ.8^Q^-lt:mC4LbZOO|%]ex~mhG*r2ƆL Onll|>ݍkךv]|@ UQ*++f.į^zŋΝ;wvvv,YEQdhs XKKb2]v-?66wwwBimm_t:'&&ZlҥK-p8H$RJ_ovuuE@ƍsȲ1|[U>sP(5M{xݺut:MO:5J0Ǝӳg.X,={p>~6WWWK?G'''W)r1v4~… oÇ=k׮}Dbw:n+) EE[ <[ [yp%kjj,~?dw8M۷oX" ~A}||\K&† 8jNzs^ӹ>ZZ/ٳS=|>mjjNi/))yhŊr]]Xii)3,tO~Ɗ=/a|UUU^c~y^vСիW!n)))yj:!- |'NL&t:惇'Oxp)?)EwW\B\rUӴl}ׯpӦM0Ƹ`0ai)RUR;:!~^ZZZ 't?b-[ V dӓOpɲ쫯u=?00ࣔn,@-!8_J_Geee R*Q4"_)INgiz<7>H{Oy8!|$`/Yw8s>ףh.c8Fi-$Il63Fbqr#z M\.8cs!ñy1FE!Bqvid.,{$I$˲L&O)pw5fk%˺@!TE"OuNBOIENDB`dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/icons/dogtail-tail.svg000066400000000000000000000723201520163156700252320ustar00rootroot00000000000000 image/svg+xml Máirín Duffy Red Hat, Inc. dogtail tail dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/pyproject.toml000066400000000000000000000025161520163156700237400ustar00rootroot00000000000000[build-system] requires = ["setuptools>=80", "setuptools-scm[simple]>=8"] build-backend = "setuptools.build_meta" [project] name = "dogtail" dynamic = ["version"] description = "GUI test tool and automation framework that uses Accessibility (AT-SPI) technologies to communicate with desktop applications." license = "GPL-2.0" readme = "README.md" requires-python = ">=3.9" authors = [ {name = "Zack Cerza", email = "zcerza@redhat.com"}, {name = "Ed Rousseau", email = "rousseau@redhat.com"}, {name = "David Malcolm", email = "dmalcolm@redhat.com"}, {name = "Vitezslav Humpa", email = "vhumpa@redhat.com"}, {name = "Michal Odehnal", email = "modehnal@redhat.com"} ] urls = { "Homepage" = "https://gitlab.com/dogtail/dogtail" } [tool.setuptools.packages.find] include = ["dogtail", "dogtail.accessibles", "dogtail.scripts", "tests"] [project.scripts] dogtail-create-config = "dogtail.scripts.dogtail_create_config:main" dogtail-get-config = "dogtail.scripts.dogtail_get_config:main" dogtail-headless = "dogtail.scripts.dogtail_headless:main" [tool.setuptools_scm] # can be empty if no extra settings are needed, presence enables setuptools-scm #tag_regex = "^DOGTAIL_(?P[0-9]+_[0-9]+_[0-9]+)$" fallback_version = "v2.0.dev0" version_scheme = "guess-next-dev" local_scheme = "no-local-version" write_to = "dogtail/_version.py" dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests/000077500000000000000000000000001520163156700221625ustar00rootroot00000000000000dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests/__init__.py000066400000000000000000000000001520163156700242610ustar00rootroot00000000000000dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests/main.fmf000066400000000000000000000232031520163156700236000ustar00rootroot00000000000000/plan: summary: Run all dogtail tests. discover: how: fmf # Used when running by hand to specify branches. # url: https://gitlab.com/dogtail/dogtail.git # ref: # provision: # how: virtual # hw: # memory: ">= 4 GB" # Specify usage of rhel-10 and api token # api-key: ${TESTING_FARM_API_TOKEN} # Use a secure CI/CD variable # # Local VM testing. # provision: # how: connect # user: root # guest: 192.168.122.92 prepare: - name: packages how: install package: - gtk3-devel - gtk4-devel-tools - gnome-ponytail-daemon - python3-gnome-ponytail-daemon - python3-pip - python3-psutil - python3-devel # Debugging logind issues. # - name: logind_debug_setup # how: shell # script: | # mkdir /etc/systemd/system/systemd-logind.service.d/ # touch /etc/systemd/system/systemd-logind.service.d/10-debug.conf # cat > /etc/systemd/system/systemd-logind.service.d/10-debug.conf << EOL # [Service] # Environment=SYSTEMD_LOG_LEVEL=debug # EOL # sleep 1 # cat /etc/systemd/system/systemd-logind.service.d/10-debug.conf - name: group_install how: shell script: # Red Hat # dnf group install -y "Server with GUI" --allowerasing # Fedora/Centos - important. dnf install -y @gnome-desktop --allowerasing - name: pip3 how: shell script: - python3 -m pip install build - name: dogtail how: shell script: | echo "Starting the Python build process..." python3 -m build python3 -m pip install dist/dogtail-2.0.dev0-py3-none-any.whl --force-reinstall - name: user_setup how: shell script: | sudo useradd test -G wheel echo "redhat" | passwd test --stdin echo "Defaults:test !env_reset" >> /etc/sudoers echo "test ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers execute: how: tmt # report: # Use the GitLab reporting plugin # how: display # The API URL of your GitLab instance. # TMT often automatically detects this from CI variables. # Testing if ci will get it on its own first. # api-url: https://gitlab.com # api-url: https://gitlab.com # The name for the status check shown in the GitLab UI. # Defaults to 'tmt'. # context: "testing-farm-results" # context: "testing-farm-results" # The access token for the GitLab API. # IMPORTANT: This is read from a secure environment variable. # api-token: ${GITLAB_API_TOKEN} # api-token: ${GITLAB_API_TOKEN} /tests: /test_config: test: sudo -u test /usr/local/bin/dogtail-headless "python3 -m unittest tests.test_config -v" environment: TEST: dogtail_test_config order: 10 duration: 10m tty: true /test_predicate: test: sudo -u test /usr/local/bin/dogtail-headless "python3 -m unittest tests.test_predicate -v" environment: TEST: dogtail_test_predicate order: 20 duration: 10m tty: true /test_rawinput: test: sudo -u test /usr/local/bin/dogtail-headless "python3 -m unittest tests.test_rawinput -v" environment: TEST: dogtail_test_rawinput order: 30 duration: 10m tty: true /test_tree_gtk3: test: sudo -u test /usr/local/bin/dogtail-headless "python3 -m unittest tests.test_tree_gtk3 -v" environment: TEST: dogtail_test_tree_gtk3 order: 40 duration: 10m tty: true /test_tree_gtk4: test: sudo -u test /usr/local/bin/dogtail-headless "python3 -m unittest tests.test_tree_gtk4 -v" environment: TEST: dogtail_test_tree_gtk4 order: 50 duration: 10m tty: true /test_tree_search_gtk3: test: sudo -u test /usr/local/bin/dogtail-headless "python3 -m unittest tests.test_tree_search_gtk3 -v" environment: TEST: dogtail_test_tree_search_gtk3 order: 60 duration: 10m tty: true /test_tree_search_gtk4: test: sudo -u test /usr/local/bin/dogtail-headless "python3 -m unittest tests.test_tree_search_gtk4 -v" environment: TEST: dogtail_test_tree_search_gtk4 order: 70 duration: 10m tty: true /test_tree_selection_gtk3: test: sudo -u test /usr/local/bin/dogtail-headless "python3 -m unittest tests.test_tree_selection_gtk3 -v" environment: TEST: dogtail_test_tree_selection_gtk3 order: 80 duration: 10m tty: true /test_tree_selection_gtk4: test: sudo -u test /usr/local/bin/dogtail-headless "python3 -m unittest tests.test_tree_selection_gtk4 -v" environment: TEST: dogtail_test_tree_selection_gtk4 order: 90 duration: 10m tty: true /test_tree_states_gtk3: test: sudo -u test /usr/local/bin/dogtail-headless "python3 -m unittest tests.test_tree_states_gtk3 -v" environment: TEST: dogtail_test_tree_states_gtk3 order: 100 duration: 10m tty: true /test_tree_states_gtk4: test: sudo -u test /usr/local/bin/dogtail-headless "python3 -m unittest tests.test_tree_states_gtk4 -v" environment: TEST: dogtail_test_tree_states_gtk4 order: 110 duration: 10m tty: true /test_tree_string_representation_gtk3: test: sudo -u test /usr/local/bin/dogtail-headless "python3 -m unittest tests.test_tree_string_representation_gtk3 -v" environment: TEST: dogtail_test_tree_string_representation_gtk3 order: 120 duration: 10m tty: true /test_tree_string_representation_gtk4: test: sudo -u test /usr/local/bin/dogtail-headless "python3 -m unittest tests.test_tree_string_representation_gtk4 -v" environment: TEST: dogtail_test_tree_string_representation_gtk4 order: 130 duration: 10m tty: true /test_tree_value_gtk3: test: sudo -u test /usr/local/bin/dogtail-headless "python3 -m unittest tests.test_tree_value_gtk3 -v" environment: TEST: dogtail_test_tree_value_gtk3 order: 140 duration: 10m tty: true /test_tree_value_gtk4: test: sudo -u test /usr/local/bin/dogtail-headless "python3 -m unittest tests.test_tree_value_gtk4 -v" environment: TEST: dogtail_test_tree_value_gtk4 order: 150 duration: 10m tty: true /test_utils_accessibility: test: sudo -u test /usr/local/bin/dogtail-headless "python3 -m unittest tests.test_utils_accessibility -v" environment: TEST: dogtail_test_utils_accessibility order: 160 duration: 10m tty: true /test_utils_delay: test: sudo -u test /usr/local/bin/dogtail-headless "python3 -m unittest tests.test_utils_delay -v" environment: TEST: dogtail_test_utils_delay order: 170 duration: 10m tty: true /test_utils_lock: test: sudo -u test /usr/local/bin/dogtail-headless "python3 -m unittest tests.test_utils_lock -v" environment: TEST: dogtail_test_utils_lock order: 180 duration: 10m tty: true /test_utils_run_gtk3: test: sudo -u test /usr/local/bin/dogtail-headless "python3 -m unittest tests.test_utils_run_gtk3 -v" environment: TEST: dogtail_test_utils_run_gtk3 order: 190 duration: 10m tty: true /test_utils_run_gtk4: test: sudo -u test /usr/local/bin/dogtail-headless "python3 -m unittest tests.test_utils_run_gtk4 -v" environment: TEST: dogtail_test_utils_run_gtk4 order: 200 duration: 10m tty: true /test_version: test: sudo -u test /usr/local/bin/dogtail-headless "python3 -m unittest tests.test_version -v" environment: TEST: dogtail_test_version order: 210 duration: 10m tty: true /test_rawinput_drag: test: sudo -u test /usr/local/bin/dogtail-headless "python3 -m unittest tests.test_rawinput_drag -v" environment: TEST: test_rawinput_drag order: 220 duration: 10m tty: true /test_rawinput_keyboard_gtk3: test: sudo -u test /usr/local/bin/dogtail-headless "python3 -m unittest tests.test_rawinput_keyboard_gtk3 -v" environment: TEST: dogtail_test_rawinput_keyboard_gtk3 order: 230 duration: 10m tty: true /test_rawinput_keyboard_gtk4: test: sudo -u test /usr/local/bin/dogtail-headless "python3 -m unittest tests.test_rawinput_keyboard_gtk4 -v" environment: TEST: dogtail_test_rawinput_keyboard_gtk4 order: 240 duration: 10m tty: true /test_rawinput_mouse_buttons_gtk3: test: sudo -u test /usr/local/bin/dogtail-headless "python3 -m unittest tests.test_rawinput_mouse_buttons_gtk3 -v" environment: TEST: dogtail_test_rawinput_mouse_buttons_gtk3 order: 250 duration: 10m tty: true /test_rawinput_mouse_buttons_gtk4: test: sudo -u test /usr/local/bin/dogtail-headless "python3 -m unittest tests.test_rawinput_mouse_buttons_gtk4 -v" environment: TEST: dogtail_test_rawinput_mouse_buttons_gtk4 order: 260 duration: 10m tty: true dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests/test_config.py000066400000000000000000000047541520163156700250520ustar00rootroot00000000000000#!/usr/bin/python3 """ Unit tests for the dogtail.config. """ import unittest from dogtail.config import config class TestConfig(unittest.TestCase): """ Class to test dogtail's config. """ def test_set_value(self): """ Testing set value. """ self.assertEqual(config.action_delay, 1.0) config.action_delay = 2.0 self.assertEqual(config.action_delay, 2.0) def test_set_incorrect_value(self): """ Testing set incorrect value. """ with self.assertLogs("__dogtail_logger__", level="INFO") as captured_message: config.action_delay = "set_string_to_int_or_float_accepted_value" expected_messages = [ "Attempt to set value of type '' to key with accepted types '[, ]'", "Fallback of config option 'action_delay' to '1'.", ] for record in captured_message.records: self.assertEqual(record.levelname, "INFO") self.assertIn(record.msg, expected_messages) def test_set_not_existing_value(self): """ Testing set not existing value. """ with self.assertRaises(AttributeError): config.not_existing = False def test_get_value(self): """ Testing get value. """ self.assertEqual(config.action_delay, 1.0) def test_get_incorrect_value(self): """ Testing get not existing value. """ self.assertRaises(AttributeError, getattr, config, "not_existing") def test_reset_configuration(self): """ Testing resetting the configuration. """ # When running unit tests we manipulate the config, reset it first. config.reset_configuration() original_action_delay = config.action_delay self.assertEqual(config.action_delay, original_action_delay) original_default_delay = config.default_delay self.assertEqual(config.default_delay, original_default_delay) config.action_delay = 2.0 config.default_delay = 2.0 self.assertNotEqual(config.action_delay, original_action_delay) self.assertNotEqual(config.default_delay, original_default_delay) config.reset_configuration() self.assertEqual(config.action_delay, original_action_delay) self.assertEqual(config.default_delay, original_default_delay) if __name__ == "__main__": unittest.main() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests/test_gtk_demo.py000066400000000000000000000075121520163156700253710ustar00rootroot00000000000000#!/usr/bin/python3 """ Useful classes for in session manipulation with applications. """ import os import sys from io import StringIO from time import sleep import unittest import dogtail.utils class Gtk3DemoTest(unittest.TestCase): """ Instances of the TestCase class represent the logical test units in the unittest universe. This class is intended to be used as a base class, with specific tests being implemented by concrete subclasses. This class implements the interface needed by the test runner to allow it to drive the tests, and methods that the test code can use to check for and report various kinds of failure. """ # To make this functional we have to override the setUp method in camelCase. def setUp(self): """ Method called to prepare the test fixture. This is called immediately before calling the test method """ self.process = dogtail.utils.run("gtk3-demo") # Let time application start properly. sleep(1) self.demo = dogtail.tree.root.application("gtk3-demo") # To make this functional we have to override the tearDown method in camelCase. def tearDown(self): """ Method called immediately after the test method has been called and the result recorded """ self.process.kill() self.process.wait() # This one can be started in test, kill it after gtk3-demo too. os.system("killall gtk3-demo-application > /dev/null 2>&1") # Sleep just enough to let the app actually die. # AT-SPI doesn't like being hammered too fast. sleep(1) def run_demo(self, demo_name, retry=True): """ Activate the named demo within the gtk3-demo app. """ tree = self.demo.child(roleName="tree table") tree.child(demo_name, retry=retry).do_action_named("activate") class Gtk4DemoTest(unittest.TestCase): """ Instances of the TestCase class represent the logical test units in the unittest universe. This class is intended to be used as a base class, with specific tests being implemented by concrete subclasses. This class implements the interface needed by the test runner to allow it to drive the tests, and methods that the test code can use to check for and report various kinds of failure. """ def setUp(self): """ Method called to prepare the test fixture. This is called immediately before calling the test method """ self.process = dogtail.utils.run("gtk4-demo") # Let time application start properly. sleep(1) self.demo = dogtail.tree.root.application("gtk4-demo") def tearDown(self): """ Method called immediately after the test method has been called and the result recorded """ self.process.kill() self.process.wait() # This one can be started in test, kill it after gtk3-demo too. os.system("killall gtk4-demo-application > /dev/null 2>&1") # Sleep just enough to let the app actually die. # AT-SPI doesn't like being hammered too fast. sleep(1) def run_demo(self, demo_name, retry=True): """ Activate the named demo within the gtk3-demo app. """ tree = self.demo.child(roleName="tree table") tree.child(demo_name, retry=retry).do_action_named("activate") def trap_stdout(function, args=None): """ Grab stdout output during function execution. """ saved_stdout = sys.stdout try: out = StringIO() sys.stdout = out if isinstance(args, dict): function(**args) elif args: function(args) else: function() output = out.getvalue().strip() finally: sys.stdout = saved_stdout return output dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests/test_predicate.py000066400000000000000000000321211520163156700255320ustar00rootroot00000000000000#!/usr/bin/python3 """ Unit tests for the dogtail.predicate. """ import unittest import dogtail.tree import dogtail.predicate class DummyNode: """ Dummy class to represent basic dogtail Node. """ def __init__(self, name=None, role_name=None, description=None): self.name = name self.role_name = role_name self.description = description self.labeler = None self.text = "dummy text" class TestPredicate(unittest.TestCase): """ Class to test Predicate. """ def test_correct_equality(self): """ Test to make sure the predicate equal comparison is working. """ predicate_1 = dogtail.predicate.GenericPredicate() predicate_2 = dogtail.predicate.GenericPredicate() self.assertEqual(predicate_1, predicate_2) def test_incorrect_equality(self): """ Test to make sure the predicate unequal comparison is working. """ predicate_1 = dogtail.predicate.GenericPredicate() self.assertNotEqual(predicate_1, self) def test_predicate_application(self): """ Test Predicate IsAnApplicationNamed. """ dummy_application = DummyNode(name="dummy", role_name="application") predicate = dogtail.predicate.IsAnApplicationNamed(dummy_application.name) self.assertTrue(predicate.satisfied_by_node(dummy_application)) def test_predicate_application_negation(self): """ Test Predicate IsAnApplicationNamed False Result. """ dummy_application = DummyNode(name="dummy", role_name="not_application") predicate = dogtail.predicate.IsAnApplicationNamed(dummy_application.name) self.assertFalse(predicate.satisfied_by_node(dummy_application)) def test_predicate_tab(self): """ Test Predicate IsATabNamed. """ dummy_application = DummyNode(name="dummy", role_name="tab") predicate = dogtail.predicate.IsATabNamed(dummy_application.name) self.assertTrue(predicate.satisfied_by_node(dummy_application)) def test_predicate_tab_negation(self): """ Test Predicate IsATabNamed False Result. """ dummy_application = DummyNode(name="dummy", role_name="not_tab") predicate = dogtail.predicate.IsATabNamed(dummy_application.name) self.assertFalse(predicate.satisfied_by_node(dummy_application)) def test_predicate_button(self): """ Test Predicate IsAButtonNamed. """ dummy_application = DummyNode(name="dummy", role_name="button") predicate = dogtail.predicate.IsAButtonNamed(dummy_application.name) self.assertTrue(predicate.satisfied_by_node(dummy_application)) def test_predicate_push_button(self): """ Test Predicate IsAPushButtonNamed. """ dummy_application = DummyNode(name="dummy", role_name="push button") predicate = dogtail.predicate.IsAPushButtonNamed(dummy_application.name) self.assertTrue(predicate.satisfied_by_node(dummy_application)) def test_predicate_button_push_button(self): """ Test Predicate IsAButtonNamed and IsAPushButtonNamed. """ dummy_button = DummyNode(name="dummy", role_name="button") dummy_push_button = DummyNode(name="dummy", role_name="push button") predicate_button = dogtail.predicate.IsAButtonNamed(dummy_button.name) predicate_push_button = dogtail.predicate.IsAPushButtonNamed(dummy_push_button.name) self.assertTrue(predicate_button.satisfied_by_node(dummy_button)) self.assertFalse(predicate_button.satisfied_by_node(dummy_push_button)) self.assertTrue(predicate_push_button.satisfied_by_node(dummy_push_button)) self.assertFalse(predicate_push_button.satisfied_by_node(dummy_button)) def test_predicate_button_negation(self): """ Test Predicate IsAButtonNamed False Result. """ dummy_application = DummyNode(name="dummy", role_name="not_push_button") predicate = dogtail.predicate.IsAButtonNamed(dummy_application.name) self.assertFalse(predicate.satisfied_by_node(dummy_application)) def test_predicate_text(self): """ Test Predicate IsATextEntryNamed. """ dummy_application = DummyNode(name="dummy", role_name="text") predicate = dogtail.predicate.IsATextEntryNamed(dummy_application.name) self.assertTrue(predicate.satisfied_by_node(dummy_application)) def test_predicate_text_negation(self): """ Test Predicate IsATextEntryNamed False Result. """ dummy_application = DummyNode(name="dummy", role_name="not_text") predicate = dogtail.predicate.IsATextEntryNamed(dummy_application.name) self.assertFalse(predicate.satisfied_by_node(dummy_application)) def test_predicate_menu_item(self): """ Test Predicate IsAMenuItemNamed. """ dummy_application = DummyNode(name="dummy", role_name="menu item") predicate = dogtail.predicate.IsAMenuItemNamed(dummy_application.name) self.assertTrue(predicate.satisfied_by_node(dummy_application)) def test_predicate_menu_item_negation(self): """ Test Predicate IsAMenuItemNamed False Result. """ dummy_application = DummyNode(name="dummy", role_name="not_menu_item") predicate = dogtail.predicate.IsAMenuItemNamed(dummy_application.name) self.assertFalse(predicate.satisfied_by_node(dummy_application)) def test_predicate_menu(self): """ Test Predicate IsAMenuNamed. """ dummy_application = DummyNode(name="dummy", role_name="menu") predicate = dogtail.predicate.IsAMenuNamed(dummy_application.name) self.assertTrue(predicate.satisfied_by_node(dummy_application)) def test_predicate_menu_negation(self): """ Test Predicate IsAMenuNamed False Result. """ dummy_application = DummyNode(name="dummy", role_name="not_menu") predicate = dogtail.predicate.IsAMenuNamed(dummy_application.name) self.assertFalse(predicate.satisfied_by_node(dummy_application)) def test_predicate_label_by_name(self): """ Test Predicate IsLabelledAs. """ dummy_application_1 = DummyNode(name="dummy_1", role_name="menu") dummy_application_2 = DummyNode(name="dummy_2", role_name="menu") dummy_application_2.labeler = dummy_application_1 predicate = dogtail.predicate.IsLabelledAs(dummy_application_1.name) self.assertTrue(predicate.satisfied_by_node(dummy_application_2)) def test_predicate_label_by_text(self): """ Test Predicate IsLabelledAs. """ dummy_application_1 = DummyNode(name="dummy_1", role_name="menu") dummy_application_1.text = "dummy_3" dummy_application_2 = DummyNode(name="dummy_2", role_name="menu") dummy_application_3 = DummyNode(name="dummy_3", role_name="menu") dummy_application_2.labeler = dummy_application_1 predicate = dogtail.predicate.IsLabelledAs(dummy_application_3.name) self.assertTrue(predicate.satisfied_by_node(dummy_application_2)) def test_predicate_label_negation(self): """ Test Predicate IsLabelledAs False Result. """ dummy_application_1 = DummyNode(name="dummy_1", role_name="menu") dummy_application_2 = DummyNode(name="dummy_2", role_name="menu") dummy_application_2.labeler = dummy_application_1 predicate = dogtail.predicate.IsLabelledAs(dummy_application_1.name) self.assertFalse(predicate.satisfied_by_node(dummy_application_1)) def test_predicate_dialog(self): """ Test Predicate IsADialogNamed. """ dummy_application = DummyNode(name="dummy", role_name="dialog") predicate = dogtail.predicate.IsADialogNamed(dummy_application.name) self.assertTrue(predicate.satisfied_by_node(dummy_application)) def test_predicate_dialog_negation(self): """ Test Predicate IsADialogNamed False Result. """ dummy_application = DummyNode(name="dummy", role_name="not_dialog") predicate = dogtail.predicate.IsADialogNamed(dummy_application.name) self.assertFalse(predicate.satisfied_by_node(dummy_application)) def test_predicate_window(self): """ Test Predicate IsAWindow. """ dummy_application = DummyNode(name="dummy", role_name="window") predicate = dogtail.predicate.IsAWindow() self.assertTrue(predicate.satisfied_by_node(dummy_application)) def test_predicate_window_negation(self): """ Test Predicate IsAWindow False Result. """ dummy_application = DummyNode(name="dummy", role_name="not_window") predicate = dogtail.predicate.IsAWindow() self.assertFalse(predicate.satisfied_by_node(dummy_application)) def test_predicate_window_named(self): """ Test Predicate IsAWindowNamed. """ dummy_application = DummyNode(name="dummy", role_name="window") predicate = dogtail.predicate.IsAWindowNamed(dummy_application.name) self.assertTrue(predicate.satisfied_by_node(dummy_application)) def test_predicate_window_named_negation(self): """ Test Predicate IsAWindowNamed False Result. """ dummy_application = DummyNode(name="dummy", role_name="not_window") predicate = dogtail.predicate.IsAWindowNamed(dummy_application.name) self.assertFalse(predicate.satisfied_by_node(dummy_application)) def test_predicate_frame_named(self): """ Test Predicate IsAFrameNamed. """ dummy_application = DummyNode(name="dummy", role_name="frame") predicate = dogtail.predicate.IsAFrameNamed(dummy_application.name) self.assertTrue(predicate.satisfied_by_node(dummy_application)) def test_predicate_frame_named_negation(self): """ Test Predicate IsAFrameNamed False Result. """ dummy_application = DummyNode(name="dummy", role_name="not_frame") predicate = dogtail.predicate.IsAFrameNamed(dummy_application.name) self.assertFalse(predicate.satisfied_by_node(dummy_application)) def test_predicate_frame_named_name_negation(self): """ Test Predicate IsAFrameNamed Name False Result. """ dummy_application = DummyNode(name="dummy", role_name="not_frame") predicate = dogtail.predicate.IsAFrameNamed("not_dummy") self.assertFalse(predicate.satisfied_by_node(dummy_application)) def test_predicate_frame_named_name_negation_frame_correction(self): """ Test Predicate IsAFrameNamed Name False Result Frame Correct. """ dummy_application = DummyNode(name="dummy", role_name="frame") predicate = dogtail.predicate.IsAFrameNamed("not_dummy") self.assertFalse(predicate.satisfied_by_node(dummy_application)) def test_predicate_named(self): """ Test Predicate IsNamed. """ dummy_application = DummyNode(name="dummy") predicate = dogtail.predicate.IsNamed(dummy_application.name) self.assertTrue(predicate.satisfied_by_node(dummy_application)) def test_predicate_named_negation(self): """ Test Predicate IsNamed False Result. """ dummy_application = DummyNode(name="dummy") predicate = dogtail.predicate.IsNamed("not_dummy") self.assertFalse(predicate.satisfied_by_node(dummy_application)) def test_generic_predicate_by_name(self): """ Test Predicate GenericPredicate by Name. """ dummy_application = DummyNode(name="dummy") predicate = dogtail.predicate.GenericPredicate(name="dummy") self.assertTrue(predicate.satisfied_by_node(dummy_application)) def test_generic_predicate_by_role_name(self): """ Test Predicate GenericPredicate by Role Name. """ dummy_application = DummyNode(role_name="dummy role name") predicate = dogtail.predicate.GenericPredicate(role_name="dummy role name") self.assertTrue(predicate.satisfied_by_node(dummy_application)) def test_generic_predicate_by_description(self): """ Test Predicate GenericPredicate by Description. """ dummy_application = DummyNode(description="dummy description") predicate = dogtail.predicate.GenericPredicate(description="dummy description") self.assertTrue(predicate.satisfied_by_node(dummy_application)) def test_generic_predicate_by_label(self): """ Test Predicate GenericPredicate by label. """ dummy_application_1 = DummyNode(name="dummy_1", role_name="menu") dummy_application_2 = DummyNode(name="dummy_2", role_name="menu") dummy_application_2.labeler = dummy_application_1 predicate = dogtail.predicate.GenericPredicate(label=dummy_application_1.name) self.assertTrue(predicate.satisfied_by_node(dummy_application_2)) if __name__ == "__main__": unittest.main() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests/test_rawinput.py000066400000000000000000000075121520163156700254510ustar00rootroot00000000000000#!/usr/bin/python3 """ Unit tests for the dogtail.rawinput. """ # pylint: disable=import-outside-toplevel import unittest from dogtail.rawinput import ( absolute_motion, absolute_motion_with_trajectory, check_coordinates, click, double_click, press, release, ) class TestRawinput(unittest.TestCase): """ Class to test dogtail's rawinput. """ def test_absolute_motion(self): """ Testing absolute_motion. """ absolute_motion(50, 50) absolute_motion(100, 100) absolute_motion(150, 150) absolute_motion(200, 200) absolute_motion(200, 200, mouse_delay=1) absolute_motion(200, 200, mouse_delay=1, check=True) absolute_motion(-200, -200, mouse_delay=1, check=False) def test_motion_with_trajectory(self): """ Testing absolute_motion_with_trajectory. """ absolute_motion_with_trajectory(100, 100, 110, 110) absolute_motion_with_trajectory(110, 110, 120, 120) absolute_motion_with_trajectory(120, 120, 130, 130) absolute_motion_with_trajectory(130, 130, 150, 150, mouse_delay=0.1) def test_check_coordinates_directly(self): """ Testing check_coordinates directly. """ check_coordinates(0, 0) check_coordinates(-0, -0) check_coordinates(10, 0) check_coordinates(0, 10) # There are shadows which can be in negative, but that does not mean a bug. # We set the limits to (-50,-50), most issues are way over this value. with self.assertRaises(ValueError): check_coordinates(-100, 100) with self.assertRaises(ValueError): check_coordinates(100, -100) with self.assertRaises(ValueError): check_coordinates(-100, -100) def test_check_coordinates_in_methods(self): """ Testing check_coordinates in methods. """ # There are shadows which can be in negative, but that does not mean a bug. # We set the limits to (-50,-50), most issues are way over this value. # Absolute Motion. with self.assertRaises(ValueError): absolute_motion(-100, 100) with self.assertRaises(ValueError): absolute_motion(100, -100) with self.assertRaises(ValueError): absolute_motion(-100, -100) # Absolute Motion With Trajectory. with self.assertRaises(ValueError): absolute_motion_with_trajectory(-100, 100, 100, 100) with self.assertRaises(ValueError): absolute_motion_with_trajectory(100, -100, 100, 100) with self.assertRaises(ValueError): absolute_motion_with_trajectory(100, 100, -100, 100) with self.assertRaises(ValueError): absolute_motion_with_trajectory(100, 100, 100, -100) # Click. with self.assertRaises(ValueError): click(-100, 100) with self.assertRaises(ValueError): click(100, -100) with self.assertRaises(ValueError): click(-100, -100) # Double Click. with self.assertRaises(ValueError): double_click(-100, 100) with self.assertRaises(ValueError): double_click(100, -100) with self.assertRaises(ValueError): double_click(-100, -100) # Press. with self.assertRaises(ValueError): press(-100, 100) with self.assertRaises(ValueError): press(100, -100) with self.assertRaises(ValueError): press(-100, -100) # Release. with self.assertRaises(ValueError): release(-100, 100) with self.assertRaises(ValueError): release(100, -100) with self.assertRaises(ValueError): release(-100, -100) if __name__ == "__main__": unittest.main() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests/test_rawinput_drag.py000066400000000000000000000067141520163156700264510ustar00rootroot00000000000000# pylint: disable=import-outside-toplevel import os import unittest from time import sleep import subprocess from dogtail.tree import root from dogtail.rawinput import ( drag, drag_with_trajectory, press_key, ) from dogtail.logging import logging_class LOGGING = logging_class.logger class TestGnomeShellOverviewRawinput(unittest.TestCase): """ Class to test dogtail's rawinput drag methods. """ # Skipping for now, breaking testing-farm somehow, probably too many events generated? #@unittest.SkipTest def test_drag_in_overview(self): """ Testing drag. """ press_key("Esc") sleep(1) os.system("killall nautilus > /dev/null 2>&1") gnome_shell = root.application("gnome-shell") print("This test assumes Nautilus - Files to be Pinned") os.system("gsettings set org.gnome.shell favorite-apps \"['org.gnome.Nautilus.desktop']\"") sleep(1) print("Checking value for favorite apps.") print(subprocess.check_output("gsettings get org.gnome.shell favorite-apps", shell=True).decode()) sleep(1) press_key("Esc") sleep(1) press_key("Super") sleep(1) # Search for Files - assuming Files are Pinned. file_button = gnome_shell.find_child( lambda x: x.name == "Files" and x.role_name in ("button", "push button") ) start_run_button_x = file_button.center[0] start_run_button_y = file_button.center[1] # SHould be enough, lets not assume resolution. end_run_button_x = 300 end_run_button_y = 300 drag( (start_run_button_x, start_run_button_y), (end_run_button_x, end_run_button_y), ) sleep(2) self.assertTrue("org.gnome.Nautilus" in [x.name for x in root.applications()]) os.system("killall nautilus > /dev/null 2>&1") press_key("Esc") def test_drag_with_trajectory_in_overview(self): """ Testing drag with trajectory. """ press_key("Esc") sleep(1) os.system("killall nautilus > /dev/null 2>&1") gnome_shell = root.application("gnome-shell") print("This test assumes Nautilus - Files to be Pinned") os.system("gsettings set org.gnome.shell favorite-apps \"['org.gnome.Nautilus.desktop']\"") sleep(1) print("Checking value for favorite apps.") print(subprocess.check_output("gsettings get org.gnome.shell favorite-apps", shell=True).decode()) sleep(1) press_key("Esc") sleep(1) press_key("Super") sleep(1) # Search for Files - assuming Files are Pinned. file_button = gnome_shell.find_child( lambda x: x.name == "Files" and x.role_name in ("button", "push button") ) start_run_button_x = file_button.center[0] start_run_button_y = file_button.center[1] # SHould be enough, lets not assume resolution. end_run_button_x = 300 end_run_button_y = 300 drag_with_trajectory( (start_run_button_x, start_run_button_y), (end_run_button_x, end_run_button_y), press_delay=0.01, mouse_delay=0.01, ) sleep(2) self.assertTrue("org.gnome.Nautilus" in [x.name for x in root.applications()]) os.system("killall nautilus > /dev/null 2>&1") press_key("Esc") if __name__ == "__main__": unittest.main()dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests/test_rawinput_keyboard_gtk3.py000066400000000000000000000056731520163156700302670ustar00rootroot00000000000000#!/usr/bin/python3 """ Unit tests for the dogtail.rawinput keyboard methods. """ # pylint: disable=import-outside-toplevel import os from time import sleep import unittest try: from tests.test_gtk_demo import Gtk3DemoTest except ImportError: from test_gtk_demo import Gtk3DemoTest from dogtail.rawinput import ( press_key, key_combo, type_text, ) @unittest.skipIf(not os.path.isfile("/usr/bin/gtk3-demo"), "Skipping, no gtk3-demo.") class TestGtk3DemoRawinputKeyboard(Gtk3DemoTest): """ Class to test dogtail's rawinput keyboard methods. """ def test_press_key(self): """ Testing press key. """ press_key("Esc") sleep(1) press_key("Super") sleep(1) press_key("Esc") sleep(1) def test_press_key_no_such_key(self): """ Testing press key with no such key. """ with self.assertRaises(KeyError): press_key("No such key.") def test_key_combo_simple(self): """ Testing simple key combo. """ key_combo("") self.assertTrue(self.demo.child("Tree View").showing) def test_key_combo_multi_key(self): """ Testing simple key combo with multiple keys. """ press_key("Esc") sleep(1) self.run_demo("Clipboard") clipboard_window = self.demo.find_children( lambda x: x.name == "Clipboard" and x.role_name == "frame" )[-1] text_field = clipboard_window.child(role_name="text") text_field.text = "testing " key_combo("") sleep(0.2) key_combo("") sleep(0.2) key_combo("") sleep(0.2) key_combo("") sleep(0.2) self.assertEqual(text_field.text, "testing testing ") def test_key_combo_wrong_key(self): """ Testing key combo with wrong key. """ with self.assertRaises(ValueError): key_combo("") def test_type_text(self): """ Testing type text. """ press_key("Esc") sleep(1) self.run_demo("Clipboard") clipboard_window = self.demo.find_children( lambda x: x.name == "Clipboard" and x.role_name == "frame" )[-1] text_field = clipboard_window.child(role_name="text") text_field.click() type_text("testing type text") sleep(1) copy_button = clipboard_window.child("Copy") copy_button.click() paste_button = clipboard_window.child("Paste") paste_button.click() number_of_text_fields = self.demo.find_children( lambda x: (x.text == "testing type text") or (x.name == "testing type text" and x.role_name == "label") ) self.assertEqual(len(number_of_text_fields), 2) if __name__ == "__main__": unittest.main() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests/test_rawinput_keyboard_gtk4.py000066400000000000000000000071551520163156700302650ustar00rootroot00000000000000#!/usr/bin/python3 """ Unit tests for the dogtail.rawinput keyboard methods. """ # pylint: disable=import-outside-toplevel import os from time import sleep import unittest try: from tests.test_gtk_demo import Gtk4DemoTest except ImportError: from test_gtk_demo import Gtk4DemoTest from dogtail.rawinput import ( press_key, key_combo, type_text, ) @unittest.skipIf(not os.path.isfile("/usr/bin/gtk4-demo"), "Skipping, no gtk4-demo.") class TestGtk4DemoRawinputKeyboard(Gtk4DemoTest): """ Class to test dogtail's rawinput keyboard methods. """ def test_press_key(self): """ Testing press key. """ press_key("Esc") sleep(1) press_key("Super") sleep(1) press_key("Esc") sleep(1) def test_press_key_no_such_key(self): """ Testing press key with no such key. """ with self.assertRaises(KeyError): press_key("No such key.") def test_key_combo_simple(self): """ Testing simple key combo. """ key_combo("") self.assertTrue(self.demo.child("Tree View").showing) def test_key_combo_multi_key(self): """ Testing simple key combo with multiple keys. """ press_key("Esc") sleep(1) clipboard_button = self.demo.child("Clipboard") clipboard_button.double_click() clipboard_window = self.demo.find_children( lambda x: x.name == "Clipboard" and x.role_name == "frame" )[-1] text_field = clipboard_window.child(role_name="text") text_field.text = "testing " # Another example of wrong coordinates. # So I locate a know node and just offset from it. self.demo.child("Copy").click() sleep(0.2) self.demo.child("Text Drag Source").click() key_combo("") sleep(0.2) key_combo("") sleep(0.2) key_combo("") sleep(0.2) key_combo("") sleep(0.2) self.assertEqual(text_field.text, "testing testing ") def test_key_combo_wrong_key(self): """ Testing key combo with wrong key. """ with self.assertRaises(ValueError): key_combo("") def test_type_text(self): """ Testing type text. """ press_key("Esc") sleep(1) clipboard_button = self.demo.child("Clipboard") clipboard_button.double_click() clipboard_window = self.demo.find_children( lambda x: x.name == "Clipboard" and x.role_name == "frame" )[-1] text_field = clipboard_window.child(role_name="text") text_field.click() # Another example of wrong coordinates. # So I locate a know node and just offset from it. self.demo.child("Copy").click() sleep(0.2) self.demo.child("Text Drag Source").click() key_combo("") type_text("testing type text") sleep(1) # Another example of wrong coordinates. copy_button = clipboard_window.child("Copy") copy_button.click() # Another example of wrong coordinates. paste_button = clipboard_window.child("Paste") paste_button.click() number_of_text_fields = self.demo.find_children( lambda x: (x.text == "testing type text") or (x.name == "testing type text" and x.role_name == "label") ) self.assertEqual(len(number_of_text_fields), 2) if __name__ == "__main__": unittest.main() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests/test_rawinput_mouse_buttons_gtk3.py000066400000000000000000000042151520163156700313640ustar00rootroot00000000000000#!/usr/bin/python3 """ Unit tests for the dogtail.rawinput mouse buttons. """ # pylint: disable=import-outside-toplevel import os import unittest from time import sleep try: from tests.test_gtk_demo import Gtk3DemoTest except ImportError: from test_gtk_demo import Gtk3DemoTest from dogtail.rawinput import ( click, double_click, press, release, press_key ) @unittest.skipIf(not os.path.isfile("/usr/bin/gtk3-demo"), "Skipping, no gtk3-demo.") class TestGtk3DemoRawinputMouse(Gtk3DemoTest): """ Class to test dogtail's rawinput mouse buttons. """ def test_click_in_demo(self): """ Testing click. """ press_key("Esc") sleep(1) builder_button = self.demo.child("Builder") self.assertFalse(builder_button.focused) click(*builder_button.center) sleep(1) self.assertTrue(builder_button.focused) def test_double_click_in_demo(self): """ Testing double_click. """ press_key("Esc") sleep(1) builder_button = self.demo.child("Builder") # At first it is not focused so the frame is named differently. self.assertEqual(len([x for x in self.demo.children if x.name == "Builder"]), 0) # Upon clicking the Builder will appear in the main frame too. click(*builder_button.center) self.assertEqual(len([x for x in self.demo.children if x.name == "Builder"]), 1) double_click(*builder_button.center) # Upon double click the new frame will open with the same name. self.assertEqual(len([x for x in self.demo.children if x.name == "Builder"]), 2) def test_press_release_in_demo(self): """ Testing press and release. """ press_key("Esc") sleep(1) self.run_demo("Builder") save_button = self.demo.child("Save", showing_only=True) self.assertFalse(save_button.armed) press(*save_button.center) self.assertTrue(save_button.armed) release(*save_button.center) self.assertFalse(save_button.armed) if __name__ == "__main__": unittest.main() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests/test_rawinput_mouse_buttons_gtk4.py000066400000000000000000000045601520163156700313700ustar00rootroot00000000000000#!/usr/bin/python3 """ Unit tests for the dogtail.rawinput mouse buttons. """ # pylint: disable=import-outside-toplevel import os import unittest from time import sleep try: from tests.test_gtk_demo import Gtk4DemoTest except ImportError: from test_gtk_demo import Gtk4DemoTest from dogtail.rawinput import ( click, double_click, press, release, press_key ) @unittest.skipIf(not os.path.isfile("/usr/bin/gtk4-demo"), "Skipping, no gtk4-demo.") class TestGtk4DemoRawinputMouse(Gtk4DemoTest): """ Class to test dogtail's rawinput mouse buttons. """ def test_click_in_demo(self): """ Testing click. """ press_key("Esc") sleep(1) builder_button = self.demo.child("Builder") self.assertFalse(builder_button.focused) click(*builder_button.center) self.assertTrue(builder_button.focused) def test_double_click_in_demo(self): """ Testing double_click. """ press_key("Esc") sleep(1) builder_button = self.demo.child("Builder") # At first it is not focused so the frame is named differently. self.assertEqual(len([x for x in self.demo.children if x.name == "Builder"]), 0) # Upon clicking the Builder will appear in the main frame too. click(*builder_button.center) self.assertEqual(len([x for x in self.demo.children if x.name == "Builder"]), 1) double_click(*builder_button.center) # Upon double click the new frame will open with the same name. self.assertEqual(len([x for x in self.demo.children if x.name == "Builder"]), 2) # No longer working in gtk4. @unittest.SkipTest def test_press_release_in_demo(self): """ Testing press and release. """ press_key("Esc") sleep(1) builder_button = self.demo.child("Builder") builder_button.double_click() save_button = self.demo.find_child( lambda x: x.name == "" and x.description == "Save a file" and x.role_name in ("button","push button") ) self.assertFalse(save_button.armed) press(*save_button.center) sleep(1) self.assertTrue(save_button.armed) release(*save_button.center) self.assertFalse(save_button.armed) if __name__ == "__main__": unittest.main() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests/test_tree_gtk3.py000066400000000000000000000241031520163156700254620ustar00rootroot00000000000000#!/usr/bin/python3 """ Unit tests for the dogtail.tree Node. """ # pylint: disable=import-outside-toplevel # pylint: disable=wrong-import-position # pylint: disable=wrong-import-order # ruff: noqa: E402 import os import unittest try: from tests.test_gtk_demo import Gtk3DemoTest except ImportError: from test_gtk_demo import Gtk3DemoTest from dogtail.tree import root from dogtail.config import config import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi @unittest.skipIf(not os.path.isfile("/usr/bin/gtk3-demo"), "Skipping, no gtk3-demo.") class TestGtk3DogtailNode(Gtk3DemoTest): """ Class to test dogtail's Node. """ def test_not_existing_attribute(self): """ Testing not existing attribute. """ self.assertRaises(AttributeError, getattr, self.demo, "not_existing_attribute") def test_get_application_name(self): """ Testing get application name. """ self.assertEqual(self.demo.name, "gtk3-demo") def test_get_root_name(self): """ Testing get root name. """ self.assertEqual(root.name, "main") def test_set_name(self): """ Testing setting a read only attribute. """ self.assertRaises(AttributeError, self.demo.__setattr__, "name", "not possible") def test_get_application_role(self): """ Testing get application role. """ self.assertEqual(self.demo.role, Atspi.Role.APPLICATION) def test_set_application_role(self): """ Testing set application role. """ with self.assertRaises((RuntimeError, AttributeError)): self.demo.role = Atspi.Role.INVALID def test_get_application_role_name(self): """ Testing get application role name. """ self.assertEqual(self.demo.role_name, "application") def test_set_application_role_name(self): """ Testing set application role name. """ with self.assertRaises(AttributeError): self.demo.role_name = "not possible" def test_get_application_description(self): """ Testing get application description. """ self.assertEqual(self.demo.description, "") # TODO revisit this. @unittest.SkipTest def test_set_application_description(self): """ Testing set application description. """ # Does not appear to be the case. Interesting. with self.assertRaises(RuntimeError): self.demo.description = "not possible" def test_get_application_children(self): """ Testing get application children. """ self.assertIsNotNone(self.demo.children) self.assertTrue(isinstance(self.demo.children[0], Atspi.Accessible)) def test_set_application_children(self): """ Testing set application children. """ with self.assertRaises(AttributeError): self.demo.children = [] # TODO do we actually need children limit? @unittest.SkipTest def test_get_application_children_limit(self): """ Testing get application children with limit. """ config.children_limit = 1 self.assertIsNotNone(self.demo.children) result = self.demo.child(role_name="tree table") self.assertEqual(len(result.children), 1) def test_get_application_parent(self): """ Testing get application parent. """ self.assertIsNotNone(self.demo.children) self.assertIsNotNone(self.demo.children[0].parent) self.assertEqual(self.demo.children[0].parent, self.demo) def test_set_application_parent(self): """ Testing set application parent. """ with self.assertRaises(AttributeError): self.demo.children[0].parent = None def test_get_application_text(self): """ Testing get application text. """ self.run_demo("Dialogs and Message Boxes") window = self.demo.find_child( lambda x: x.name == "Dialogs and Message Boxes" and x.role_name == "frame", retry=False ) entry_one = window.child(name="Entry 1", role_name="label").labelee entry_two = window.child(name="", role_name="text") self.assertEqual(entry_one.text, "") self.assertEqual(entry_two.text, "") def test_set_application_text(self): """ Testing set application text. """ self.run_demo("Dialogs and Message Boxes") window = self.demo.find_child( lambda x: x.name == "Dialogs and Message Boxes" and x.role_name == "frame", retry=False ) entry_one = window.child(name="Entry 1", role_name="label").labelee entry_two = window.child(name="", role_name="text") entry_one.text = "test one" entry_two.text = "test two" self.assertEqual(entry_one.text, "test one") self.assertEqual(entry_two.text, "test two") # Atspi does not throw error on this, no point to have failing test then. @unittest.SkipTest def test_set_application_text_on_non_text_interface(self): """ Testing set application text. """ with self.assertRaises(AttributeError): self.demo.text = "not possible" @unittest.SkipTest def test_get_relations(self): """ Testing relations. """ # We use this only for labels, lets keep the note if some other uses appear. def test_get_labelee(self): """ Testing get labelee. """ self.run_demo("Dialogs and Message Boxes") window = self.demo.find_child( lambda x: x.name == "Dialogs and Message Boxes" and x.role_name == "frame", retry=False ) entry_one = window.child(name="Entry 1", role_name="label") self.assertEqual(entry_one.labelee.role_name, "text") def test_set_labelee(self): """ Testing set labelee. """ with self.assertRaises(AttributeError): self.demo.labelee = None def test_get_labeler(self): """ Testing get labeler. """ self.run_demo("Dialogs and Message Boxes") window = self.demo.find_child( lambda x: x.name == "Dialogs and Message Boxes" and x.role_name == "frame", retry=False ) entry_one_labelee = window.child(name="Entry 1", role_name="label").labelee self.assertEqual(entry_one_labelee.labeler.role_name, "label") def test_set_labeler(self): """ Testing set labeler. """ with self.assertRaises(AttributeError): self.demo.labeler = None def test_get_actions(self): """ Testing get actions. """ self.assertEqual(len(self.demo.actions), 0) def test_set_actions(self): """ Testing set actions. """ with self.assertRaises(AttributeError): self.demo.actions = {} def test_get_extents(self): """ Testing get extents. """ (x_position, y_position, width, height) = self.demo.children[0].extents self.assertTrue(x_position >= 0) self.assertTrue(y_position >= 0) self.assertTrue(width > 0) self.assertTrue(height > 0) def test_get_from_wrong_node(self): """ Testing get extents but some nodes do not provide it. """ self.assertIsNone(self.demo.extents) def test_set_extents(self): """ Testing set extents. """ with self.assertRaises(AttributeError): self.demo.extents = (0, 0, 500, 500) def test_get_position(self): """ Testing get position. """ (x_position, y_position) = self.demo.children[0].position self.assertTrue(isinstance(x_position, int)) self.assertTrue(isinstance(y_position, int)) def test_get_position_from_wrong_node(self): """ Testing get position but some nodes do not provide it. """ # The root object does not have Component Interface - we return (-1, -1) self.assertEqual(self.demo.position, (-1, -1)) def test_set_position(self): """ Testing set position. """ with self.assertRaises(AttributeError): self.demo.position = (500, 500) def test_get_size(self): """ Testing get size. """ (x_size, y_size) = self.demo.children[0].size self.assertTrue(isinstance(x_size, int)) self.assertTrue(isinstance(y_size, int)) self.assertTrue(x_size > 0) self.assertTrue(y_size > 0) def test_get_size_from_wrong_node(self): """ Testing get size but some nodes do not provide it. """ # The root object does not have Component Interface - we return (-1, -1) self.assertEqual(self.demo.size, (-1, -1)) def test_set_size(self): """ Testing set size. """ with self.assertRaises(AttributeError): self.demo.size = (500, 500) def test_get_toolkit(self): """ Testing get toolkit. """ self.assertIsNotNone(self.demo.toolkit) def test_set_toolkit(self): """ Testing set toolkit. """ with self.assertRaises(AttributeError): self.demo.toolkit = "not possible" def test_get_id(self): """ Testing get Id. """ self.assertIsNotNone(self.demo.id) def test_set_id(self): """ Testing set id. """ with self.assertRaises(AttributeError): self.demo.id = "not possible" def test_child_unsupported_argument_raises_type_error(self): """ Testing child method raises TypeError upon using unsupported argument. """ with self.assertRaises(TypeError): self.demo.child(not_supported="not supported") if __name__ == "__main__": unittest.main() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests/test_tree_gtk4.py000066400000000000000000000303741520163156700254720ustar00rootroot00000000000000#!/usr/bin/python3 """ Unit tests for the dogtail.tree Node. """ # pylint: disable=import-outside-toplevel # pylint: disable=wrong-import-position # pylint: disable=wrong-import-order # ruff: noqa: E402 import os import unittest from time import sleep try: from tests.test_gtk_demo import Gtk4DemoTest except ImportError: from test_gtk_demo import Gtk4DemoTest from dogtail.tree import root, SearchError from dogtail.config import config from dogtail.rawinput import type_text, press_key import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi @unittest.skipIf(not os.path.isfile("/usr/bin/gtk4-demo"), "Skipping, no gtk4-demo.") class TestGtk4DogtailNode(Gtk4DemoTest): """ Class to test dogtail's Node. """ def test_not_existing_attribute(self): """ Testing not existing attribute. """ self.assertRaises(AttributeError, getattr, self.demo, "not_existing_attribute") def test_get_application_name(self): """ Testing get application name. """ self.assertEqual(self.demo.name, "gtk4-demo") def test_get_root_name(self): """ Testing get root name. """ self.assertEqual(root.name, "main") def test_set_name(self): """ Testing setting a read only attribute. """ self.assertRaises(AttributeError, self.demo.__setattr__, "name", "not possible") def test_get_application_role(self): """ Testing get application role. """ self.assertEqual(self.demo.role, Atspi.Role.APPLICATION) def test_set_application_role(self): """ Testing set application role. """ with self.assertRaises((RuntimeError, AttributeError)): self.demo.role = Atspi.Role.INVALID def test_get_application_role_name(self): """ Testing get application role name. """ self.assertEqual(self.demo.role_name, "application") def test_set_application_role_name(self): """ Testing set application role name. """ with self.assertRaises(AttributeError): self.demo.role_name = "not possible" def test_get_application_description(self): """ Testing get application description. """ self.assertEqual(self.demo.children[0].description, "") # TODO revisit this. @unittest.SkipTest def test_set_application_description(self): """ Testing set application description. """ # Does not appear to be the case. Interesting. with self.assertRaises(RuntimeError): self.demo.description = "not possible" def test_get_application_children(self): """ Testing get application children. """ self.assertIsNotNone(self.demo.children) self.assertTrue(isinstance(self.demo.children[0], Atspi.Accessible)) def test_set_application_children(self): """ Testing set application children. """ with self.assertRaises(AttributeError): self.demo.children = [] # TODO do we actually need children limit? @unittest.SkipTest def test_get_application_children_limit(self): """ Testing get application children with limit. """ config.children_limit = 1 self.assertIsNotNone(self.demo.children) result = self.demo.child(role_name="tree table") self.assertEqual(len(result.children), 1) def test_get_application_parent(self): """ Testing get application parent. """ self.assertIsNotNone(self.demo.children) self.assertIsNotNone(self.demo.children[0].parent) self.assertEqual(self.demo.children[0].parent, self.demo) def test_set_application_parent(self): """ Testing set application parent. """ with self.assertRaises(AttributeError): self.demo.children[0].parent = None def test_get_application_text(self): """ Testing get application text. """ press_key("Esc") sleep(1) # Weird way to do this but it works as a unit test. window = None scroll_bar = self.demo.find_child(lambda x: x.role_name=="scroll bar" and x.showing) for _ in range(50): scroll_bar.value += 100 sleep(1) try: clipboard_button = self.demo.child("Dialogs", retry=False) clipboard_button.double_click() sleep(1) window = self.demo.find_children( lambda x: x.name == "Dialogs" and x.role_name == "frame" )[-1] break except (RuntimeError, SearchError, IndexError): pass self.assertIsNotNone(window) entry_one = window.child(name="Entry 1", role_name="text") entry_two = window.child(name="", role_name="text") self.assertEqual(entry_one.text, "") self.assertEqual(entry_two.text, "") def test_set_application_text(self): """ Testing set application text. """ press_key("Esc") sleep(1) # Weird way to do this but it works as a unit test. scroll_bar = self.demo.child(role_name="scroll bar") for _ in range(50): scroll_bar.value += 100 try: clipboard_button = self.demo.child("Dialogs", retry=False) clipboard_button.double_click() window = self.demo.find_children( lambda x: x.name == "Dialogs" and x.role_name == "frame" )[-1] break except (RuntimeError, SearchError, IndexError): pass entry_one = window.child(name="Entry 1", role_name="text") entry_two = window.child(name="", role_name="text") # No longer works in gtk4. # entry_one.text = "test one" # entry_two.text = "test two" # Coordinates are wrong, offset it. entry_one.click() type_text("test one") # Coordinates are wrong, offset it. entry_two.click() type_text("test two") self.assertEqual(entry_one.text, "test one") self.assertEqual(entry_two.text, "test two") # Atspi does not throw error on this, no point to have failing test then. @unittest.SkipTest def test_set_application_text_on_non_text_interface(self): """ Testing set application text. """ with self.assertRaises(AttributeError): self.demo.text = "not possible" @unittest.SkipTest def test_get_relations(self): """ Testing relations. """ # We use this only for labels, lets keep the note if some other uses appear. # This is bugged in gtk4-demo, lets skip this test. @unittest.SkipTest def test_get_labelee(self): """ Testing get labelee. """ press_key("Esc") sleep(1) # Weird way to do this but it works as a unit test. scroll_bar = self.demo.child(role_name="scroll bar") for _ in range(50): scroll_bar.value += 100 try: clipboard_button = self.demo.child("Dialogs", retry=False) clipboard_button.double_click() window = self.demo.find_children( lambda x: x.name == "Dialogs" and x.role_name == "frame" )[-1] break except (RuntimeError, SearchError, IndexError): pass entry_one = window.child(name="Entry 1", role_name="label") self.assertEqual(entry_one.labelee.role_name, "text") def test_set_labelee(self): """ Testing set labelee. """ with self.assertRaises(AttributeError): self.demo.labelee = None def test_get_labeler(self): """ Testing get labeler. """ press_key("Esc") sleep(1) # Weird way to do this but it works as a unit test. scroll_bar = self.demo.child(role_name="scroll bar") for _ in range(50): scroll_bar.value += 100 try: clipboard_button = self.demo.child("Dialogs", retry=False) clipboard_button.double_click() sleep(1) window = self.demo.find_children( lambda x: x.name == "Dialogs" and x.role_name == "frame" )[-1] break except (RuntimeError, SearchError, IndexError): pass entry_one_labelee = window.child(name="Entry 1", role_name="text") self.assertEqual(entry_one_labelee.labeler.role_name, "label") def test_set_labeler(self): """ Testing set labeler. """ with self.assertRaises(AttributeError): self.demo.labeler = None def test_get_actions(self): """ Testing get actions. """ self.assertEqual(len(self.demo.actions), 0) def test_set_actions(self): """ Testing set actions. """ with self.assertRaises(AttributeError): self.demo.actions = {} def test_get_extents(self): """ Testing get extents. """ (x_position, y_position, width, height) = self.demo.children[0].extents self.assertTrue(x_position >= 0) self.assertTrue(y_position >= 0) self.assertTrue(width > 0) self.assertTrue(height > 0) def test_get_from_wrong_node(self): """ Testing get extents but some nodes do not provide it. """ self.assertIsNone(self.demo.extents) def test_set_extents(self): """ Testing set extents. """ with self.assertRaises(AttributeError): self.demo.extents = (0, 0, 500, 500) def test_get_position(self): """ Testing get position. """ (x_position, y_position) = self.demo.children[0].position self.assertTrue(isinstance(x_position, int)) self.assertTrue(isinstance(y_position, int)) def test_get_position_from_wrong_node(self): """ Testing get position but some nodes do not provide it. """ # The root object does not have Component Interface - we return (-1, -1) self.assertEqual(self.demo.position, (-1, -1)) def test_set_position(self): """ Testing set position. """ with self.assertRaises(AttributeError): self.demo.position = (500, 500) def test_get_size(self): """ Testing get size. """ (x_size, y_size) = self.demo.children[0].size self.assertTrue(isinstance(x_size, int)) self.assertTrue(isinstance(y_size, int)) self.assertTrue(x_size > 0) self.assertTrue(y_size > 0) def test_get_size_from_wrong_node(self): """ Testing get size but some nodes do not provide it. """ # The root object does not have Component Interface - we return (-1, -1) self.assertEqual(self.demo.size, (-1, -1)) def test_set_size(self): """ Testing set size. """ with self.assertRaises(AttributeError): self.demo.size = (500, 500) def test_get_toolkit(self): """ Testing get toolkit. """ self.assertIsNotNone(self.demo.toolkit) def test_set_toolkit(self): """ Testing set toolkit. """ with self.assertRaises(AttributeError): self.demo.toolkit = "not possible" def test_get_id(self): """ Testing get Id. """ self.assertIsNotNone(self.demo.id) def test_set_id(self): """ Testing set id. """ with self.assertRaises(AttributeError): self.demo.id = "not possible" def test_child_unsupported_argument_raises_type_error(self): """ Testing child method raises TypeError upon using unsupported argument. """ with self.assertRaises(TypeError): self.demo.child(not_supported="not supported") if __name__ == "__main__": unittest.main() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests/test_tree_search_gtk3.py000066400000000000000000000055201520163156700270110ustar00rootroot00000000000000#!/usr/bin/python3 """ Unit tests for the dogtail.tree Node Search. """ # pylint: disable=import-outside-toplevel # pylint: disable=wrong-import-position # pylint: disable=wrong-import-order # ruff: noqa: E402 import os import unittest import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi from dogtail.predicate import GenericPredicate try: from tests.test_gtk_demo import Gtk3DemoTest except ImportError: from test_gtk_demo import Gtk3DemoTest @unittest.skipIf(not os.path.isfile("/usr/bin/gtk3-demo"), "Skipping, no gtk3-demo.") class TestDogtailNodeSearch(Gtk3DemoTest): """ Class to test dogtail's Node Search. """ def test_child(self): """ Testing child. """ child_result = self.demo.child("Info") self.assertIsNotNone(child_result) self.assertIsInstance(child_result, Atspi.Accessible) def test_find_child(self): """ Testing find child. """ find_child_result = self.demo.find_child(lambda x: x.name == "Info") self.assertIsNotNone(find_child_result) self.assertIsInstance(find_child_result, Atspi.Accessible) def test_find_children(self): """ Testing find children. """ find_children_result = self.demo.find_children(lambda x: x.name == "Info") self.assertIsNotNone(self.demo.find_children(lambda x: x.name == "Info")) self.assertIsInstance(find_children_result, list) def test_find_child_with_predicate(self): """ Testing find child with predicate. """ find_child_result = self.demo.find_child(GenericPredicate(name="Info")) self.assertIsNotNone(find_child_result) self.assertIsInstance(find_child_result, Atspi.Accessible) def test_find_children_with_predicate(self): """ Testing find children with predicate. """ find_children_result = self.demo.find_children( GenericPredicate(name="Info") ) self.assertIsNotNone(find_children_result) self.assertIsInstance(find_children_result, list) def test_find_ancestor(self): """ Testing find ancestor. """ find_ancestor_result = self.demo.children[0].find_ancestor( lambda x: x.role_name == "application" ) self.assertIsNotNone(find_ancestor_result) self.assertIsInstance(find_ancestor_result, Atspi.Accessible) def test_is_child(self): """ Testing is child. """ child_name = self.demo.children[0].name child_role_name = self.demo.children[0].role_name is_child_result = self.demo.is_child(child_name, child_role_name) self.assertEqual(is_child_result, True) self.assertIsInstance(is_child_result, bool) if __name__ == "__main__": unittest.main() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests/test_tree_search_gtk4.py000066400000000000000000000055201520163156700270120ustar00rootroot00000000000000#!/usr/bin/python3 """ Unit tests for the dogtail.tree Node Search. """ # pylint: disable=import-outside-toplevel # pylint: disable=wrong-import-position # pylint: disable=wrong-import-order # ruff: noqa: E402 import os import unittest import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi from dogtail.predicate import GenericPredicate try: from tests.test_gtk_demo import Gtk4DemoTest except ImportError: from test_gtk_demo import Gtk4DemoTest @unittest.skipIf(not os.path.isfile("/usr/bin/gtk4-demo"), "Skipping, no gtk4-demo.") class TestDogtailNodeSearch(Gtk4DemoTest): """ Class to test dogtail's Node Search. """ def test_child(self): """ Testing child. """ child_result = self.demo.child("Info") self.assertIsNotNone(child_result) self.assertIsInstance(child_result, Atspi.Accessible) def test_find_child(self): """ Testing find child. """ find_child_result = self.demo.find_child(lambda x: x.name == "Info") self.assertIsNotNone(find_child_result) self.assertIsInstance(find_child_result, Atspi.Accessible) def test_find_children(self): """ Testing find children. """ find_children_result = self.demo.find_children(lambda x: x.name == "Info") self.assertIsNotNone(self.demo.find_children(lambda x: x.name == "Info")) self.assertIsInstance(find_children_result, list) def test_find_child_with_predicate(self): """ Testing find child with predicate. """ find_child_result = self.demo.find_child(GenericPredicate(name="Info")) self.assertIsNotNone(find_child_result) self.assertIsInstance(find_child_result, Atspi.Accessible) def test_find_children_with_predicate(self): """ Testing find children with predicate. """ find_children_result = self.demo.find_children( GenericPredicate(name="Info") ) self.assertIsNotNone(find_children_result) self.assertIsInstance(find_children_result, list) def test_find_ancestor(self): """ Testing find ancestor. """ find_ancestor_result = self.demo.children[0].find_ancestor( lambda x: x.role_name == "application" ) self.assertIsNotNone(find_ancestor_result) self.assertIsInstance(find_ancestor_result, Atspi.Accessible) def test_is_child(self): """ Testing is child. """ child_name = self.demo.children[0].name child_role_name = self.demo.children[0].role_name is_child_result = self.demo.is_child(child_name, child_role_name) self.assertEqual(is_child_result, True) self.assertIsInstance(is_child_result, bool) if __name__ == "__main__": unittest.main() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests/test_tree_selection_gtk3.py000066400000000000000000000130431520163156700275300ustar00rootroot00000000000000#!/usr/bin/python3 """ Unit tests for the dogtail.tree Node Selection. """ # pylint: disable=import-outside-toplevel # pylint: disable=wrong-import-position # pylint: disable=wrong-import-order # ruff: noqa: E402 import os import unittest from time import sleep from dogtail.tree import SearchError from dogtail.rawinput import press_key try: from tests.test_gtk_demo import Gtk3DemoTest except ImportError: from test_gtk_demo import Gtk3DemoTest @unittest.skipIf(not os.path.isfile("/usr/bin/gtk3-demo"), "Skipping, no gtk3-demo.") class TestGtk3DogtailNodeSelection(Gtk3DemoTest): """ Class to test dogtail's Node Selection. """ def test_select_simple(self): """ Testing select simple. """ info_node = self.demo.child("Info") source_node = self.demo.child("Source") self.assertTrue(info_node.selected) self.assertFalse(source_node.selected) source_node.select() self.assertFalse(info_node.selected) self.assertTrue(source_node.selected) def test_select(self): """ Testing select. """ press_key("Esc") sleep(1) # Weird way to do this but it works as a unit test. scroll_bar = self.demo.find_child(lambda x: x.role_name == "scroll bar" and x.showing) for _ in range(20): scroll_bar.value += 100 try: self.demo.child("Icon View").click() sleep(0.5) press_key("+") self.run_demo("Icon View Basics") break except (RuntimeError, SearchError, ValueError): sleep(1) window = self.demo.find_children( lambda x: x.name == "Icon View Basics" and x.role_name == "frame" )[-1] layered_pane = window.child(role_name="layered pane") self.assertFalse(layered_pane.children[1].selected) layered_pane.children[1].select() self.assertTrue(layered_pane.children[1].selected) def test_deselect(self): """ Testing deselect. """ press_key("Esc") sleep(1) # Weird way to do this but it works as a unit test. scroll_bar = self.demo.find_child(lambda x: x.role_name == "scroll bar" and x.showing) for _ in range(20): scroll_bar.value += 100 try: self.demo.child("Icon View").click() sleep(0.5) press_key("+") self.run_demo("Icon View Basics") break except (RuntimeError, SearchError, ValueError): sleep(1) window = self.demo.find_children( lambda x: x.name == "Icon View Basics" and x.role_name == "frame" )[-1] layered_pane = window.child(role_name="layered pane") layered_pane.children[1].select() self.assertTrue(layered_pane.children[1].selected) layered_pane.children[1].deselect() self.assertFalse(layered_pane.children[1].selected) def test_select_all(self): """ Testing select all. """ press_key("Esc") sleep(1) # Weird way to do this but it works as a unit test. scroll_bar = self.demo.find_child(lambda x: x.role_name == "scroll bar" and x.showing) for _ in range(20): scroll_bar.value += 100 try: self.demo.child("Icon View").click() sleep(0.5) press_key("+") self.run_demo("Icon View Basics") break except (RuntimeError, SearchError, ValueError): sleep(1) window = self.demo.find_children( lambda x: x.name == "Icon View Basics" and x.role_name == "frame" )[-1] layered_pane = window.child(role_name="layered pane") self.assertFalse(layered_pane.children[0].selected) self.assertFalse(layered_pane.children[1].selected) self.assertFalse(layered_pane.children[2].selected) layered_pane.select_all() self.assertTrue(layered_pane.children[0].selected) self.assertTrue(layered_pane.children[1].selected) self.assertTrue(layered_pane.children[2].selected) def test_deselect_all(self): """ Testing deselect all. """ press_key("Esc") sleep(1) # Weird way to do this but it works as a unit test. scroll_bar = self.demo.find_child(lambda x: x.role_name == "scroll bar" and x.showing) for _ in range(20): scroll_bar.value += 100 try: self.demo.child("Icon View").click() sleep(0.5) press_key("+") self.run_demo("Icon View Basics") break except (RuntimeError, SearchError, ValueError): sleep(1) window = self.demo.find_children( lambda x: x.name == "Icon View Basics" and x.role_name == "frame" )[-1] layered_pane = window.child(role_name="layered pane") layered_pane.select_all() self.assertTrue(layered_pane.children[0].selected) self.assertTrue(layered_pane.children[1].selected) self.assertTrue(layered_pane.children[2].selected) layered_pane.deselect_all() self.assertFalse(layered_pane.children[0].selected) self.assertFalse(layered_pane.children[1].selected) self.assertFalse(layered_pane.children[2].selected) if __name__ == "__main__": unittest.main() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests/test_tree_selection_gtk4.py000066400000000000000000000120721520163156700275320ustar00rootroot00000000000000#!/usr/bin/python3 """ Unit tests for the dogtail.tree Node Selection. """ # pylint: disable=import-outside-toplevel # pylint: disable=wrong-import-position # pylint: disable=wrong-import-order # ruff: noqa: E402 import os import unittest try: from tests.test_gtk_demo import Gtk4DemoTest except ImportError: from test_gtk_demo import Gtk4DemoTest @unittest.skipIf(not os.path.isfile("/usr/bin/gtk4-demo"), "Skipping, no gtk4-demo.") class TestGtk4DogtailNodeSelection(Gtk4DemoTest): """ Class to test dogtail's Node Selection. """ def test_select_simple(self): """ Testing select simple. """ info_node = self.demo.child("Info") source_node = self.demo.child("Source") self.assertTrue(info_node.selected) self.assertFalse(source_node.selected) source_node.select() self.assertFalse(info_node.selected) self.assertTrue(source_node.selected) # Not working in gtk4-demo. @unittest.SkipTest def test_select(self): """ Testing select. """ # Weird way to do this but it works as a unit test. scroll_bar = self.demo.child(role_name="scroll bar") for _ in range(50): scroll_bar.value += 100 try: icon_view = self.demo.child("Icon View Basics", retry=False) icon_view.double_click() break except RuntimeError: pass window = self.demo.find_children( lambda x: x.name == "Icon View Basics" and x.role_name == "frame" )[-1] layered_pane = window.child(role_name="layered pane") self.assertFalse(layered_pane.children[1].selected) layered_pane.children[1].select() self.assertTrue(layered_pane.children[1].selected) # Not working in gtk4-demo. @unittest.SkipTest def test_deselect(self): """ Testing deselect. """ # Weird way to do this but it works as a unit test. scroll_bar = self.demo.child(role_name="scroll bar") for _ in range(50): scroll_bar.value += 100 try: icon_view = self.demo.child("Icon View Basics", retry=False) icon_view.double_click() break except RuntimeError: pass window = self.demo.find_children( lambda x: x.name == "Icon View Basics" and x.role_name == "frame" )[-1] layered_pane = window.child(role_name="layered pane") layered_pane.children[1].select() self.assertTrue(layered_pane.children[1].selected) layered_pane.children[1].deselect() self.assertFalse(layered_pane.children[1].selected) # Not working in gtk4-demo. @unittest.SkipTest def test_select_all(self): """ Testing select all. """ # Weird way to do this but it works as a unit test. scroll_bar = self.demo.child(role_name="scroll bar") for _ in range(50): scroll_bar.value += 100 try: icon_view = self.demo.child("Icon View Basics", retry=False) icon_view.double_click() break except RuntimeError: pass window = self.demo.find_children( lambda x: x.name == "Icon View Basics" and x.role_name == "frame" )[-1] layered_pane = window.child(role_name="layered pane") self.assertFalse(layered_pane.children[0].selected) self.assertFalse(layered_pane.children[1].selected) self.assertFalse(layered_pane.children[2].selected) layered_pane.select_all() self.assertTrue(layered_pane.children[0].selected) self.assertTrue(layered_pane.children[1].selected) self.assertTrue(layered_pane.children[2].selected) # Not working in gtk4-demo. @unittest.SkipTest def test_deselect_all(self): """ Testing deselect all. """ # Weird way to do this but it works as a unit test. scroll_bar = self.demo.child(role_name="scroll bar") for _ in range(50): scroll_bar.value += 100 try: icon_view = self.demo.child("Icon View Basics", retry=False) icon_view.double_click() break except RuntimeError: pass window = self.demo.find_children( lambda x: x.name == "Icon View Basics" and x.role_name == "frame" )[-1] layered_pane = window.child(role_name="layered pane") layered_pane.select_all() self.assertTrue(layered_pane.children[0].selected) self.assertTrue(layered_pane.children[1].selected) self.assertTrue(layered_pane.children[2].selected) layered_pane.deselect_all() self.assertFalse(layered_pane.children[0].selected) self.assertFalse(layered_pane.children[1].selected) self.assertFalse(layered_pane.children[2].selected) if __name__ == "__main__": unittest.main() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests/test_tree_states_gtk3.py000066400000000000000000000071441520163156700270530ustar00rootroot00000000000000#!/usr/bin/python3 """ Unit tests for the dogtail.tree Node States. """ # pylint: disable=import-outside-toplevel # pylint: disable=wrong-import-position # pylint: disable=wrong-import-order # ruff: noqa: E402 import os import unittest from time import sleep try: from tests.test_gtk_demo import Gtk3DemoTest except ImportError: from test_gtk_demo import Gtk3DemoTest from dogtail.tree import root, Node @unittest.skipIf(not os.path.isfile("/usr/bin/gtk3-demo"), "Skipping, no gtk3-demo.") class TestGtk3DogtailNodeStates(Gtk3DemoTest): """ Class to test dogtail's Node States. """ def test_get_selected(self): """ Testing get selected. """ info_node = self.demo.child("Info") self.assertTrue(info_node.selected) def test_set_selected(self): """ Testing get selected. """ info_node = self.demo.child("Info") with self.assertRaises(AttributeError): info_node.selected = "not possible" def test_get_selectable(self): """ Testing get selectable. """ info_node = self.demo.child("Info") self.assertTrue(info_node.selected) def test_set_selectable(self): """ Testing get selectable. """ info_node = self.demo.child("Info") with self.assertRaises(AttributeError): info_node.selectable = "not possible" def test_get_sensitive(self): """ Testing get sensitive. """ self.assertFalse(self.demo.sensitive) self.assertTrue(self.demo.children[0].sensitive) def test_set_sensitive(self): """ Testing set sensitive. """ with self.assertRaises(AttributeError): self.demo.sensitive = True def test_get_showing(self): """ Testing get showing. """ self.assertFalse(self.demo.showing) self.assertTrue(self.demo.children[0].showing) def test_set_showing(self): """ Testing set showing. """ with self.assertRaises(AttributeError): self.demo.showing = True def test_get_visible(self): """ Testing get visible. """ self.assertFalse(self.demo.visible) self.assertTrue(self.demo.children[0].visible) def test_set_visible(self): """ Testing set visible. """ with self.assertRaises(AttributeError): self.demo.visible = True def test_checked(self): """ Testing checked. """ self.run_demo("Application Class") application = root.application("gtk3-demo-application") # Sometimes on wrong coordinates, we can use actions or select. application.child("Preferences", "menu").select() sleep(1) menu_item = application.child("Prefer Dark Theme") self.assertFalse(menu_item.checked) application.child("Prefer Dark Theme").do_action_named("click") sleep(1) menu_item = application.child("Prefer Dark Theme") self.assertTrue(menu_item.checked) def test_dead_empty(self): """ Testing is dead method by empty Node. """ node = Node() self.assertTrue(node.dead) def test_dead_by_dead_application(self): """ Testing is dead method by dead Application. """ self.assertFalse(self.demo.dead) # Close the application. self.process.kill() self.process.wait() sleep(5) self.assertTrue(self.demo.dead) if __name__ == "__main__": unittest.main() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests/test_tree_states_gtk4.py000066400000000000000000000074461520163156700270610ustar00rootroot00000000000000#!/usr/bin/python3 """ Unit tests for the dogtail.tree Node States. """ # pylint: disable=import-outside-toplevel # pylint: disable=wrong-import-position # pylint: disable=wrong-import-order # ruff: noqa: E402 import os import unittest from time import sleep try: from tests.test_gtk_demo import Gtk4DemoTest except ImportError: from test_gtk_demo import Gtk4DemoTest from dogtail.tree import root, Node @unittest.skipIf(not os.path.isfile("/usr/bin/gtk4-demo"), "Skipping, no gtk4-demo.") class TestGtk4DogtailNodeStates(Gtk4DemoTest): """ Class to test dogtail's Node States. """ def test_get_selected(self): """ Testing get selected. """ info_node = self.demo.child("Info") self.assertTrue(info_node.selected) def test_set_selected(self): """ Testing get selected. """ info_node = self.demo.child("Info") with self.assertRaises(AttributeError): info_node.selected = "not possible" def test_get_selectable(self): """ Testing get selectable. """ info_node = self.demo.child("Info") self.assertTrue(info_node.selected) def test_set_selectable(self): """ Testing get selectable. """ info_node = self.demo.child("Info") with self.assertRaises(AttributeError): info_node.selectable = "not possible" def test_get_sensitive(self): """ Testing get sensitive. """ self.assertFalse(self.demo.sensitive) self.assertTrue(self.demo.children[0].sensitive) def test_set_sensitive(self): """ Testing set sensitive. """ with self.assertRaises(AttributeError): self.demo.sensitive = True def test_get_showing(self): """ Testing get showing. """ self.assertFalse(self.demo.showing) self.assertTrue(self.demo.children[0].showing) def test_set_showing(self): """ Testing set showing. """ with self.assertRaises(AttributeError): self.demo.showing = True def test_get_visible(self): """ Testing get visible. """ self.assertFalse(self.demo.visible) self.assertTrue(self.demo.children[0].visible) def test_set_visible(self): """ Testing set visible. """ with self.assertRaises(AttributeError): self.demo.visible = True # For some reason this is also not working in gtk4-demo. # The Preferences is missing from the tree. @unittest.SkipTest def test_checked(self): """ Testing checked. """ clipboard_button = self.demo.child("Application Class") clipboard_button.double_click() application = root.application("gtk4-demo-application") # Sometimes on wrong coordinates, we can use actions or select. application.child("Preferences", "menu").select() sleep(1) menu_item = application.child("Prefer Dark Theme") self.assertFalse(menu_item.checked) application.child("Prefer Dark Theme").do_action_named("click") sleep(1) menu_item = application.child("Prefer Dark Theme") self.assertTrue(menu_item.checked) def test_dead_empty(self): """ Testing is dead method by empty Node. """ node = Node() self.assertTrue(node.dead) def test_dead_by_dead_application(self): """ Testing is dead method by dead Application. """ self.assertFalse(self.demo.dead) # Close the application. self.process.kill() self.process.wait() sleep(5) self.assertTrue(self.demo.dead) if __name__ == "__main__": unittest.main() test_tree_string_representation_gtk3.py000066400000000000000000000031321520163156700321120ustar00rootroot00000000000000dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests#!/usr/bin/python3 """ Unit tests for the dogtail.tree Node String Representation. """ # pylint: disable=import-outside-toplevel # pylint: disable=wrong-import-position # pylint: disable=wrong-import-order # ruff: noqa: E402 import os import unittest try: from tests.test_gtk_demo import Gtk3DemoTest, trap_stdout except ImportError: from test_gtk_demo import Gtk3DemoTest, trap_stdout SOURCE_DUMP_STRING = """[ 'Source' | 'page tab' | '' ] [ '' | 'scroll pane' | '' ] [ '' | 'text' | '' ] [ '' | 'scroll bar' | '' ] [ '' | 'scroll bar' | '' ]""" SOURCE_TREE_STRING = """└──[ 'Source' | 'page tab' | '' ] └──[ '' | 'scroll pane' | '' ] ├──[ '' | 'text' | '' ] ├──[ '' | 'scroll bar' | '' ] └──[ '' | 'scroll bar' | '' ]""" @unittest.skipIf(not os.path.isfile("/usr/bin/gtk3-demo"), "Skipping, no gtk3-demo.") class TestGtk3DogtailNodeValue(Gtk3DemoTest): """ Class to test dogtail's Node String Representation. """ def test_dump_representation(self): """ Testing dump string. """ source_node = self.demo.child("Source") source_node_dump_output = trap_stdout(source_node.dump) self.assertEqual(source_node_dump_output, SOURCE_DUMP_STRING) def test_tree_representation(self): """ Testing tree string. """ source_node = self.demo.child("Source") source_node_tree_output = trap_stdout(source_node.tree) self.assertEqual(source_node_tree_output, SOURCE_TREE_STRING) if __name__ == "__main__": unittest.main() test_tree_string_representation_gtk4.py000066400000000000000000000026021520163156700321140ustar00rootroot00000000000000dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests#!/usr/bin/python3 """ Unit tests for the dogtail.tree Node String Representation. """ # pylint: disable=import-outside-toplevel # pylint: disable=wrong-import-position # pylint: disable=wrong-import-order # ruff: noqa: E402 import os import unittest try: from tests.test_gtk_demo import Gtk4DemoTest, trap_stdout except ImportError: from test_gtk_demo import Gtk4DemoTest, trap_stdout SOURCE_DUMP_STRING = """[ 'Source' | 'page tab' | '' ] [ 'Source' | 'label' | '' ]""" SOURCE_TREE_STRING = """└──[ 'Source' | 'page tab' | '' ] └──[ 'Source' | 'label' | '' ]""" @unittest.skipIf(not os.path.isfile("/usr/bin/gtk4-demo"), "Skipping, no gtk4-demo.") class TestGtk4DogtailNodeValue(Gtk4DemoTest): """ Class to test dogtail's Node String Representation. """ def test_dump_representation(self): """ Testing dump string. """ source_node = self.demo.child("Source") source_node_dump_output = trap_stdout(source_node.dump) self.assertEqual(source_node_dump_output, SOURCE_DUMP_STRING) def test_tree_representation(self): """ Testing tree string. """ source_node = self.demo.child("Source") source_node_tree_output = trap_stdout(source_node.tree) self.assertEqual(source_node_tree_output, SOURCE_TREE_STRING) if __name__ == "__main__": unittest.main() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests/test_tree_value_gtk3.py000066400000000000000000000050531520163156700266610ustar00rootroot00000000000000#!/usr/bin/python3 """ Unit tests for the dogtail.tree Node Values. """ # pylint: disable=import-outside-toplevel # pylint: disable=wrong-import-position # pylint: disable=wrong-import-order # ruff: noqa: E402 import os import unittest try: from tests.test_gtk_demo import Gtk3DemoTest except ImportError: from test_gtk_demo import Gtk3DemoTest @unittest.skipIf(not os.path.isfile("/usr/bin/gtk3-demo"), "Skipping, no gtk3-demo.") class TestGtk3DogtailNodeValue(Gtk3DemoTest): """ Class to test dogtail's Node Value. """ def test_get_value(self): """ Testing get value. """ value_node = self.demo.child(role_name="scroll bar") self.assertEqual(value_node.value, 0) def test_set_value(self): """ Testing set value. """ self.demo.child("Source") value_node = self.demo.find_children( lambda x: x.role_name == "scroll bar" )[-1] value_node.value = 100 self.assertEqual(value_node.value, 100) def test_get_min_value(self): """ Testing get min value. """ value_node = self.demo.child(role_name="scroll bar") self.assertEqual(value_node.min_value, 0) def test_set_min_value(self): """ Testing set min value. """ value_node = self.demo.child(role_name="scroll bar") with self.assertRaises(AttributeError): value_node.min_value = "not possible" def test_get_max_value(self): """ Testing get max value. """ self.demo.child("Source") value_node = self.demo.find_children( lambda x: x.role_name == "scroll bar" )[-1] self.assertTrue(value_node.max_value > 0) def test_set_max_value(self): """ Testing set max value. """ value_node = self.demo.child(role_name="scroll bar") with self.assertRaises(AttributeError): value_node.max_value = "not possible" def test_get_min_value_increment(self): """ Testing get min value increment. """ value_node = self.demo.child(role_name="scroll bar") self.assertNotEqual(value_node.min_value_increment, 0) def test_set_min_value_increment(self): """ Testing set min value increment. """ value_node = self.demo.child(role_name="scroll bar") with self.assertRaises(AttributeError): value_node.min_value_increment = "not possible" if __name__ == "__main__": unittest.main() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests/test_tree_value_gtk4.py000066400000000000000000000051771520163156700266710ustar00rootroot00000000000000#!/usr/bin/python3 """ Unit tests for the dogtail.tree Node Values. """ # pylint: disable=import-outside-toplevel # pylint: disable=wrong-import-position # pylint: disable=wrong-import-order # ruff: noqa: E402 import os import unittest try: from tests.test_gtk_demo import Gtk4DemoTest except ImportError: from test_gtk_demo import Gtk4DemoTest @unittest.skipIf(not os.path.isfile("/usr/bin/gtk4-demo"), "Skipping, no gtk4-demo.") class TestGtk4DogtailNodeValue(Gtk4DemoTest): """ Class to test dogtail's Node Value. """ def test_get_value(self): """ Testing get value. """ value_node = self.demo.child(role_name="scroll bar") self.assertEqual(value_node.value, 0) def test_set_value(self): """ Testing set value. """ self.demo.child("Source") value_node = self.demo.find_children( lambda x: x.role_name == "scroll bar" )[-1] value_node.value = 100 self.assertEqual(value_node.value, 100) def test_get_min_value(self): """ Testing get min value. """ value_node = self.demo.child(role_name="scroll bar") self.assertEqual(value_node.min_value, 0) def test_set_min_value(self): """ Testing set min value. """ value_node = self.demo.child(role_name="scroll bar") with self.assertRaises(AttributeError): value_node.min_value = "not possible" def test_get_max_value(self): """ Testing get max value. """ self.demo.child("Source") value_node = self.demo.find_children( lambda x: x.role_name == "scroll bar" )[-1] self.assertTrue(value_node.max_value > 0) def test_set_max_value(self): """ Testing set max value. """ value_node = self.demo.child(role_name="scroll bar") with self.assertRaises(AttributeError): value_node.max_value = "not possible" # For some reason this is also not working in gtk4-demo. @unittest.SkipTest def test_get_min_value_increment(self): """ Testing get min value increment. """ value_node = self.demo.child(role_name="scroll bar") self.assertNotEqual(value_node.min_value_increment, 0) def test_set_min_value_increment(self): """ Testing set min value increment. """ value_node = self.demo.child(role_name="scroll bar") with self.assertRaises(AttributeError): value_node.min_value_increment = "not possible" if __name__ == "__main__": unittest.main() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests/test_utils_accessibility.py000066400000000000000000000014501520163156700276420ustar00rootroot00000000000000#!/usr/bin/python3 """ Unit tests for the dogtail.utils accessibility. """ import unittest from dogtail.utils import ( enable_accessibility, is_accessibility_enabled, bail_because_accessibility_is_disabled ) class TestAccessibility(unittest.TestCase): """ Class to test dogtail's utils accessibility methods. """ def test_a11y_enable(self): """ Test utils accessibility with enable. """ enable_accessibility() self.assertTrue(is_accessibility_enabled()) def test_bail_when_a11y_disabled(self): """ Test utils accessibility with bail on disabled state. """ with self.assertRaises(SystemExit): bail_because_accessibility_is_disabled() if __name__ == "__main__": unittest.main() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests/test_utils_delay.py000066400000000000000000000025721520163156700261170ustar00rootroot00000000000000#!/usr/bin/python3 """ Unit tests for the dogtail.utils delays. """ import os from time import time import unittest from dogtail.utils import do_delay from dogtail.config import config class TestDelay(unittest.TestCase): """ Class to test dogtail's utils do_delay method. """ def test_do_delay_implicit(self): """ Test utils do_delay function by setting config value. """ config.default_delay = 2.0 start_time = time() do_delay() self.assertTrue(time() - start_time >= 2.0) def test_do_delay_explicit(self): """ Test utils do_delay function by passing value to the method. """ config.default_delay = 2.0 start_time = time() do_delay(2.0) self.assertTrue(time() - start_time >= 2.0) def test_do_delay_debug_sleep(self): """ Test utils do_delay function with debug_sleep option. """ config.default_delay = 2.0 config.debug_sleep = True start_time = time() do_delay(2.0) self.assertTrue(os.path.isfile(config.debug_file)) with open(config.debug_file, "r", encoding="utf-8") as _f: file_content = _f.read() self.assertTrue("Debug Sleep: " in file_content) self.assertTrue(time() - start_time >= 2.0) if __name__ == "__main__": unittest.main() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests/test_utils_lock.py000066400000000000000000000042061520163156700257450ustar00rootroot00000000000000#!/usr/bin/python3 """ Unit tests for the dogtail.utils Lock. """ # ruff: noqa: E501 import unittest import os from dogtail.utils import Lock class TestLock(unittest.TestCase): """ Class to test dogtail's utils Lock Class. """ def tearDown(self): os.system("rm -rf /tmp/dogtail-test.lock*") def test_set_not_randomized_lock(self): """ Test utils Lock not randomized lock. """ test_lock = Lock(lockname="dogtail-test.lock", randomize=False) self.assertEqual(test_lock.lockdir, "/tmp/dogtail-test.lock") self.assertFalse(os.path.isdir(test_lock.lockdir)) test_lock.lock() self.assertTrue(os.path.isdir(test_lock.lockdir)) test_lock.unlock() self.assertFalse(os.path.isdir(test_lock.lockdir)) def test_double_lock(self): """ Test utils Lock double locked. """ test_lock = Lock(lockname="dogtail-test.lock", randomize=False, unlock_on_exit=True) test_lock.lock() with self.assertRaises(OSError): test_lock.lock() def test_double_lock_original_parameter(self): """ Test utils Lock double locked with original parameter. """ test_lock = Lock(lockname="dogtail-test.lock", randomize=False, unlockOnExit=True) test_lock.lock() with self.assertRaises(OSError): test_lock.lock() def test_double_unlock(self): """ Test utils Lock double unlock. """ test_lock = Lock(lockname="dogtail-test.lock", randomize=False) test_lock.lock() test_lock.unlock() with self.assertRaises(OSError): test_lock.unlock() def test_randomize(self): """ Test utils Lock randomized lock. """ test_lock = Lock(lockname="dogtail-test.lock", randomize=True) self.assertIn("/tmp/dogtail-test.lock", test_lock.lockdir) self.assertFalse(os.path.isdir(test_lock.lockdir)) test_lock.lock() self.assertTrue(os.path.isdir(test_lock.lockdir)) test_lock.unlock() self.assertFalse(os.path.isdir(test_lock.lockdir)) dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests/test_utils_run_gtk3.py000066400000000000000000000026511520163156700265530ustar00rootroot00000000000000#!/usr/bin/python3 """ Unit tests for the dogtail.utils run. """ from time import sleep import os import unittest from dogtail.utils import run from dogtail.tree import root @unittest.skipIf(not os.path.isfile("/usr/bin/gtk3-demo"), "Skipping, no gtk3-demo.") class TestGtk3Run(unittest.TestCase): """ Class to test dogtail's utils run method. """ def setUp(self): self.process = None def tearDown(self): if self.process: self.process.kill() self.process.wait() os.system('killall gtk3-demo-application > /dev/null 2>&1') # Sleep just enough to let the app actually die. # AT-SPI doesn't like being hammered too fast. sleep(0.5) def test_run(self): """ Test utils run function. """ self.process = run("gtk3-demo") self.assertIsNotNone(root.application("gtk3-demo")) def test_run_incorrect(self): """ Test utils run function on incorrect script. """ self.process = None with self.assertRaises(OSError): self.process = run("gtk3-not-existing-demo") def test_run_dumb(self): """ Test utils run function with dumb parameter to not check start up. """ self.process = run("gtk3-demo", dumb=True) self.assertIsNotNone(root.application("gtk3-demo")) if __name__ == "__main__": unittest.main() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests/test_utils_run_gtk4.py000066400000000000000000000026511520163156700265540ustar00rootroot00000000000000#!/usr/bin/python3 """ Unit tests for the dogtail.utils run. """ from time import sleep import os import unittest from dogtail.utils import run from dogtail.tree import root @unittest.skipIf(not os.path.isfile("/usr/bin/gtk4-demo"), "Skipping, no gtk4-demo.") class TestGtk4Run(unittest.TestCase): """ Class to test dogtail's utils run method. """ def setUp(self): self.process = None def tearDown(self): if self.process: self.process.kill() self.process.wait() os.system('killall gtk4-demo-application > /dev/null 2>&1') # Sleep just enough to let the app actually die. # AT-SPI doesn't like being hammered too fast. sleep(0.5) def test_run(self): """ Test utils run function. """ self.process = run("gtk4-demo") self.assertIsNotNone(root.application("gtk4-demo")) def test_run_incorrect(self): """ Test utils run function on incorrect script. """ self.process = None with self.assertRaises(OSError): self.process = run("gtk4-not-existing-demo") def test_run_dumb(self): """ Test utils run function with dumb parameter to not check start up. """ self.process = run("gtk4-demo", dumb=True) self.assertIsNotNone(root.application("gtk4-demo")) if __name__ == "__main__": unittest.main() dogtail-v2.0.3-cdfa9c233b596f4c97f8fa7c925015759baa9345/tests/test_version.py000066400000000000000000000115011520163156700252560ustar00rootroot00000000000000#!/usr/bin/python3 """ Unit tests for the dogtail.version module. """ # pylint: disable=line-too-long # ruff: noqa: E501 import unittest import dogtail.version class TestVersion(unittest.TestCase): """ Class to test dogtail's version methods. """ def test_version_from_string_list(self): """ Tests for Version using a list. """ version_instance = dogtail.version.Version([1, 2, 3]) self.assertEqual(str(version_instance), "1.2.3") def test_version_from_string(self): """ Tests for Version using a string. """ version_instance = dogtail.version.Version("1.2.3") self.assertEqual(str(version_instance), "1.2.3") def test_version_from_string_compatibility_method(self): """ Tests for Version using a fromString function. """ version_instance = dogtail.version.Version.fromString("1.2.3") self.assertEqual(str(version_instance), "1.2.3") def test_version_from_string_method(self): """ Tests for Version using a from_string function. """ version_instance = dogtail.version.Version.from_string("1.2.3") self.assertEqual(str(version_instance), "1.2.3") def test_version_less_than(self): """ Tests for Version matching less than values. """ version_base = dogtail.version.Version.from_string("1.2.3") version_less_z = dogtail.version.Version.from_string("1.2.2") version_less_y = dogtail.version.Version.from_string("1.1.3") version_less_x = dogtail.version.Version.from_string("0.2.3") self.assertTrue(version_less_z < version_base) self.assertTrue(version_less_y < version_base) self.assertTrue(version_less_x < version_base) self.assertTrue(version_less_z <= version_base) self.assertTrue(version_less_y <= version_base) self.assertTrue(version_less_x <= version_base) def test_version_more_than(self): """ Tests for Version matching less than values. """ version_base = dogtail.version.Version.from_string("1.2.3") version_less_z = dogtail.version.Version.from_string("1.2.2") version_less_y = dogtail.version.Version.from_string("1.1.3") version_less_x = dogtail.version.Version.from_string("0.2.3") self.assertTrue(version_base > version_less_z) self.assertTrue(version_base > version_less_y) self.assertTrue(version_base > version_less_x) self.assertTrue(version_base >= version_less_z) self.assertTrue(version_base >= version_less_y) self.assertTrue(version_base >= version_less_x) def test_version_equals(self): """ Tests for Version matching equal values. """ version_base = dogtail.version.Version([1, 2, 3]) version_equal = dogtail.version.Version.from_string("1.2.3") version_not_equal = dogtail.version.Version.from_string("1.2.2") self.assertTrue(version_base == version_equal) self.assertFalse(version_base == version_not_equal) self.assertFalse(version_equal == version_not_equal) self.assertTrue(version_base >= version_equal) self.assertTrue(version_base <= version_equal) self.assertTrue(version_base >= version_not_equal) self.assertFalse(version_base <= version_not_equal) self.assertTrue(version_equal >= version_not_equal) self.assertFalse(version_equal <= version_not_equal) self.assertFalse(version_base != version_equal) self.assertFalse(version_equal != version_base) self.assertTrue(version_not_equal != version_base) self.assertTrue(version_not_equal != version_equal) def test_version_constructor_new(self): """ Tests for Version constructor snake case value. """ version_instance = dogtail.version.Version(version_list=[1, 2, 3]) self.assertEqual(str(version_instance), "1.2.3") def test_version_constructor_old(self): """ Tests for Version constructor camelCase value. """ version_instance = dogtail.version.Version(versionList=[1, 2, 3]) self.assertEqual(str(version_instance), "1.2.3") def test_version_constructor_no_values(self): """ Tests for Version constructor no values. """ version_instance = dogtail.version.Version() self.assertEqual(str(version_instance), "") def test_version_constructor_both_values_respect_snake_case(self): """ Tests for Version constructor both values given, respect snake case value. """ version_instance = dogtail.version.Version(version_list=[1, 2, 3], versionList=[4, 5, 6]) self.assertEqual(str(version_instance), "1.2.3") self.assertNotEqual(str(version_instance), "4.5.6")